khoj 1.31.0__py3-none-any.whl → 1.31.1.dev62__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 (133) hide show
  1. khoj/configure.py +4 -2
  2. khoj/database/adapters/__init__.py +66 -57
  3. khoj/database/admin.py +9 -9
  4. khoj/database/migrations/0077_chatmodel_alter_agent_chat_model_and_more.py +62 -0
  5. khoj/database/migrations/0078_khojuser_email_verification_code_expiry.py +17 -0
  6. khoj/database/models/__init__.py +8 -7
  7. khoj/interface/compiled/404/index.html +1 -1
  8. khoj/interface/compiled/_next/static/9O0zlbSu9rZ459NKSv2aS/_buildManifest.js +1 -0
  9. khoj/interface/compiled/_next/static/chunks/1201-aac5b5f9a28edf09.js +1 -0
  10. khoj/interface/compiled/_next/static/chunks/1662-adf4c615bef2fdc2.js +1 -0
  11. khoj/interface/compiled/_next/static/chunks/1915-878efdc6db697d8f.js +1 -0
  12. khoj/interface/compiled/_next/static/chunks/2117-9886e6a0232dc093.js +2 -0
  13. khoj/interface/compiled/_next/static/chunks/{5538-0ea2d3944ca051e1.js → 2264-23b2c33cd8c74d07.js} +1 -1
  14. khoj/interface/compiled/_next/static/chunks/2781-4f022b6e9eb6df6e.js +3 -0
  15. khoj/interface/compiled/_next/static/chunks/2813-f842b08bce4c61a0.js +1 -0
  16. khoj/interface/compiled/_next/static/chunks/3091-e0ff2288e8a29dd7.js +1 -0
  17. khoj/interface/compiled/_next/static/chunks/3727.dcea8f2193111552.js +1 -0
  18. khoj/interface/compiled/_next/static/chunks/5401-980a4f512c81232e.js +20 -0
  19. khoj/interface/compiled/_next/static/chunks/{1279-4cb23143aa2c0228.js → 5473-b1cf56dedac6577a.js} +1 -1
  20. khoj/interface/compiled/_next/static/chunks/5477-8d032883aed8a2d2.js +1 -0
  21. khoj/interface/compiled/_next/static/chunks/6589-f806113de469d684.js +1 -0
  22. khoj/interface/compiled/_next/static/chunks/8117-2e1697b782c5f185.js +1 -0
  23. khoj/interface/compiled/_next/static/chunks/8407-af326f8c200e619b.js +1 -0
  24. khoj/interface/compiled/_next/static/chunks/8667-d3e5bc726e4ff4e3.js +1 -0
  25. khoj/interface/compiled/_next/static/chunks/9058-25ef3344805f06ea.js +1 -0
  26. khoj/interface/compiled/_next/static/chunks/9262-21c17de77aafdce8.js +1 -0
  27. khoj/interface/compiled/_next/static/chunks/94ca1967.1d9b42d929a1ee8c.js +1 -0
  28. khoj/interface/compiled/_next/static/chunks/{1210.ef7a0f9a7e43da1d.js → 9597.83583248dfbf6e73.js} +1 -1
  29. khoj/interface/compiled/_next/static/chunks/964ecbae.51d6faf8801d15e6.js +1 -0
  30. khoj/interface/compiled/_next/static/chunks/app/_not-found/{page-cfba071f5a657256.js → page-a834eddae3e235df.js} +1 -1
  31. khoj/interface/compiled/_next/static/chunks/app/agents/layout-e49165209d2e406c.js +1 -0
  32. khoj/interface/compiled/_next/static/chunks/app/agents/page-6f4ff1d32a66ed71.js +1 -0
  33. khoj/interface/compiled/_next/static/chunks/app/automations/{layout-7f1b79a2c67af0b4.js → layout-dce809da279a4a8a.js} +1 -1
  34. khoj/interface/compiled/_next/static/chunks/app/automations/page-148a48ddfb2ff90d.js +1 -0
  35. khoj/interface/compiled/_next/static/chunks/app/chat/layout-33934fc2d6ae6838.js +1 -0
  36. khoj/interface/compiled/_next/static/chunks/app/chat/page-be00870a40de3a25.js +1 -0
  37. khoj/interface/compiled/_next/static/chunks/app/layout-30e7fda7262713ce.js +1 -0
  38. khoj/interface/compiled/_next/static/chunks/app/page-765292332c31523e.js +1 -0
  39. khoj/interface/compiled/_next/static/chunks/app/search/layout-c02531d586972d7d.js +1 -0
  40. khoj/interface/compiled/_next/static/chunks/app/search/{page-bd47c769b7700d1d.js → page-7af2cab294dccd81.js} +1 -1
  41. khoj/interface/compiled/_next/static/chunks/app/settings/layout-b3f6bc6f1aa118e0.js +1 -0
  42. khoj/interface/compiled/_next/static/chunks/app/settings/page-6b600bf11fa89194.js +1 -0
  43. khoj/interface/compiled/_next/static/chunks/app/share/chat/layout-6fb51c5c80f8ec67.js +1 -0
  44. khoj/interface/compiled/_next/static/chunks/app/share/chat/{page-751695d28116e626.js → page-6054e88b56708f44.js} +1 -1
  45. khoj/interface/compiled/_next/static/chunks/d3ac728e-44ebd2a0c99b12a0.js +1 -0
  46. khoj/interface/compiled/_next/static/chunks/{fd9d1056-2e6c8140e79afc3b.js → fd9d1056-4482b99a36fd1673.js} +1 -1
  47. khoj/interface/compiled/_next/static/chunks/main-app-de1f09df97a3cfc7.js +1 -0
  48. khoj/interface/compiled/_next/static/chunks/main-db4bfac6b0a8d00b.js +1 -0
  49. khoj/interface/compiled/_next/static/chunks/pages/{_app-f870474a17b7f2fd.js → _app-3c9ca398d360b709.js} +1 -1
  50. khoj/interface/compiled/_next/static/chunks/pages/{_error-c66a4e8afc46f17b.js → _error-cf5ca766ac8f493f.js} +1 -1
  51. khoj/interface/compiled/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  52. khoj/interface/compiled/_next/static/chunks/webpack-afd772b9f7c34b3f.js +1 -0
  53. khoj/interface/compiled/_next/static/css/3f27c3cf45375eb5.css +1 -0
  54. khoj/interface/compiled/_next/static/css/65ac59e147eb2057.css +25 -0
  55. khoj/interface/compiled/_next/static/css/8a00c3799ec0c9f8.css +1 -0
  56. khoj/interface/compiled/_next/static/css/{e8fb39147bff7bb4.css → 9504108437df6804.css} +1 -1
  57. khoj/interface/compiled/agents/index.html +1 -1
  58. khoj/interface/compiled/agents/index.txt +6 -6
  59. khoj/interface/compiled/automations/index.html +1 -1
  60. khoj/interface/compiled/automations/index.txt +7 -7
  61. khoj/interface/compiled/chat/index.html +1 -1
  62. khoj/interface/compiled/chat/index.txt +6 -6
  63. khoj/interface/compiled/index.html +1 -1
  64. khoj/interface/compiled/index.txt +6 -6
  65. khoj/interface/compiled/search/index.html +1 -1
  66. khoj/interface/compiled/search/index.txt +6 -6
  67. khoj/interface/compiled/settings/index.html +1 -1
  68. khoj/interface/compiled/settings/index.txt +8 -8
  69. khoj/interface/compiled/share/chat/index.html +1 -1
  70. khoj/interface/compiled/share/chat/index.txt +6 -6
  71. khoj/interface/email/magic_link.html +36 -13
  72. khoj/main.py +1 -1
  73. khoj/migrations/migrate_server_pg.py +7 -7
  74. khoj/processor/conversation/anthropic/anthropic_chat.py +3 -3
  75. khoj/processor/conversation/google/gemini_chat.py +3 -3
  76. khoj/processor/conversation/offline/chat_model.py +12 -12
  77. khoj/processor/conversation/openai/gpt.py +4 -4
  78. khoj/processor/conversation/openai/utils.py +18 -10
  79. khoj/processor/conversation/utils.py +4 -4
  80. khoj/processor/tools/online_search.py +49 -2
  81. khoj/routers/api.py +22 -27
  82. khoj/routers/api_agents.py +4 -4
  83. khoj/routers/api_chat.py +19 -12
  84. khoj/routers/api_model.py +4 -4
  85. khoj/routers/auth.py +94 -7
  86. khoj/routers/email.py +10 -14
  87. khoj/routers/helpers.py +176 -134
  88. khoj/routers/web_client.py +1 -1
  89. khoj/utils/helpers.py +5 -3
  90. khoj/utils/initialization.py +28 -26
  91. {khoj-1.31.0.dist-info → khoj-1.31.1.dev62.dist-info}/METADATA +5 -5
  92. {khoj-1.31.0.dist-info → khoj-1.31.1.dev62.dist-info}/RECORD +96 -93
  93. {khoj-1.31.0.dist-info → khoj-1.31.1.dev62.dist-info}/WHEEL +1 -1
  94. khoj/interface/compiled/_next/static/SHDrv3iet5TKNwccvVt6m/_buildManifest.js +0 -1
  95. khoj/interface/compiled/_next/static/chunks/1459.690bf20e7d7b7090.js +0 -1
  96. khoj/interface/compiled/_next/static/chunks/1603-f8ef9930c1f4eaef.js +0 -1
  97. khoj/interface/compiled/_next/static/chunks/1970-1b63ac1497b03a10.js +0 -1
  98. khoj/interface/compiled/_next/static/chunks/2646-92ba433951d02d52.js +0 -20
  99. khoj/interface/compiled/_next/static/chunks/3072-be830e4f8412b9d2.js +0 -1
  100. khoj/interface/compiled/_next/static/chunks/3463-081c031e873b7966.js +0 -3
  101. khoj/interface/compiled/_next/static/chunks/3690-51312931ba1eae30.js +0 -1
  102. khoj/interface/compiled/_next/static/chunks/3717-b46079dbe9f55694.js +0 -1
  103. khoj/interface/compiled/_next/static/chunks/4200-ea75740bb3c6ae60.js +0 -1
  104. khoj/interface/compiled/_next/static/chunks/4504-62ac13e7d94c52f9.js +0 -1
  105. khoj/interface/compiled/_next/static/chunks/4602-460621c3241e0d13.js +0 -1
  106. khoj/interface/compiled/_next/static/chunks/5512-7cc62049bbe60e11.js +0 -1
  107. khoj/interface/compiled/_next/static/chunks/7023-e8de2bded4df6539.js +0 -2
  108. khoj/interface/compiled/_next/static/chunks/7592-a09c39a38e60634b.js +0 -1
  109. khoj/interface/compiled/_next/static/chunks/8423-1dda16bc56236523.js +0 -1
  110. khoj/interface/compiled/_next/static/chunks/94ca1967.5584df65931cfe83.js +0 -1
  111. khoj/interface/compiled/_next/static/chunks/964ecbae.ea4eab2a3a835ffe.js +0 -1
  112. khoj/interface/compiled/_next/static/chunks/app/agents/layout-1878cc328ea380bd.js +0 -1
  113. khoj/interface/compiled/_next/static/chunks/app/agents/page-379949e11f084cf5.js +0 -1
  114. khoj/interface/compiled/_next/static/chunks/app/automations/page-ca10c1cf79ae54bb.js +0 -1
  115. khoj/interface/compiled/_next/static/chunks/app/chat/layout-1072c3b0ab136e74.js +0 -1
  116. khoj/interface/compiled/_next/static/chunks/app/chat/page-8a87c5de878f4f44.js +0 -1
  117. khoj/interface/compiled/_next/static/chunks/app/layout-6310c57b674dd6f5.js +0 -1
  118. khoj/interface/compiled/_next/static/chunks/app/page-b09139cb91859cd7.js +0 -1
  119. khoj/interface/compiled/_next/static/chunks/app/search/layout-cae84c87073877f0.js +0 -1
  120. khoj/interface/compiled/_next/static/chunks/app/settings/layout-f285795bc3154b8c.js +0 -1
  121. khoj/interface/compiled/_next/static/chunks/app/settings/page-2a2679b6e10dbac1.js +0 -1
  122. khoj/interface/compiled/_next/static/chunks/app/share/chat/layout-3b0c60bc13a963db.js +0 -1
  123. khoj/interface/compiled/_next/static/chunks/d3ac728e-a9e3522eef9b6b28.js +0 -1
  124. khoj/interface/compiled/_next/static/chunks/main-1ea5c2e0fdef4626.js +0 -1
  125. khoj/interface/compiled/_next/static/chunks/main-app-6d6ee3495efe03d4.js +0 -1
  126. khoj/interface/compiled/_next/static/chunks/polyfills-78c92fac7aa8fdd8.js +0 -1
  127. khoj/interface/compiled/_next/static/chunks/webpack-4bd818a6399690ae.js +0 -1
  128. khoj/interface/compiled/_next/static/css/1f293605f2871853.css +0 -1
  129. khoj/interface/compiled/_next/static/css/592ca99f5122e75a.css +0 -1
  130. khoj/interface/compiled/_next/static/css/fd628f01a581ec3c.css +0 -25
  131. /khoj/interface/compiled/_next/static/{SHDrv3iet5TKNwccvVt6m → 9O0zlbSu9rZ459NKSv2aS}/_ssgManifest.js +0 -0
  132. {khoj-1.31.0.dist-info → khoj-1.31.1.dev62.dist-info}/entry_points.txt +0 -0
  133. {khoj-1.31.0.dist-info → khoj-1.31.1.dev62.dist-info}/licenses/LICENSE +0 -0
khoj/routers/helpers.py CHANGED
@@ -49,6 +49,7 @@ from khoj.database.adapters import (
49
49
  ais_user_subscribed,
50
50
  create_khoj_token,
51
51
  get_khoj_tokens,
52
+ get_user_by_email,
52
53
  get_user_name,
53
54
  get_user_notion_config,
54
55
  get_user_subscription_state,
@@ -56,7 +57,7 @@ from khoj.database.adapters import (
56
57
  )
57
58
  from khoj.database.models import (
58
59
  Agent,
59
- ChatModelOptions,
60
+ ChatModel,
60
61
  ClientApplication,
61
62
  Conversation,
62
63
  GithubConfig,
@@ -133,40 +134,40 @@ def is_query_empty(query: str) -> bool:
133
134
  return is_none_or_empty(query.strip())
134
135
 
135
136
 
136
- def validate_conversation_config(user: KhojUser):
137
- default_config = ConversationAdapters.get_default_conversation_config(user)
137
+ def validate_chat_model(user: KhojUser):
138
+ default_chat_model = ConversationAdapters.get_default_chat_model(user)
138
139
 
139
- if default_config is None:
140
+ if default_chat_model is None:
140
141
  raise HTTPException(status_code=500, detail="Contact the server administrator to add a chat model.")
141
142
 
142
- if default_config.model_type == "openai" and not default_config.ai_model_api:
143
+ if default_chat_model.model_type == "openai" and not default_chat_model.ai_model_api:
143
144
  raise HTTPException(status_code=500, detail="Contact the server administrator to add a chat model.")
144
145
 
145
146
 
146
147
  async def is_ready_to_chat(user: KhojUser):
147
- user_conversation_config = await ConversationAdapters.aget_user_conversation_config(user)
148
- if user_conversation_config == None:
149
- user_conversation_config = await ConversationAdapters.aget_default_conversation_config(user)
148
+ user_chat_model = await ConversationAdapters.aget_user_chat_model(user)
149
+ if user_chat_model == None:
150
+ user_chat_model = await ConversationAdapters.aget_default_chat_model(user)
150
151
 
151
- if user_conversation_config and user_conversation_config.model_type == ChatModelOptions.ModelType.OFFLINE:
152
- chat_model = user_conversation_config.chat_model
153
- max_tokens = user_conversation_config.max_prompt_size
152
+ if user_chat_model and user_chat_model.model_type == ChatModel.ModelType.OFFLINE:
153
+ chat_model_name = user_chat_model.name
154
+ max_tokens = user_chat_model.max_prompt_size
154
155
  if state.offline_chat_processor_config is None:
155
156
  logger.info("Loading Offline Chat Model...")
156
- state.offline_chat_processor_config = OfflineChatProcessorModel(chat_model, max_tokens)
157
+ state.offline_chat_processor_config = OfflineChatProcessorModel(chat_model_name, max_tokens)
157
158
  return True
158
159
 
159
160
  if (
160
- user_conversation_config
161
+ user_chat_model
161
162
  and (
162
- user_conversation_config.model_type
163
+ user_chat_model.model_type
163
164
  in [
164
- ChatModelOptions.ModelType.OPENAI,
165
- ChatModelOptions.ModelType.ANTHROPIC,
166
- ChatModelOptions.ModelType.GOOGLE,
165
+ ChatModel.ModelType.OPENAI,
166
+ ChatModel.ModelType.ANTHROPIC,
167
+ ChatModel.ModelType.GOOGLE,
167
168
  ]
168
169
  )
169
- and user_conversation_config.ai_model_api
170
+ and user_chat_model.ai_model_api
170
171
  ):
171
172
  return True
172
173
 
@@ -230,7 +231,7 @@ def get_next_url(request: Request) -> str:
230
231
  return urljoin(str(request.base_url).rstrip("/"), next_path)
231
232
 
232
233
 
233
- def get_conversation_command(query: str, any_references: bool = False) -> ConversationCommand:
234
+ def get_conversation_command(query: str) -> ConversationCommand:
234
235
  if query.startswith("/notes"):
235
236
  return ConversationCommand.Notes
236
237
  elif query.startswith("/help"):
@@ -253,9 +254,6 @@ def get_conversation_command(query: str, any_references: bool = False) -> Conver
253
254
  return ConversationCommand.Code
254
255
  elif query.startswith("/research"):
255
256
  return ConversationCommand.Research
256
- # If no relevant notes found for the given query
257
- elif not any_references:
258
- return ConversationCommand.General
259
257
  else:
260
258
  return ConversationCommand.Default
261
259
 
@@ -407,42 +405,39 @@ async def aget_data_sources_and_output_format(
407
405
  response = clean_json(response)
408
406
  response = json.loads(response)
409
407
 
410
- selected_sources = [q.strip() for q in response.get("source", []) if q.strip()]
411
- selected_output = response.get("output", "text").strip() # Default to text output
408
+ chosen_sources = [s.strip() for s in response.get("source", []) if s.strip()]
409
+ chosen_output = response.get("output", "text").strip() # Default to text output
412
410
 
413
- if not isinstance(selected_sources, list) or not selected_sources or len(selected_sources) == 0:
411
+ if is_none_or_empty(chosen_sources) or not isinstance(chosen_sources, list):
414
412
  raise ValueError(
415
- f"Invalid response for determining relevant tools: {selected_sources}. Raw Response: {response}"
413
+ f"Invalid response for determining relevant tools: {chosen_sources}. Raw Response: {response}"
416
414
  )
417
415
 
418
- result: Dict = {"sources": [], "output": None if not is_task else ConversationCommand.AutomatedTask}
419
- for selected_source in selected_sources:
420
- # Add a double check to verify it's in the agent list, because the LLM sometimes gets confused by the tool options.
421
- if (
422
- selected_source in source_options.keys()
423
- and isinstance(result["sources"], list)
424
- and (len(agent_sources) == 0 or selected_source in agent_sources)
425
- ):
426
- # Check whether the tool exists as a valid ConversationCommand
427
- result["sources"].append(ConversationCommand(selected_source))
428
-
429
- # Add a double check to verify it's in the agent list, because the LLM sometimes gets confused by the tool options.
430
- if selected_output in output_options.keys() and (len(agent_outputs) == 0 or selected_output in agent_outputs):
431
- # Check whether the tool exists as a valid ConversationCommand
432
- result["output"] = ConversationCommand(selected_output)
433
-
434
- if is_none_or_empty(result):
416
+ output_mode = ConversationCommand.Text
417
+ # Verify selected output mode is enabled for the agent, as the LLM can sometimes get confused by the tool options.
418
+ if chosen_output in output_options.keys() and (len(agent_outputs) == 0 or chosen_output in agent_outputs):
419
+ # Ensure that the chosen output mode exists as a valid ConversationCommand
420
+ output_mode = ConversationCommand(chosen_output)
421
+
422
+ data_sources = []
423
+ # Verify selected data sources are enabled for the agent, as the LLM can sometimes get confused by the tool options.
424
+ for chosen_source in chosen_sources:
425
+ # Ensure that the chosen data source exists as a valid ConversationCommand
426
+ if chosen_source in source_options.keys() and (len(agent_sources) == 0 or chosen_source in agent_sources):
427
+ data_sources.append(ConversationCommand(chosen_source))
428
+
429
+ # Fallback to default sources if the inferred data sources are unset or invalid
430
+ if is_none_or_empty(data_sources):
435
431
  if len(agent_sources) == 0:
436
- result = {"sources": [ConversationCommand.Default], "output": ConversationCommand.Text}
432
+ data_sources = [ConversationCommand.Default]
437
433
  else:
438
- result = {"sources": [ConversationCommand.General], "output": ConversationCommand.Text}
434
+ data_sources = [ConversationCommand.General]
439
435
  except Exception as e:
440
436
  logger.error(f"Invalid response for determining relevant tools: {response}. Error: {e}", exc_info=True)
441
- sources = agent_sources if len(agent_sources) > 0 else [ConversationCommand.Default]
442
- output = agent_outputs[0] if len(agent_outputs) > 0 else ConversationCommand.Text
443
- result = {"sources": sources, "output": output}
437
+ data_sources = agent_sources if len(agent_sources) > 0 else [ConversationCommand.Default]
438
+ output_mode = agent_outputs[0] if len(agent_outputs) > 0 else ConversationCommand.Text
444
439
 
445
- return result
440
+ return {"sources": data_sources, "output": output_mode}
446
441
 
447
442
 
448
443
  async def infer_webpage_urls(
@@ -942,120 +937,124 @@ async def send_message_to_model_wrapper(
942
937
  query_files: str = None,
943
938
  tracer: dict = {},
944
939
  ):
945
- conversation_config: ChatModelOptions = await ConversationAdapters.aget_default_conversation_config(user)
946
- vision_available = conversation_config.vision_enabled
940
+ chat_model: ChatModel = await ConversationAdapters.aget_default_chat_model(user)
941
+ vision_available = chat_model.vision_enabled
947
942
  if not vision_available and query_images:
948
- logger.warning(f"Vision is not enabled for default model: {conversation_config.chat_model}.")
943
+ logger.warning(f"Vision is not enabled for default model: {chat_model.name}.")
949
944
  vision_enabled_config = await ConversationAdapters.aget_vision_enabled_config()
950
945
  if vision_enabled_config:
951
- conversation_config = vision_enabled_config
946
+ chat_model = vision_enabled_config
952
947
  vision_available = True
953
948
  if vision_available and query_images:
954
- logger.info(f"Using {conversation_config.chat_model} model to understand {len(query_images)} images.")
949
+ logger.info(f"Using {chat_model.name} model to understand {len(query_images)} images.")
955
950
 
956
951
  subscribed = await ais_user_subscribed(user)
957
- chat_model = conversation_config.chat_model
952
+ chat_model_name = chat_model.name
958
953
  max_tokens = (
959
- conversation_config.subscribed_max_prompt_size
960
- if subscribed and conversation_config.subscribed_max_prompt_size
961
- else conversation_config.max_prompt_size
954
+ chat_model.subscribed_max_prompt_size
955
+ if subscribed and chat_model.subscribed_max_prompt_size
956
+ else chat_model.max_prompt_size
962
957
  )
963
- tokenizer = conversation_config.tokenizer
964
- model_type = conversation_config.model_type
965
- vision_available = conversation_config.vision_enabled
958
+ tokenizer = chat_model.tokenizer
959
+ model_type = chat_model.model_type
960
+ vision_available = chat_model.vision_enabled
966
961
 
967
- if model_type == ChatModelOptions.ModelType.OFFLINE:
962
+ if model_type == ChatModel.ModelType.OFFLINE:
968
963
  if state.offline_chat_processor_config is None or state.offline_chat_processor_config.loaded_model is None:
969
- state.offline_chat_processor_config = OfflineChatProcessorModel(chat_model, max_tokens)
964
+ state.offline_chat_processor_config = OfflineChatProcessorModel(chat_model_name, max_tokens)
970
965
 
971
966
  loaded_model = state.offline_chat_processor_config.loaded_model
972
967
  truncated_messages = generate_chatml_messages_with_context(
973
968
  user_message=query,
974
969
  context_message=context,
975
970
  system_message=system_message,
976
- model_name=chat_model,
971
+ model_name=chat_model_name,
977
972
  loaded_model=loaded_model,
978
973
  tokenizer_name=tokenizer,
979
974
  max_prompt_size=max_tokens,
980
975
  vision_enabled=vision_available,
981
- model_type=conversation_config.model_type,
976
+ model_type=chat_model.model_type,
982
977
  query_files=query_files,
983
978
  )
984
979
 
985
980
  return send_message_to_model_offline(
986
981
  messages=truncated_messages,
987
982
  loaded_model=loaded_model,
988
- model=chat_model,
983
+ model_name=chat_model_name,
989
984
  max_prompt_size=max_tokens,
990
985
  streaming=False,
991
986
  response_type=response_type,
992
987
  tracer=tracer,
993
988
  )
994
989
 
995
- elif model_type == ChatModelOptions.ModelType.OPENAI:
996
- openai_chat_config = conversation_config.ai_model_api
990
+ elif model_type == ChatModel.ModelType.OPENAI:
991
+ openai_chat_config = chat_model.ai_model_api
997
992
  api_key = openai_chat_config.api_key
998
993
  api_base_url = openai_chat_config.api_base_url
999
994
  truncated_messages = generate_chatml_messages_with_context(
1000
995
  user_message=query,
1001
996
  context_message=context,
1002
997
  system_message=system_message,
1003
- model_name=chat_model,
998
+ model_name=chat_model_name,
1004
999
  max_prompt_size=max_tokens,
1005
1000
  tokenizer_name=tokenizer,
1006
1001
  vision_enabled=vision_available,
1007
1002
  query_images=query_images,
1008
- model_type=conversation_config.model_type,
1003
+ model_type=chat_model.model_type,
1009
1004
  query_files=query_files,
1010
1005
  )
1011
1006
 
1012
1007
  return send_message_to_model(
1013
1008
  messages=truncated_messages,
1014
1009
  api_key=api_key,
1015
- model=chat_model,
1010
+ model=chat_model_name,
1016
1011
  response_type=response_type,
1017
1012
  api_base_url=api_base_url,
1018
1013
  tracer=tracer,
1019
1014
  )
1020
- elif model_type == ChatModelOptions.ModelType.ANTHROPIC:
1021
- api_key = conversation_config.ai_model_api.api_key
1015
+ elif model_type == ChatModel.ModelType.ANTHROPIC:
1016
+ api_key = chat_model.ai_model_api.api_key
1022
1017
  truncated_messages = generate_chatml_messages_with_context(
1023
1018
  user_message=query,
1024
1019
  context_message=context,
1025
1020
  system_message=system_message,
1026
- model_name=chat_model,
1021
+ model_name=chat_model_name,
1027
1022
  max_prompt_size=max_tokens,
1028
1023
  tokenizer_name=tokenizer,
1029
1024
  vision_enabled=vision_available,
1030
1025
  query_images=query_images,
1031
- model_type=conversation_config.model_type,
1026
+ model_type=chat_model.model_type,
1032
1027
  query_files=query_files,
1033
1028
  )
1034
1029
 
1035
1030
  return anthropic_send_message_to_model(
1036
1031
  messages=truncated_messages,
1037
1032
  api_key=api_key,
1038
- model=chat_model,
1033
+ model=chat_model_name,
1039
1034
  response_type=response_type,
1040
1035
  tracer=tracer,
1041
1036
  )
1042
- elif model_type == ChatModelOptions.ModelType.GOOGLE:
1043
- api_key = conversation_config.ai_model_api.api_key
1037
+ elif model_type == ChatModel.ModelType.GOOGLE:
1038
+ api_key = chat_model.ai_model_api.api_key
1044
1039
  truncated_messages = generate_chatml_messages_with_context(
1045
1040
  user_message=query,
1046
1041
  context_message=context,
1047
1042
  system_message=system_message,
1048
- model_name=chat_model,
1043
+ model_name=chat_model_name,
1049
1044
  max_prompt_size=max_tokens,
1050
1045
  tokenizer_name=tokenizer,
1051
1046
  vision_enabled=vision_available,
1052
1047
  query_images=query_images,
1053
- model_type=conversation_config.model_type,
1048
+ model_type=chat_model.model_type,
1054
1049
  query_files=query_files,
1055
1050
  )
1056
1051
 
1057
1052
  return gemini_send_message_to_model(
1058
- messages=truncated_messages, api_key=api_key, model=chat_model, response_type=response_type, tracer=tracer
1053
+ messages=truncated_messages,
1054
+ api_key=api_key,
1055
+ model=chat_model_name,
1056
+ response_type=response_type,
1057
+ tracer=tracer,
1059
1058
  )
1060
1059
  else:
1061
1060
  raise HTTPException(status_code=500, detail="Invalid conversation config")
@@ -1069,99 +1068,99 @@ def send_message_to_model_wrapper_sync(
1069
1068
  query_files: str = "",
1070
1069
  tracer: dict = {},
1071
1070
  ):
1072
- conversation_config: ChatModelOptions = ConversationAdapters.get_default_conversation_config(user)
1071
+ chat_model: ChatModel = ConversationAdapters.get_default_chat_model(user)
1073
1072
 
1074
- if conversation_config is None:
1073
+ if chat_model is None:
1075
1074
  raise HTTPException(status_code=500, detail="Contact the server administrator to set a default chat model.")
1076
1075
 
1077
- chat_model = conversation_config.chat_model
1078
- max_tokens = conversation_config.max_prompt_size
1079
- vision_available = conversation_config.vision_enabled
1076
+ chat_model_name = chat_model.name
1077
+ max_tokens = chat_model.max_prompt_size
1078
+ vision_available = chat_model.vision_enabled
1080
1079
 
1081
- if conversation_config.model_type == ChatModelOptions.ModelType.OFFLINE:
1080
+ if chat_model.model_type == ChatModel.ModelType.OFFLINE:
1082
1081
  if state.offline_chat_processor_config is None or state.offline_chat_processor_config.loaded_model is None:
1083
- state.offline_chat_processor_config = OfflineChatProcessorModel(chat_model, max_tokens)
1082
+ state.offline_chat_processor_config = OfflineChatProcessorModel(chat_model_name, max_tokens)
1084
1083
 
1085
1084
  loaded_model = state.offline_chat_processor_config.loaded_model
1086
1085
  truncated_messages = generate_chatml_messages_with_context(
1087
1086
  user_message=message,
1088
1087
  system_message=system_message,
1089
- model_name=chat_model,
1088
+ model_name=chat_model_name,
1090
1089
  loaded_model=loaded_model,
1091
1090
  max_prompt_size=max_tokens,
1092
1091
  vision_enabled=vision_available,
1093
- model_type=conversation_config.model_type,
1092
+ model_type=chat_model.model_type,
1094
1093
  query_files=query_files,
1095
1094
  )
1096
1095
 
1097
1096
  return send_message_to_model_offline(
1098
1097
  messages=truncated_messages,
1099
1098
  loaded_model=loaded_model,
1100
- model=chat_model,
1099
+ model_name=chat_model_name,
1101
1100
  max_prompt_size=max_tokens,
1102
1101
  streaming=False,
1103
1102
  response_type=response_type,
1104
1103
  tracer=tracer,
1105
1104
  )
1106
1105
 
1107
- elif conversation_config.model_type == ChatModelOptions.ModelType.OPENAI:
1108
- api_key = conversation_config.ai_model_api.api_key
1106
+ elif chat_model.model_type == ChatModel.ModelType.OPENAI:
1107
+ api_key = chat_model.ai_model_api.api_key
1109
1108
  truncated_messages = generate_chatml_messages_with_context(
1110
1109
  user_message=message,
1111
1110
  system_message=system_message,
1112
- model_name=chat_model,
1111
+ model_name=chat_model_name,
1113
1112
  max_prompt_size=max_tokens,
1114
1113
  vision_enabled=vision_available,
1115
- model_type=conversation_config.model_type,
1114
+ model_type=chat_model.model_type,
1116
1115
  query_files=query_files,
1117
1116
  )
1118
1117
 
1119
1118
  openai_response = send_message_to_model(
1120
1119
  messages=truncated_messages,
1121
1120
  api_key=api_key,
1122
- model=chat_model,
1121
+ model=chat_model_name,
1123
1122
  response_type=response_type,
1124
1123
  tracer=tracer,
1125
1124
  )
1126
1125
 
1127
1126
  return openai_response
1128
1127
 
1129
- elif conversation_config.model_type == ChatModelOptions.ModelType.ANTHROPIC:
1130
- api_key = conversation_config.ai_model_api.api_key
1128
+ elif chat_model.model_type == ChatModel.ModelType.ANTHROPIC:
1129
+ api_key = chat_model.ai_model_api.api_key
1131
1130
  truncated_messages = generate_chatml_messages_with_context(
1132
1131
  user_message=message,
1133
1132
  system_message=system_message,
1134
- model_name=chat_model,
1133
+ model_name=chat_model_name,
1135
1134
  max_prompt_size=max_tokens,
1136
1135
  vision_enabled=vision_available,
1137
- model_type=conversation_config.model_type,
1136
+ model_type=chat_model.model_type,
1138
1137
  query_files=query_files,
1139
1138
  )
1140
1139
 
1141
1140
  return anthropic_send_message_to_model(
1142
1141
  messages=truncated_messages,
1143
1142
  api_key=api_key,
1144
- model=chat_model,
1143
+ model=chat_model_name,
1145
1144
  response_type=response_type,
1146
1145
  tracer=tracer,
1147
1146
  )
1148
1147
 
1149
- elif conversation_config.model_type == ChatModelOptions.ModelType.GOOGLE:
1150
- api_key = conversation_config.ai_model_api.api_key
1148
+ elif chat_model.model_type == ChatModel.ModelType.GOOGLE:
1149
+ api_key = chat_model.ai_model_api.api_key
1151
1150
  truncated_messages = generate_chatml_messages_with_context(
1152
1151
  user_message=message,
1153
1152
  system_message=system_message,
1154
- model_name=chat_model,
1153
+ model_name=chat_model_name,
1155
1154
  max_prompt_size=max_tokens,
1156
1155
  vision_enabled=vision_available,
1157
- model_type=conversation_config.model_type,
1156
+ model_type=chat_model.model_type,
1158
1157
  query_files=query_files,
1159
1158
  )
1160
1159
 
1161
1160
  return gemini_send_message_to_model(
1162
1161
  messages=truncated_messages,
1163
1162
  api_key=api_key,
1164
- model=chat_model,
1163
+ model=chat_model_name,
1165
1164
  response_type=response_type,
1166
1165
  tracer=tracer,
1167
1166
  )
@@ -1229,15 +1228,15 @@ def generate_chat_response(
1229
1228
  online_results = {}
1230
1229
  code_results = {}
1231
1230
 
1232
- conversation_config = ConversationAdapters.get_valid_conversation_config(user, conversation)
1233
- vision_available = conversation_config.vision_enabled
1231
+ chat_model = ConversationAdapters.get_valid_chat_model(user, conversation)
1232
+ vision_available = chat_model.vision_enabled
1234
1233
  if not vision_available and query_images:
1235
1234
  vision_enabled_config = ConversationAdapters.get_vision_enabled_config()
1236
1235
  if vision_enabled_config:
1237
- conversation_config = vision_enabled_config
1236
+ chat_model = vision_enabled_config
1238
1237
  vision_available = True
1239
1238
 
1240
- if conversation_config.model_type == "offline":
1239
+ if chat_model.model_type == "offline":
1241
1240
  loaded_model = state.offline_chat_processor_config.loaded_model
1242
1241
  chat_response = converse_offline(
1243
1242
  user_query=query_to_run,
@@ -1247,9 +1246,9 @@ def generate_chat_response(
1247
1246
  conversation_log=meta_log,
1248
1247
  completion_func=partial_completion,
1249
1248
  conversation_commands=conversation_commands,
1250
- model=conversation_config.chat_model,
1251
- max_prompt_size=conversation_config.max_prompt_size,
1252
- tokenizer_name=conversation_config.tokenizer,
1249
+ model_name=chat_model.name,
1250
+ max_prompt_size=chat_model.max_prompt_size,
1251
+ tokenizer_name=chat_model.tokenizer,
1253
1252
  location_data=location_data,
1254
1253
  user_name=user_name,
1255
1254
  agent=agent,
@@ -1259,10 +1258,10 @@ def generate_chat_response(
1259
1258
  tracer=tracer,
1260
1259
  )
1261
1260
 
1262
- elif conversation_config.model_type == ChatModelOptions.ModelType.OPENAI:
1263
- openai_chat_config = conversation_config.ai_model_api
1261
+ elif chat_model.model_type == ChatModel.ModelType.OPENAI:
1262
+ openai_chat_config = chat_model.ai_model_api
1264
1263
  api_key = openai_chat_config.api_key
1265
- chat_model = conversation_config.chat_model
1264
+ chat_model_name = chat_model.name
1266
1265
  chat_response = converse_openai(
1267
1266
  compiled_references,
1268
1267
  query_to_run,
@@ -1270,13 +1269,13 @@ def generate_chat_response(
1270
1269
  online_results=online_results,
1271
1270
  code_results=code_results,
1272
1271
  conversation_log=meta_log,
1273
- model=chat_model,
1272
+ model=chat_model_name,
1274
1273
  api_key=api_key,
1275
1274
  api_base_url=openai_chat_config.api_base_url,
1276
1275
  completion_func=partial_completion,
1277
1276
  conversation_commands=conversation_commands,
1278
- max_prompt_size=conversation_config.max_prompt_size,
1279
- tokenizer_name=conversation_config.tokenizer,
1277
+ max_prompt_size=chat_model.max_prompt_size,
1278
+ tokenizer_name=chat_model.tokenizer,
1280
1279
  location_data=location_data,
1281
1280
  user_name=user_name,
1282
1281
  agent=agent,
@@ -1288,8 +1287,8 @@ def generate_chat_response(
1288
1287
  tracer=tracer,
1289
1288
  )
1290
1289
 
1291
- elif conversation_config.model_type == ChatModelOptions.ModelType.ANTHROPIC:
1292
- api_key = conversation_config.ai_model_api.api_key
1290
+ elif chat_model.model_type == ChatModel.ModelType.ANTHROPIC:
1291
+ api_key = chat_model.ai_model_api.api_key
1293
1292
  chat_response = converse_anthropic(
1294
1293
  compiled_references,
1295
1294
  query_to_run,
@@ -1297,12 +1296,12 @@ def generate_chat_response(
1297
1296
  online_results=online_results,
1298
1297
  code_results=code_results,
1299
1298
  conversation_log=meta_log,
1300
- model=conversation_config.chat_model,
1299
+ model=chat_model.name,
1301
1300
  api_key=api_key,
1302
1301
  completion_func=partial_completion,
1303
1302
  conversation_commands=conversation_commands,
1304
- max_prompt_size=conversation_config.max_prompt_size,
1305
- tokenizer_name=conversation_config.tokenizer,
1303
+ max_prompt_size=chat_model.max_prompt_size,
1304
+ tokenizer_name=chat_model.tokenizer,
1306
1305
  location_data=location_data,
1307
1306
  user_name=user_name,
1308
1307
  agent=agent,
@@ -1313,20 +1312,20 @@ def generate_chat_response(
1313
1312
  program_execution_context=program_execution_context,
1314
1313
  tracer=tracer,
1315
1314
  )
1316
- elif conversation_config.model_type == ChatModelOptions.ModelType.GOOGLE:
1317
- api_key = conversation_config.ai_model_api.api_key
1315
+ elif chat_model.model_type == ChatModel.ModelType.GOOGLE:
1316
+ api_key = chat_model.ai_model_api.api_key
1318
1317
  chat_response = converse_gemini(
1319
1318
  compiled_references,
1320
1319
  query_to_run,
1321
1320
  online_results,
1322
1321
  code_results,
1323
1322
  meta_log,
1324
- model=conversation_config.chat_model,
1323
+ model=chat_model.name,
1325
1324
  api_key=api_key,
1326
1325
  completion_func=partial_completion,
1327
1326
  conversation_commands=conversation_commands,
1328
- max_prompt_size=conversation_config.max_prompt_size,
1329
- tokenizer_name=conversation_config.tokenizer,
1327
+ max_prompt_size=chat_model.max_prompt_size,
1328
+ tokenizer_name=chat_model.tokenizer,
1330
1329
  location_data=location_data,
1331
1330
  user_name=user_name,
1332
1331
  agent=agent,
@@ -1339,7 +1338,7 @@ def generate_chat_response(
1339
1338
  tracer=tracer,
1340
1339
  )
1341
1340
 
1342
- metadata.update({"chat_model": conversation_config.chat_model})
1341
+ metadata.update({"chat_model": chat_model.name})
1343
1342
 
1344
1343
  except Exception as e:
1345
1344
  logger.error(e, exc_info=True)
@@ -1359,6 +1358,49 @@ class FeedbackData(BaseModel):
1359
1358
  sentiment: str
1360
1359
 
1361
1360
 
1361
+ class EmailVerificationApiRateLimiter:
1362
+ def __init__(self, requests: int, window: int, slug: str):
1363
+ self.requests = requests
1364
+ self.window = window
1365
+ self.slug = slug
1366
+
1367
+ def __call__(self, request: Request):
1368
+ # Rate limiting disabled if billing is disabled
1369
+ if state.billing_enabled is False:
1370
+ return
1371
+
1372
+ # Extract the email query parameter
1373
+ email = request.query_params.get("email")
1374
+
1375
+ if email:
1376
+ logger.info(f"Email query parameter: {email}")
1377
+
1378
+ user: KhojUser = get_user_by_email(email)
1379
+
1380
+ if not user:
1381
+ raise HTTPException(
1382
+ status_code=404,
1383
+ detail="User not found.",
1384
+ )
1385
+
1386
+ # Remove requests outside of the time window
1387
+ cutoff = datetime.now(tz=timezone.utc) - timedelta(seconds=self.window)
1388
+ count_requests = UserRequests.objects.filter(user=user, created_at__gte=cutoff, slug=self.slug).count()
1389
+
1390
+ # Check if the user has exceeded the rate limit
1391
+ if count_requests >= self.requests:
1392
+ logger.info(
1393
+ f"Rate limit: {count_requests}/{self.requests} requests not allowed in {self.window} seconds for email: {email}."
1394
+ )
1395
+ raise HTTPException(
1396
+ status_code=429,
1397
+ detail="Ran out of login attempts",
1398
+ )
1399
+
1400
+ # Add the current request to the db
1401
+ UserRequests.objects.create(user=user, slug=self.slug)
1402
+
1403
+
1362
1404
  class ApiUserRateLimiter:
1363
1405
  def __init__(self, requests: int, subscribed_requests: int, window: int, slug: str):
1364
1406
  self.requests = requests
@@ -1638,7 +1680,7 @@ def scheduled_chat(
1638
1680
  last_run_time = datetime.strptime(last_run_time, "%Y-%m-%d %I:%M %p %Z").replace(tzinfo=timezone.utc)
1639
1681
 
1640
1682
  # If the last run time was within the last 6 hours, don't run it again. This helps avoid multithreading issues and rate limits.
1641
- if (datetime.now(timezone.utc) - last_run_time).total_seconds() < 21600:
1683
+ if (datetime.now(timezone.utc) - last_run_time).total_seconds() < 6 * 60 * 60:
1642
1684
  logger.info(f"Skipping scheduled chat {job_id} as the next run time is in the future.")
1643
1685
  return
1644
1686
 
@@ -1939,13 +1981,13 @@ def get_user_config(user: KhojUser, request: Request, is_detailed: bool = False)
1939
1981
  current_notion_config = get_user_notion_config(user)
1940
1982
  notion_token = current_notion_config.token if current_notion_config else ""
1941
1983
 
1942
- selected_chat_model_config = ConversationAdapters.get_conversation_config(
1984
+ selected_chat_model_config = ConversationAdapters.get_chat_model(
1943
1985
  user
1944
- ) or ConversationAdapters.get_default_conversation_config(user)
1986
+ ) or ConversationAdapters.get_default_chat_model(user)
1945
1987
  chat_models = ConversationAdapters.get_conversation_processor_options().all()
1946
1988
  chat_model_options = list()
1947
1989
  for chat_model in chat_models:
1948
- chat_model_options.append({"name": chat_model.chat_model, "id": chat_model.id})
1990
+ chat_model_options.append({"name": chat_model.name, "id": chat_model.id})
1949
1991
 
1950
1992
  selected_paint_model_config = ConversationAdapters.get_user_text_to_image_model_config(user)
1951
1993
  paint_model_options = ConversationAdapters.get_text_to_image_model_options().all()
@@ -57,7 +57,7 @@ def login_page(request: Request):
57
57
  if request.user.is_authenticated:
58
58
  return RedirectResponse(url=next_url)
59
59
  google_client_id = os.environ.get("GOOGLE_CLIENT_ID")
60
- redirect_uri = str(request.app.url_path_for("auth"))
60
+ redirect_uri = str(request.app.url_path_for("auth_post"))
61
61
  return templates.TemplateResponse(
62
62
  "login.html",
63
63
  context={