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
@@ -2,11 +2,13 @@ import asyncio
2
2
  import json
3
3
  import logging
4
4
  import math
5
+ from concurrent.futures import ThreadPoolExecutor
5
6
  from typing import Dict, List, Optional, Union
6
7
 
7
8
  from asgiref.sync import sync_to_async
8
9
  from fastapi import (
9
10
  APIRouter,
11
+ BackgroundTasks,
10
12
  Depends,
11
13
  Header,
12
14
  HTTPException,
@@ -58,6 +60,8 @@ logger = logging.getLogger(__name__)
58
60
 
59
61
  api_content = APIRouter()
60
62
 
63
+ executor = ThreadPoolExecutor()
64
+
61
65
 
62
66
  class File(BaseModel):
63
67
  path: str
@@ -77,6 +81,11 @@ class IndexerInput(BaseModel):
77
81
  docx: Optional[dict[str, bytes]] = None
78
82
 
79
83
 
84
+ async def run_in_executor(func, *args):
85
+ loop = asyncio.get_event_loop()
86
+ return await loop.run_in_executor(executor, func, *args)
87
+
88
+
80
89
  @api_content.put("")
81
90
  @requires(["authenticated"])
82
91
  async def put_content(
@@ -209,6 +218,7 @@ async def set_content_github(
209
218
  @requires(["authenticated"])
210
219
  async def set_content_notion(
211
220
  request: Request,
221
+ background_tasks: BackgroundTasks,
212
222
  updated_config: Union[NotionContentConfig, None],
213
223
  client: Optional[str] = None,
214
224
  ):
@@ -225,6 +235,10 @@ async def set_content_notion(
225
235
  logger.error(e, exc_info=True)
226
236
  raise HTTPException(status_code=500, detail="Failed to set Notion config")
227
237
 
238
+ if updated_config.token:
239
+ # Trigger an async job to configure_content. Let it run without blocking the response.
240
+ background_tasks.add_task(run_in_executor, configure_content, {}, False, SearchType.Notion, user)
241
+
228
242
  update_telemetry_state(
229
243
  request=request,
230
244
  telemetry_type="api",
khoj/routers/api_model.py CHANGED
@@ -40,7 +40,7 @@ def get_user_chat_model(
40
40
  chat_model = ConversationAdapters.get_conversation_config(user)
41
41
 
42
42
  if chat_model is None:
43
- chat_model = ConversationAdapters.get_default_conversation_config()
43
+ chat_model = ConversationAdapters.get_default_conversation_config(user)
44
44
 
45
45
  return Response(status_code=200, content=json.dumps({"id": chat_model.id, "chat_model": chat_model.chat_model}))
46
46
 
khoj/routers/auth.py CHANGED
@@ -80,11 +80,19 @@ async def login_magic_link(request: Request, form: MagicLinkForm):
80
80
  request.session.pop("user", None)
81
81
 
82
82
  email = form.email
83
- user = await aget_or_create_user_by_email(email)
83
+ user, is_new = await aget_or_create_user_by_email(email)
84
84
  unique_id = user.email_verification_code
85
85
 
86
86
  if user:
87
87
  await send_magic_link_email(email, unique_id, request.base_url)
88
+ if is_new:
89
+ update_telemetry_state(
90
+ request=request,
91
+ telemetry_type="api",
92
+ api="create_user",
93
+ metadata={"user_id": str(user.uuid)},
94
+ )
95
+ logger.log(logging.INFO, f"🥳 New User Created: {user.uuid}")
88
96
 
89
97
  return Response(status_code=200)
90
98
 
khoj/routers/helpers.py CHANGED
@@ -39,6 +39,7 @@ from khoj.database.adapters import (
39
39
  AutomationAdapters,
40
40
  ConversationAdapters,
41
41
  EntryAdapters,
42
+ ais_user_subscribed,
42
43
  create_khoj_token,
43
44
  get_khoj_tokens,
44
45
  get_user_name,
@@ -47,6 +48,7 @@ from khoj.database.adapters import (
47
48
  run_with_process_lock,
48
49
  )
49
50
  from khoj.database.models import (
51
+ Agent,
50
52
  ChatModelOptions,
51
53
  ClientApplication,
52
54
  Conversation,
@@ -118,20 +120,20 @@ def is_query_empty(query: str) -> bool:
118
120
  return is_none_or_empty(query.strip())
119
121
 
120
122
 
121
- def validate_conversation_config():
122
- default_config = ConversationAdapters.get_default_conversation_config()
123
+ def validate_conversation_config(user: KhojUser):
124
+ default_config = ConversationAdapters.get_default_conversation_config(user)
123
125
 
124
126
  if default_config is None:
125
- raise HTTPException(status_code=500, detail="Contact the server administrator to set a default chat model.")
127
+ raise HTTPException(status_code=500, detail="Contact the server administrator to add a chat model.")
126
128
 
127
129
  if default_config.model_type == "openai" and not default_config.openai_config:
128
- raise HTTPException(status_code=500, detail="Contact the server administrator to set a default chat model.")
130
+ raise HTTPException(status_code=500, detail="Contact the server administrator to add a chat model.")
129
131
 
130
132
 
131
133
  async def is_ready_to_chat(user: KhojUser):
132
- user_conversation_config = (await ConversationAdapters.aget_user_conversation_config(user)) or (
133
- await ConversationAdapters.aget_default_conversation_config()
134
- )
134
+ user_conversation_config = await ConversationAdapters.aget_user_conversation_config(user)
135
+ if user_conversation_config == None:
136
+ user_conversation_config = await ConversationAdapters.aget_default_conversation_config()
135
137
 
136
138
  if user_conversation_config and user_conversation_config.model_type == ChatModelOptions.ModelType.OFFLINE:
137
139
  chat_model = user_conversation_config.chat_model
@@ -207,7 +209,7 @@ def get_next_url(request: Request) -> str:
207
209
  def construct_chat_history(conversation_history: dict, n: int = 4, agent_name="AI") -> str:
208
210
  chat_history = ""
209
211
  for chat in conversation_history.get("chat", [])[-n:]:
210
- if chat["by"] == "khoj" and chat["intent"].get("type") in ["remember", "reminder"]:
212
+ if chat["by"] == "khoj" and chat["intent"].get("type") in ["remember", "reminder", "summarize"]:
211
213
  chat_history += f"User: {chat['intent']['query']}\n"
212
214
  chat_history += f"{agent_name}: {chat['message']}\n"
213
215
  elif chat["by"] == "khoj" and ("text-to-image" in chat["intent"].get("type")):
@@ -245,20 +247,51 @@ async def agenerate_chat_response(*args):
245
247
  return await loop.run_in_executor(executor, generate_chat_response, *args)
246
248
 
247
249
 
248
- async def acreate_title_from_query(query: str) -> str:
250
+ async def acreate_title_from_query(query: str, user: KhojUser = None) -> str:
249
251
  """
250
252
  Create a title from the given query
251
253
  """
252
254
  title_generation_prompt = prompts.subject_generation.format(query=query)
253
255
 
254
256
  with timer("Chat actor: Generate title from query", logger):
255
- response = await send_message_to_model_wrapper(title_generation_prompt)
257
+ response = await send_message_to_model_wrapper(title_generation_prompt, user=user)
256
258
 
257
259
  return response.strip()
258
260
 
259
261
 
262
+ async def acheck_if_safe_prompt(system_prompt: str, user: KhojUser = None) -> Tuple[bool, str]:
263
+ """
264
+ Check if the system prompt is safe to use
265
+ """
266
+ safe_prompt_check = prompts.personality_prompt_safety_expert.format(prompt=system_prompt)
267
+ is_safe = True
268
+ reason = ""
269
+
270
+ with timer("Chat actor: Check if safe prompt", logger):
271
+ response = await send_message_to_model_wrapper(safe_prompt_check, user=user)
272
+
273
+ response = response.strip()
274
+ try:
275
+ response = json.loads(response)
276
+ is_safe = response.get("safe", "True") == "True"
277
+ if not is_safe:
278
+ reason = response.get("reason", "")
279
+ except Exception:
280
+ logger.error(f"Invalid response for checking safe prompt: {response}")
281
+
282
+ if not is_safe:
283
+ logger.error(f"Unsafe prompt: {system_prompt}. Reason: {reason}")
284
+
285
+ return is_safe, reason
286
+
287
+
260
288
  async def aget_relevant_information_sources(
261
- query: str, conversation_history: dict, is_task: bool, subscribed: bool, uploaded_image_url: str = None
289
+ query: str,
290
+ conversation_history: dict,
291
+ is_task: bool,
292
+ user: KhojUser,
293
+ uploaded_image_url: str = None,
294
+ agent: Agent = None,
262
295
  ):
263
296
  """
264
297
  Given a query, determine which of the available tools the agent should use in order to answer appropriately.
@@ -267,26 +300,34 @@ async def aget_relevant_information_sources(
267
300
  tool_options = dict()
268
301
  tool_options_str = ""
269
302
 
303
+ agent_tools = agent.input_tools if agent else []
304
+
270
305
  for tool, description in tool_descriptions_for_llm.items():
271
306
  tool_options[tool.value] = description
272
- tool_options_str += f'- "{tool.value}": "{description}"\n'
307
+ if len(agent_tools) == 0 or tool.value in agent_tools:
308
+ tool_options_str += f'- "{tool.value}": "{description}"\n'
273
309
 
274
310
  chat_history = construct_chat_history(conversation_history)
275
311
 
276
312
  if uploaded_image_url:
277
313
  query = f"[placeholder for user attached image]\n{query}"
278
314
 
315
+ personality_context = (
316
+ prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
317
+ )
318
+
279
319
  relevant_tools_prompt = prompts.pick_relevant_information_collection_tools.format(
280
320
  query=query,
281
321
  tools=tool_options_str,
282
322
  chat_history=chat_history,
323
+ personality_context=personality_context,
283
324
  )
284
325
 
285
326
  with timer("Chat actor: Infer information sources to refer", logger):
286
327
  response = await send_message_to_model_wrapper(
287
328
  relevant_tools_prompt,
288
329
  response_type="json_object",
289
- subscribed=subscribed,
330
+ user=user,
290
331
  )
291
332
 
292
333
  try:
@@ -300,20 +341,34 @@ async def aget_relevant_information_sources(
300
341
 
301
342
  final_response = [] if not is_task else [ConversationCommand.AutomatedTask]
302
343
  for llm_suggested_tool in response:
303
- if llm_suggested_tool in tool_options.keys():
344
+ # Add a double check to verify it's in the agent list, because the LLM sometimes gets confused by the tool options.
345
+ if llm_suggested_tool in tool_options.keys() and (
346
+ len(agent_tools) == 0 or llm_suggested_tool in agent_tools
347
+ ):
304
348
  # Check whether the tool exists as a valid ConversationCommand
305
349
  final_response.append(ConversationCommand(llm_suggested_tool))
306
350
 
307
351
  if is_none_or_empty(final_response):
308
- final_response = [ConversationCommand.Default]
309
- return final_response
310
- except Exception as e:
352
+ if len(agent_tools) == 0:
353
+ final_response = [ConversationCommand.Default]
354
+ else:
355
+ final_response = [ConversationCommand.General]
356
+ except Exception:
311
357
  logger.error(f"Invalid response for determining relevant tools: {response}")
312
- return [ConversationCommand.Default]
358
+ if len(agent_tools) == 0:
359
+ final_response = [ConversationCommand.Default]
360
+ else:
361
+ final_response = agent_tools
362
+ return final_response
313
363
 
314
364
 
315
365
  async def aget_relevant_output_modes(
316
- query: str, conversation_history: dict, is_task: bool = False, uploaded_image_url: str = None
366
+ query: str,
367
+ conversation_history: dict,
368
+ is_task: bool = False,
369
+ user: KhojUser = None,
370
+ uploaded_image_url: str = None,
371
+ agent: Agent = None,
317
372
  ):
318
373
  """
319
374
  Given a query, determine which of the available tools the agent should use in order to answer appropriately.
@@ -322,26 +377,34 @@ async def aget_relevant_output_modes(
322
377
  mode_options = dict()
323
378
  mode_options_str = ""
324
379
 
380
+ output_modes = agent.output_modes if agent else []
381
+
325
382
  for mode, description in mode_descriptions_for_llm.items():
326
383
  # Do not allow tasks to schedule another task
327
384
  if is_task and mode == ConversationCommand.Automation:
328
385
  continue
329
386
  mode_options[mode.value] = description
330
- mode_options_str += f'- "{mode.value}": "{description}"\n'
387
+ if len(output_modes) == 0 or mode.value in output_modes:
388
+ mode_options_str += f'- "{mode.value}": "{description}"\n'
331
389
 
332
390
  chat_history = construct_chat_history(conversation_history)
333
391
 
334
392
  if uploaded_image_url:
335
393
  query = f"[placeholder for user attached image]\n{query}"
336
394
 
395
+ personality_context = (
396
+ prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
397
+ )
398
+
337
399
  relevant_mode_prompt = prompts.pick_relevant_output_mode.format(
338
400
  query=query,
339
401
  modes=mode_options_str,
340
402
  chat_history=chat_history,
403
+ personality_context=personality_context,
341
404
  )
342
405
 
343
406
  with timer("Chat actor: Infer output mode for chat response", logger):
344
- response = await send_message_to_model_wrapper(relevant_mode_prompt, response_type="json_object")
407
+ response = await send_message_to_model_wrapper(relevant_mode_prompt, response_type="json_object", user=user)
345
408
 
346
409
  try:
347
410
  response = response.strip()
@@ -352,7 +415,9 @@ async def aget_relevant_output_modes(
352
415
  return ConversationCommand.Text
353
416
 
354
417
  output_mode = response["output"]
355
- if output_mode in mode_options.keys():
418
+
419
+ # Add a double check to verify it's in the agent list, because the LLM sometimes gets confused by the tool options.
420
+ if output_mode in mode_options.keys() and (len(output_modes) == 0 or output_mode in output_modes):
356
421
  # Check whether the tool exists as a valid ConversationCommand
357
422
  return ConversationCommand(output_mode)
358
423
 
@@ -364,7 +429,12 @@ async def aget_relevant_output_modes(
364
429
 
365
430
 
366
431
  async def infer_webpage_urls(
367
- q: str, conversation_history: dict, location_data: LocationData, user: KhojUser, uploaded_image_url: str = None
432
+ q: str,
433
+ conversation_history: dict,
434
+ location_data: LocationData,
435
+ user: KhojUser,
436
+ uploaded_image_url: str = None,
437
+ agent: Agent = None,
368
438
  ) -> List[str]:
369
439
  """
370
440
  Infer webpage links from the given query
@@ -374,17 +444,22 @@ async def infer_webpage_urls(
374
444
  chat_history = construct_chat_history(conversation_history)
375
445
 
376
446
  utc_date = datetime.utcnow().strftime("%Y-%m-%d")
447
+ personality_context = (
448
+ prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
449
+ )
450
+
377
451
  online_queries_prompt = prompts.infer_webpages_to_read.format(
378
452
  current_date=utc_date,
379
453
  query=q,
380
454
  chat_history=chat_history,
381
455
  location=location,
382
456
  username=username,
457
+ personality_context=personality_context,
383
458
  )
384
459
 
385
460
  with timer("Chat actor: Infer webpage urls to read", logger):
386
461
  response = await send_message_to_model_wrapper(
387
- online_queries_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object"
462
+ online_queries_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object", user=user
388
463
  )
389
464
 
390
465
  # Validate that the response is a non-empty, JSON-serializable list of URLs
@@ -400,7 +475,12 @@ async def infer_webpage_urls(
400
475
 
401
476
 
402
477
  async def generate_online_subqueries(
403
- q: str, conversation_history: dict, location_data: LocationData, user: KhojUser, uploaded_image_url: str = None
478
+ q: str,
479
+ conversation_history: dict,
480
+ location_data: LocationData,
481
+ user: KhojUser,
482
+ uploaded_image_url: str = None,
483
+ agent: Agent = None,
404
484
  ) -> List[str]:
405
485
  """
406
486
  Generate subqueries from the given query
@@ -410,17 +490,22 @@ async def generate_online_subqueries(
410
490
  chat_history = construct_chat_history(conversation_history)
411
491
 
412
492
  utc_date = datetime.utcnow().strftime("%Y-%m-%d")
493
+ personality_context = (
494
+ prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
495
+ )
496
+
413
497
  online_queries_prompt = prompts.online_search_conversation_subqueries.format(
414
498
  current_date=utc_date,
415
499
  query=q,
416
500
  chat_history=chat_history,
417
501
  location=location,
418
502
  username=username,
503
+ personality_context=personality_context,
419
504
  )
420
505
 
421
506
  with timer("Chat actor: Generate online search subqueries", logger):
422
507
  response = await send_message_to_model_wrapper(
423
- online_queries_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object"
508
+ online_queries_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object", user=user
424
509
  )
425
510
 
426
511
  # Validate that the response is a non-empty, JSON-serializable list
@@ -438,7 +523,9 @@ async def generate_online_subqueries(
438
523
  return [q]
439
524
 
440
525
 
441
- async def schedule_query(q: str, conversation_history: dict, uploaded_image_url: str = None) -> Tuple[str, ...]:
526
+ async def schedule_query(
527
+ q: str, conversation_history: dict, user: KhojUser, uploaded_image_url: str = None
528
+ ) -> Tuple[str, ...]:
442
529
  """
443
530
  Schedule the date, time to run the query. Assume the server timezone is UTC.
444
531
  """
@@ -450,7 +537,7 @@ async def schedule_query(q: str, conversation_history: dict, uploaded_image_url:
450
537
  )
451
538
 
452
539
  raw_response = await send_message_to_model_wrapper(
453
- crontime_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object"
540
+ crontime_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object", user=user
454
541
  )
455
542
 
456
543
  # Validate that the response is a non-empty, JSON-serializable list
@@ -464,33 +551,41 @@ async def schedule_query(q: str, conversation_history: dict, uploaded_image_url:
464
551
  raise AssertionError(f"Invalid response for scheduling query: {raw_response}")
465
552
 
466
553
 
467
- async def extract_relevant_info(q: str, corpus: str, subscribed: bool) -> Union[str, None]:
554
+ async def extract_relevant_info(
555
+ qs: set[str], corpus: str, user: KhojUser = None, agent: Agent = None
556
+ ) -> Union[str, None]:
468
557
  """
469
558
  Extract relevant information for a given query from the target corpus
470
559
  """
471
560
 
472
- if is_none_or_empty(corpus) or is_none_or_empty(q):
561
+ if is_none_or_empty(corpus) or is_none_or_empty(qs):
473
562
  return None
474
563
 
564
+ personality_context = (
565
+ prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
566
+ )
567
+
475
568
  extract_relevant_information = prompts.extract_relevant_information.format(
476
- query=q,
569
+ query=", ".join(qs),
477
570
  corpus=corpus.strip(),
571
+ personality_context=personality_context,
478
572
  )
479
573
 
480
- chat_model: ChatModelOptions = await ConversationAdapters.aget_default_conversation_config()
481
-
482
- with timer("Chat actor: Extract relevant information from data", logger):
483
- response = await send_message_to_model_wrapper(
484
- extract_relevant_information,
485
- prompts.system_prompt_extract_relevant_information,
486
- chat_model_option=chat_model,
487
- subscribed=subscribed,
488
- )
574
+ response = await send_message_to_model_wrapper(
575
+ extract_relevant_information,
576
+ prompts.system_prompt_extract_relevant_information,
577
+ user=user,
578
+ )
489
579
  return response.strip()
490
580
 
491
581
 
492
582
  async def extract_relevant_summary(
493
- q: str, corpus: str, subscribed: bool = False, uploaded_image_url: str = None
583
+ q: str,
584
+ corpus: str,
585
+ conversation_history: dict,
586
+ uploaded_image_url: str = None,
587
+ user: KhojUser = None,
588
+ agent: Agent = None,
494
589
  ) -> Union[str, None]:
495
590
  """
496
591
  Extract relevant information for a given query from the target corpus
@@ -499,19 +594,24 @@ async def extract_relevant_summary(
499
594
  if is_none_or_empty(corpus) or is_none_or_empty(q):
500
595
  return None
501
596
 
597
+ personality_context = (
598
+ prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
599
+ )
600
+
601
+ chat_history = construct_chat_history(conversation_history)
602
+
502
603
  extract_relevant_information = prompts.extract_relevant_summary.format(
503
604
  query=q,
605
+ chat_history=chat_history,
504
606
  corpus=corpus.strip(),
607
+ personality_context=personality_context,
505
608
  )
506
609
 
507
- chat_model: ChatModelOptions = await ConversationAdapters.aget_default_conversation_config()
508
-
509
610
  with timer("Chat actor: Extract relevant information from data", logger):
510
611
  response = await send_message_to_model_wrapper(
511
612
  extract_relevant_information,
512
613
  prompts.system_prompt_extract_relevant_summary,
513
- chat_model_option=chat_model,
514
- subscribed=subscribed,
614
+ user=user,
515
615
  uploaded_image_url=uploaded_image_url,
516
616
  )
517
617
  return response.strip()
@@ -524,14 +624,18 @@ async def generate_better_image_prompt(
524
624
  note_references: List[Dict[str, Any]],
525
625
  online_results: Optional[dict] = None,
526
626
  model_type: Optional[str] = None,
527
- subscribed: bool = False,
528
627
  uploaded_image_url: Optional[str] = None,
628
+ user: KhojUser = None,
629
+ agent: Agent = None,
529
630
  ) -> str:
530
631
  """
531
632
  Generate a better image prompt from the given query
532
633
  """
533
634
 
534
635
  today_date = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d, %A")
636
+ personality_context = (
637
+ prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
638
+ )
535
639
  model_type = model_type or TextToImageModelConfig.ModelType.OPENAI
536
640
 
537
641
  if location_data:
@@ -558,6 +662,7 @@ async def generate_better_image_prompt(
558
662
  current_date=today_date,
559
663
  references=user_references,
560
664
  online_results=simplified_online_results,
665
+ personality_context=personality_context,
561
666
  )
562
667
  elif model_type in [TextToImageModelConfig.ModelType.STABILITYAI, TextToImageModelConfig.ModelType.REPLICATE]:
563
668
  image_prompt = prompts.image_generation_improve_prompt_sd.format(
@@ -567,14 +672,11 @@ async def generate_better_image_prompt(
567
672
  current_date=today_date,
568
673
  references=user_references,
569
674
  online_results=simplified_online_results,
675
+ personality_context=personality_context,
570
676
  )
571
677
 
572
- chat_model: ChatModelOptions = await ConversationAdapters.aget_default_conversation_config()
573
-
574
678
  with timer("Chat actor: Generate contextual image prompt", logger):
575
- response = await send_message_to_model_wrapper(
576
- image_prompt, chat_model_option=chat_model, subscribed=subscribed, uploaded_image_url=uploaded_image_url
577
- )
679
+ response = await send_message_to_model_wrapper(image_prompt, uploaded_image_url=uploaded_image_url, user=user)
578
680
  response = response.strip()
579
681
  if response.startswith(('"', "'")) and response.endswith(('"', "'")):
580
682
  response = response[1:-1]
@@ -586,14 +688,10 @@ async def send_message_to_model_wrapper(
586
688
  message: str,
587
689
  system_message: str = "",
588
690
  response_type: str = "text",
589
- chat_model_option: ChatModelOptions = None,
590
- subscribed: bool = False,
691
+ user: KhojUser = None,
591
692
  uploaded_image_url: str = None,
592
693
  ):
593
- conversation_config: ChatModelOptions = (
594
- chat_model_option or await ConversationAdapters.aget_default_conversation_config()
595
- )
596
-
694
+ conversation_config: ChatModelOptions = await ConversationAdapters.aget_default_conversation_config(user)
597
695
  vision_available = conversation_config.vision_enabled
598
696
  if not vision_available and uploaded_image_url:
599
697
  vision_enabled_config = await ConversationAdapters.aget_vision_enabled_config()
@@ -601,6 +699,7 @@ async def send_message_to_model_wrapper(
601
699
  conversation_config = vision_enabled_config
602
700
  vision_available = True
603
701
 
702
+ subscribed = await ais_user_subscribed(user)
604
703
  chat_model = conversation_config.chat_model
605
704
  max_tokens = (
606
705
  conversation_config.subscribed_max_prompt_size
@@ -651,15 +750,13 @@ async def send_message_to_model_wrapper(
651
750
  model_type=conversation_config.model_type,
652
751
  )
653
752
 
654
- openai_response = send_message_to_model(
753
+ return send_message_to_model(
655
754
  messages=truncated_messages,
656
755
  api_key=api_key,
657
756
  model=chat_model,
658
757
  response_type=response_type,
659
758
  api_base_url=api_base_url,
660
759
  )
661
-
662
- return openai_response
663
760
  elif model_type == ChatModelOptions.ModelType.ANTHROPIC:
664
761
  api_key = conversation_config.openai_config.api_key
665
762
  truncated_messages = generate_chatml_messages_with_context(
@@ -701,8 +798,9 @@ def send_message_to_model_wrapper_sync(
701
798
  message: str,
702
799
  system_message: str = "",
703
800
  response_type: str = "text",
801
+ user: KhojUser = None,
704
802
  ):
705
- conversation_config: ChatModelOptions = ConversationAdapters.get_default_conversation_config()
803
+ conversation_config: ChatModelOptions = ConversationAdapters.get_default_conversation_config(user)
706
804
 
707
805
  if conversation_config is None:
708
806
  raise HTTPException(status_code=500, detail="Contact the server administrator to set a default chat model.")
@@ -942,13 +1040,23 @@ class ApiUserRateLimiter:
942
1040
 
943
1041
  # Check if the user has exceeded the rate limit
944
1042
  if subscribed and count_requests >= self.subscribed_requests:
1043
+ logger.info(
1044
+ f"Rate limit: {count_requests} requests in {self.window} seconds for user: {user}. Limit is {self.subscribed_requests} requests."
1045
+ )
945
1046
  raise HTTPException(status_code=429, detail="Slow down! Too Many Requests")
946
1047
  if not subscribed and count_requests >= self.requests:
947
1048
  if self.requests >= self.subscribed_requests:
1049
+ logger.info(
1050
+ f"Rate limit: {count_requests} requests in {self.window} seconds for user: {user}. Limit is {self.subscribed_requests} requests."
1051
+ )
948
1052
  raise HTTPException(
949
1053
  status_code=429,
950
1054
  detail="Slow down! Too Many Requests",
951
1055
  )
1056
+
1057
+ logger.info(
1058
+ f"Rate limit: {count_requests} requests in {self.window} seconds for user: {user}. Limit is {self.subscribed_requests} requests."
1059
+ )
952
1060
  raise HTTPException(
953
1061
  status_code=429,
954
1062
  detail="We're glad you're enjoying Khoj! You've exceeded your usage limit for today. Come back tomorrow or subscribe to increase your usage limit via [your settings](https://app.khoj.dev/settings).",
@@ -986,6 +1094,9 @@ class ConversationCommandRateLimiter:
986
1094
  ).acount()
987
1095
 
988
1096
  if subscribed and count_requests >= self.subscribed_rate_limit:
1097
+ logger.info(
1098
+ f"Rate limit: {count_requests} requests in 24 hours for user: {user}. Limit is {self.subscribed_rate_limit} requests."
1099
+ )
989
1100
  raise HTTPException(status_code=429, detail="Slow down! Too Many Requests")
990
1101
  if not subscribed and count_requests >= self.trial_rate_limit:
991
1102
  raise HTTPException(
@@ -1068,7 +1179,7 @@ class CommonQueryParamsClass:
1068
1179
  CommonQueryParams = Annotated[CommonQueryParamsClass, Depends()]
1069
1180
 
1070
1181
 
1071
- def should_notify(original_query: str, executed_query: str, ai_response: str) -> bool:
1182
+ def should_notify(original_query: str, executed_query: str, ai_response: str, user: KhojUser) -> bool:
1072
1183
  """
1073
1184
  Decide whether to notify the user of the AI response.
1074
1185
  Default to notifying the user for now.
@@ -1085,7 +1196,7 @@ def should_notify(original_query: str, executed_query: str, ai_response: str) ->
1085
1196
  with timer("Chat actor: Decide to notify user of automation response", logger):
1086
1197
  try:
1087
1198
  # TODO Replace with async call so we don't have to maintain a sync version
1088
- response = send_message_to_model_wrapper_sync(to_notify_or_not)
1199
+ response = send_message_to_model_wrapper_sync(to_notify_or_not, user)
1089
1200
  should_notify_result = "no" not in response.lower()
1090
1201
  logger.info(f'Decided to {"not " if not should_notify_result else ""}notify user of automation response.')
1091
1202
  return should_notify_result
@@ -1177,7 +1288,9 @@ def scheduled_chat(
1177
1288
  ai_response = raw_response.text
1178
1289
 
1179
1290
  # Notify user if the AI response is satisfactory
1180
- if should_notify(original_query=scheduling_request, executed_query=cleaned_query, ai_response=ai_response):
1291
+ if should_notify(
1292
+ original_query=scheduling_request, executed_query=cleaned_query, ai_response=ai_response, user=user
1293
+ ):
1181
1294
  if is_resend_enabled():
1182
1295
  send_task_email(user.get_short_name(), user.email, cleaned_query, ai_response, subject, is_image)
1183
1296
  else:
@@ -1187,7 +1300,7 @@ def scheduled_chat(
1187
1300
  async def create_automation(
1188
1301
  q: str, timezone: str, user: KhojUser, calling_url: URL, meta_log: dict = {}, conversation_id: str = None
1189
1302
  ):
1190
- crontime, query_to_run, subject = await schedule_query(q, meta_log)
1303
+ crontime, query_to_run, subject = await schedule_query(q, meta_log, user)
1191
1304
  job = await schedule_automation(query_to_run, subject, crontime, timezone, q, user, calling_url, conversation_id)
1192
1305
  return job, crontime, query_to_run, subject
1193
1306
 
@@ -1381,9 +1494,9 @@ def get_user_config(user: KhojUser, request: Request, is_detailed: bool = False)
1381
1494
  current_notion_config = get_user_notion_config(user)
1382
1495
  notion_token = current_notion_config.token if current_notion_config else ""
1383
1496
 
1384
- selected_chat_model_config = (
1385
- ConversationAdapters.get_conversation_config(user) or ConversationAdapters.get_default_conversation_config()
1386
- )
1497
+ selected_chat_model_config = ConversationAdapters.get_conversation_config(
1498
+ user
1499
+ ) or ConversationAdapters.get_default_conversation_config(user)
1387
1500
  chat_models = ConversationAdapters.get_conversation_processor_options().all()
1388
1501
  chat_model_options = list()
1389
1502
  for chat_model in chat_models: