khoj 1.29.2.dev5__py3-none-any.whl → 1.29.2.dev35__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 (51) hide show
  1. khoj/database/adapters/__init__.py +2 -0
  2. khoj/database/admin.py +32 -1
  3. khoj/interface/compiled/404/index.html +1 -1
  4. khoj/interface/compiled/_next/static/chunks/5538-b87b60ecc0c27ceb.js +1 -0
  5. khoj/interface/compiled/_next/static/chunks/796-68f9e87f9cdfda1d.js +3 -0
  6. khoj/interface/compiled/_next/static/chunks/8423-c0123d454681e03a.js +1 -0
  7. khoj/interface/compiled/_next/static/chunks/app/chat/{page-e95e87da53d725a7.js → page-e60a55d029b6216a.js} +1 -1
  8. khoj/interface/compiled/_next/static/chunks/app/page-fcf7411ff80b6bf5.js +1 -0
  9. khoj/interface/compiled/_next/static/chunks/app/share/chat/{page-3a752baa5fb62e20.js → page-4a4c0f199b89bd80.js} +1 -1
  10. khoj/interface/compiled/_next/static/chunks/{webpack-d37377886a1b4e56.js → webpack-323bbe2678102a2f.js} +1 -1
  11. khoj/interface/compiled/agents/index.html +1 -1
  12. khoj/interface/compiled/agents/index.txt +1 -1
  13. khoj/interface/compiled/automations/index.html +1 -1
  14. khoj/interface/compiled/automations/index.txt +1 -1
  15. khoj/interface/compiled/chat/index.html +1 -1
  16. khoj/interface/compiled/chat/index.txt +2 -2
  17. khoj/interface/compiled/index.html +1 -1
  18. khoj/interface/compiled/index.txt +2 -2
  19. khoj/interface/compiled/search/index.html +1 -1
  20. khoj/interface/compiled/search/index.txt +1 -1
  21. khoj/interface/compiled/settings/index.html +1 -1
  22. khoj/interface/compiled/settings/index.txt +1 -1
  23. khoj/interface/compiled/share/chat/index.html +1 -1
  24. khoj/interface/compiled/share/chat/index.txt +2 -2
  25. khoj/main.py +7 -3
  26. khoj/processor/content/pdf/pdf_to_entries.py +1 -1
  27. khoj/processor/conversation/anthropic/anthropic_chat.py +2 -3
  28. khoj/processor/conversation/google/gemini_chat.py +2 -3
  29. khoj/processor/conversation/offline/chat_model.py +2 -2
  30. khoj/processor/conversation/openai/gpt.py +2 -2
  31. khoj/processor/conversation/prompts.py +14 -69
  32. khoj/processor/conversation/utils.py +7 -0
  33. khoj/processor/tools/online_search.py +22 -4
  34. khoj/routers/api_chat.py +23 -15
  35. khoj/routers/helpers.py +36 -77
  36. khoj/routers/research.py +33 -30
  37. khoj/utils/constants.py +1 -1
  38. khoj/utils/helpers.py +5 -6
  39. khoj/utils/initialization.py +77 -10
  40. {khoj-1.29.2.dev5.dist-info → khoj-1.29.2.dev35.dist-info}/METADATA +1 -1
  41. {khoj-1.29.2.dev5.dist-info → khoj-1.29.2.dev35.dist-info}/RECORD +46 -47
  42. khoj/interface/compiled/_next/static/chunks/5538-32bd787d106700dc.js +0 -1
  43. khoj/interface/compiled/_next/static/chunks/5961-3c104d9736b7902b.js +0 -3
  44. khoj/interface/compiled/_next/static/chunks/8423-ffdc2b835629c7f8.js +0 -1
  45. khoj/interface/compiled/_next/static/chunks/app/page-774dcd8ca4459c7e.js +0 -1
  46. khoj/interface/web/assets/icons/favicon-128x128.ico +0 -0
  47. /khoj/interface/compiled/_next/static/{bIVLxe5g7EDG455p-cfe7 → bkshWraYdEa_w254xnxBc}/_buildManifest.js +0 -0
  48. /khoj/interface/compiled/_next/static/{bIVLxe5g7EDG455p-cfe7 → bkshWraYdEa_w254xnxBc}/_ssgManifest.js +0 -0
  49. {khoj-1.29.2.dev5.dist-info → khoj-1.29.2.dev35.dist-info}/WHEEL +0 -0
  50. {khoj-1.29.2.dev5.dist-info → khoj-1.29.2.dev35.dist-info}/entry_points.txt +0 -0
  51. {khoj-1.29.2.dev5.dist-info → khoj-1.29.2.dev35.dist-info}/licenses/LICENSE +0 -0
khoj/routers/api_chat.py CHANGED
@@ -46,8 +46,7 @@ from khoj.routers.helpers import (
46
46
  FeedbackData,
47
47
  acreate_title_from_history,
48
48
  agenerate_chat_response,
49
- aget_relevant_information_sources,
50
- aget_relevant_output_modes,
49
+ aget_relevant_tools_to_execute,
51
50
  construct_automation_created_message,
52
51
  create_automation,
53
52
  gather_raw_query_files,
@@ -753,7 +752,7 @@ async def chat(
753
752
  attached_file_context = gather_raw_query_files(query_files)
754
753
 
755
754
  if conversation_commands == [ConversationCommand.Default] or is_automated_task:
756
- conversation_commands = await aget_relevant_information_sources(
755
+ conversation_commands = await aget_relevant_tools_to_execute(
757
756
  q,
758
757
  meta_log,
759
758
  is_automated_task,
@@ -769,19 +768,9 @@ async def chat(
769
768
  conversation_commands = [ConversationCommand.Research]
770
769
 
771
770
  conversation_commands_str = ", ".join([cmd.value for cmd in conversation_commands])
772
- async for result in send_event(
773
- ChatEvent.STATUS, f"**Chose Data Sources to Search:** {conversation_commands_str}"
774
- ):
771
+ async for result in send_event(ChatEvent.STATUS, f"**Selected Tools:** {conversation_commands_str}"):
775
772
  yield result
776
773
 
777
- mode = await aget_relevant_output_modes(
778
- q, meta_log, is_automated_task, user, uploaded_images, agent, tracer=tracer
779
- )
780
- async for result in send_event(ChatEvent.STATUS, f"**Decided Response Mode:** {mode.value}"):
781
- yield result
782
- if mode not in conversation_commands:
783
- conversation_commands.append(mode)
784
-
785
774
  for cmd in conversation_commands:
786
775
  try:
787
776
  await conversation_command_rate_limiter.update_and_check_if_valid(request, cmd)
@@ -1175,8 +1164,27 @@ async def chat(
1175
1164
  inferred_queries.append(better_diagram_description_prompt)
1176
1165
  diagram_description = excalidraw_diagram_description
1177
1166
  else:
1178
- async for result in send_llm_response(f"Failed to generate diagram. Please try again later."):
1167
+ error_message = "Failed to generate diagram. Please try again later."
1168
+ async for result in send_llm_response(error_message):
1179
1169
  yield result
1170
+
1171
+ await sync_to_async(save_to_conversation_log)(
1172
+ q,
1173
+ error_message,
1174
+ user,
1175
+ meta_log,
1176
+ user_message_time,
1177
+ inferred_queries=[better_diagram_description_prompt],
1178
+ client_application=request.user.client_app,
1179
+ conversation_id=conversation_id,
1180
+ compiled_references=compiled_references,
1181
+ online_results=online_results,
1182
+ code_results=code_results,
1183
+ query_images=uploaded_images,
1184
+ train_of_thought=train_of_thought,
1185
+ raw_query_files=raw_query_files,
1186
+ tracer=tracer,
1187
+ )
1180
1188
  return
1181
1189
 
1182
1190
  content_obj = {
khoj/routers/helpers.py CHANGED
@@ -336,7 +336,7 @@ async def acheck_if_safe_prompt(system_prompt: str, user: KhojUser = None, lax:
336
336
  return is_safe, reason
337
337
 
338
338
 
339
- async def aget_relevant_information_sources(
339
+ async def aget_relevant_tools_to_execute(
340
340
  query: str,
341
341
  conversation_history: dict,
342
342
  is_task: bool,
@@ -360,6 +360,19 @@ async def aget_relevant_information_sources(
360
360
  if len(agent_tools) == 0 or tool.value in agent_tools:
361
361
  tool_options_str += f'- "{tool.value}": "{description}"\n'
362
362
 
363
+ mode_options = dict()
364
+ mode_options_str = ""
365
+
366
+ output_modes = agent.output_modes if agent else []
367
+
368
+ for mode, description in mode_descriptions_for_llm.items():
369
+ # Do not allow tasks to schedule another task
370
+ if is_task and mode == ConversationCommand.Automation:
371
+ continue
372
+ mode_options[mode.value] = description
373
+ if len(output_modes) == 0 or mode.value in output_modes:
374
+ mode_options_str += f'- "{mode.value}": "{description}"\n'
375
+
363
376
  chat_history = construct_chat_history(conversation_history)
364
377
 
365
378
  if query_images:
@@ -369,9 +382,10 @@ async def aget_relevant_information_sources(
369
382
  prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
370
383
  )
371
384
 
372
- relevant_tools_prompt = prompts.pick_relevant_information_collection_tools.format(
385
+ relevant_tools_prompt = prompts.pick_relevant_tools.format(
373
386
  query=query,
374
387
  tools=tool_options_str,
388
+ outputs=mode_options_str,
375
389
  chat_history=chat_history,
376
390
  personality_context=personality_context,
377
391
  )
@@ -388,13 +402,18 @@ async def aget_relevant_information_sources(
388
402
  try:
389
403
  response = clean_json(response)
390
404
  response = json.loads(response)
391
- response = [q.strip() for q in response["source"] if q.strip()]
392
- if not isinstance(response, list) or not response or len(response) == 0:
393
- logger.error(f"Invalid response for determining relevant tools: {response}")
405
+ input_tools = [q.strip() for q in response["source"] if q.strip()]
406
+ if not isinstance(input_tools, list) or not input_tools or len(input_tools) == 0:
407
+ logger.error(f"Invalid response for determining relevant tools: {input_tools}")
394
408
  return tool_options
395
409
 
410
+ output_modes = [q.strip() for q in response["output"] if q.strip()]
411
+ if not isinstance(output_modes, list) or not output_modes or len(output_modes) == 0:
412
+ logger.error(f"Invalid response for determining relevant output modes: {output_modes}")
413
+ return mode_options
414
+
396
415
  final_response = [] if not is_task else [ConversationCommand.AutomatedTask]
397
- for llm_suggested_tool in response:
416
+ for llm_suggested_tool in input_tools:
398
417
  # Add a double check to verify it's in the agent list, because the LLM sometimes gets confused by the tool options.
399
418
  if llm_suggested_tool in tool_options.keys() and (
400
419
  len(agent_tools) == 0 or llm_suggested_tool in agent_tools
@@ -402,88 +421,28 @@ async def aget_relevant_information_sources(
402
421
  # Check whether the tool exists as a valid ConversationCommand
403
422
  final_response.append(ConversationCommand(llm_suggested_tool))
404
423
 
424
+ for llm_suggested_output in output_modes:
425
+ # Add a double check to verify it's in the agent list, because the LLM sometimes gets confused by the tool options.
426
+ if llm_suggested_output in mode_options.keys() and (
427
+ len(output_modes) == 0 or llm_suggested_output in output_modes
428
+ ):
429
+ # Check whether the tool exists as a valid ConversationCommand
430
+ final_response.append(ConversationCommand(llm_suggested_output))
431
+
405
432
  if is_none_or_empty(final_response):
406
433
  if len(agent_tools) == 0:
407
- final_response = [ConversationCommand.Default]
434
+ final_response = [ConversationCommand.Default, ConversationCommand.Text]
408
435
  else:
409
- final_response = [ConversationCommand.General]
436
+ final_response = [ConversationCommand.General, ConversationCommand.Text]
410
437
  except Exception:
411
438
  logger.error(f"Invalid response for determining relevant tools: {response}")
412
439
  if len(agent_tools) == 0:
413
- final_response = [ConversationCommand.Default]
440
+ final_response = [ConversationCommand.Default, ConversationCommand.Text]
414
441
  else:
415
442
  final_response = agent_tools
416
443
  return final_response
417
444
 
418
445
 
419
- async def aget_relevant_output_modes(
420
- query: str,
421
- conversation_history: dict,
422
- is_task: bool = False,
423
- user: KhojUser = None,
424
- query_images: List[str] = None,
425
- agent: Agent = None,
426
- tracer: dict = {},
427
- ):
428
- """
429
- Given a query, determine which of the available tools the agent should use in order to answer appropriately.
430
- """
431
-
432
- mode_options = dict()
433
- mode_options_str = ""
434
-
435
- output_modes = agent.output_modes if agent else []
436
-
437
- for mode, description in mode_descriptions_for_llm.items():
438
- # Do not allow tasks to schedule another task
439
- if is_task and mode == ConversationCommand.Automation:
440
- continue
441
- mode_options[mode.value] = description
442
- if len(output_modes) == 0 or mode.value in output_modes:
443
- mode_options_str += f'- "{mode.value}": "{description}"\n'
444
-
445
- chat_history = construct_chat_history(conversation_history)
446
-
447
- if query_images:
448
- query = f"[placeholder for {len(query_images)} user attached images]\n{query}"
449
-
450
- personality_context = (
451
- prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
452
- )
453
-
454
- relevant_mode_prompt = prompts.pick_relevant_output_mode.format(
455
- query=query,
456
- modes=mode_options_str,
457
- chat_history=chat_history,
458
- personality_context=personality_context,
459
- )
460
-
461
- with timer("Chat actor: Infer output mode for chat response", logger):
462
- response = await send_message_to_model_wrapper(
463
- relevant_mode_prompt, response_type="json_object", user=user, tracer=tracer
464
- )
465
-
466
- try:
467
- response = clean_json(response)
468
- response = json.loads(response)
469
-
470
- if is_none_or_empty(response):
471
- return ConversationCommand.Text
472
-
473
- output_mode = response["output"]
474
-
475
- # Add a double check to verify it's in the agent list, because the LLM sometimes gets confused by the tool options.
476
- if output_mode in mode_options.keys() and (len(output_modes) == 0 or output_mode in output_modes):
477
- # Check whether the tool exists as a valid ConversationCommand
478
- return ConversationCommand(output_mode)
479
-
480
- logger.error(f"Invalid output mode selected: {output_mode}. Defaulting to text.")
481
- return ConversationCommand.Text
482
- except Exception:
483
- logger.error(f"Invalid response for determining output mode: {response}")
484
- return ConversationCommand.Text
485
-
486
-
487
446
  async def infer_webpage_urls(
488
447
  q: str,
489
448
  conversation_history: dict,
khoj/routers/research.py CHANGED
@@ -114,7 +114,7 @@ async def apick_next_tool(
114
114
  logger.info(f"Response for determining relevant tools: {response}")
115
115
 
116
116
  # Detect selection of previously used query, tool combination.
117
- previous_tool_query_combinations = {(i.tool, i.query) for i in previous_iterations}
117
+ previous_tool_query_combinations = {(i.tool, i.query) for i in previous_iterations if i.warning is None}
118
118
  if (selected_tool, generated_query) in previous_tool_query_combinations:
119
119
  warning = f"Repeated tool, query combination detected. Skipping iteration. Try something different."
120
120
  # Only send client status updates if we'll execute this iteration
@@ -226,7 +226,8 @@ async def execute_information_collection(
226
226
  ):
227
227
  yield result
228
228
  except Exception as e:
229
- logger.error(f"Error extracting document references: {e}", exc_info=True)
229
+ this_iteration.warning = f"Error extracting document references: {e}"
230
+ logger.error(this_iteration.warning, exc_info=True)
230
231
 
231
232
  elif this_iteration.tool == ConversationCommand.Online:
232
233
  previous_subqueries = {
@@ -235,28 +236,30 @@ async def execute_information_collection(
235
236
  if iteration.onlineContext
236
237
  for subquery in iteration.onlineContext.keys()
237
238
  }
238
- async for result in search_online(
239
- this_iteration.query,
240
- construct_tool_chat_history(previous_iterations, ConversationCommand.Online),
241
- location,
242
- user,
243
- send_status_func,
244
- [],
245
- max_webpages_to_read=0,
246
- query_images=query_images,
247
- previous_subqueries=previous_subqueries,
248
- agent=agent,
249
- tracer=tracer,
250
- ):
251
- if isinstance(result, dict) and ChatEvent.STATUS in result:
252
- yield result[ChatEvent.STATUS]
253
- elif is_none_or_empty(result):
254
- this_iteration.warning = (
255
- "Detected previously run online search queries. Skipping iteration. Try something different."
256
- )
257
- else:
258
- online_results: Dict[str, Dict] = result # type: ignore
259
- this_iteration.onlineContext = online_results
239
+ try:
240
+ async for result in search_online(
241
+ this_iteration.query,
242
+ construct_tool_chat_history(previous_iterations, ConversationCommand.Online),
243
+ location,
244
+ user,
245
+ send_status_func,
246
+ [],
247
+ max_webpages_to_read=0,
248
+ query_images=query_images,
249
+ previous_subqueries=previous_subqueries,
250
+ agent=agent,
251
+ tracer=tracer,
252
+ ):
253
+ if isinstance(result, dict) and ChatEvent.STATUS in result:
254
+ yield result[ChatEvent.STATUS]
255
+ elif is_none_or_empty(result):
256
+ this_iteration.warning = "Detected previously run online search queries. Skipping iteration. Try something different."
257
+ else:
258
+ online_results: Dict[str, Dict] = result # type: ignore
259
+ this_iteration.onlineContext = online_results
260
+ except Exception as e:
261
+ this_iteration.warning = f"Error searching online: {e}"
262
+ logger.error(this_iteration.warning, exc_info=True)
260
263
 
261
264
  elif this_iteration.tool == ConversationCommand.Webpage:
262
265
  try:
@@ -287,7 +290,8 @@ async def execute_information_collection(
287
290
  webpages.append(webpage["link"])
288
291
  this_iteration.onlineContext = online_results
289
292
  except Exception as e:
290
- logger.error(f"Error reading webpages: {e}", exc_info=True)
293
+ this_iteration.warning = f"Error reading webpages: {e}"
294
+ logger.error(this_iteration.warning, exc_info=True)
291
295
 
292
296
  elif this_iteration.tool == ConversationCommand.Code:
293
297
  try:
@@ -311,10 +315,8 @@ async def execute_information_collection(
311
315
  async for result in send_status_func(f"**Ran code snippets**: {len(this_iteration.codeContext)}"):
312
316
  yield result
313
317
  except ValueError as e:
314
- logger.warning(
315
- f"Failed to use code tool: {e}. Attempting to respond without code results",
316
- exc_info=True,
317
- )
318
+ this_iteration.warning = f"Error running code: {e}"
319
+ logger.warning(this_iteration.warning, exc_info=True)
318
320
 
319
321
  elif this_iteration.tool == ConversationCommand.Summarize:
320
322
  try:
@@ -333,7 +335,8 @@ async def execute_information_collection(
333
335
  else:
334
336
  summarize_files = result # type: ignore
335
337
  except Exception as e:
336
- logger.error(f"Error generating summary: {e}", exc_info=True)
338
+ this_iteration.warning = f"Error summarizing files: {e}"
339
+ logger.error(this_iteration.warning, exc_info=True)
337
340
 
338
341
  else:
339
342
  # No valid tools. This is our exit condition.
khoj/utils/constants.py CHANGED
@@ -16,7 +16,7 @@ default_offline_chat_models = [
16
16
  ]
17
17
  default_openai_chat_models = ["gpt-4o-mini", "gpt-4o"]
18
18
  default_gemini_chat_models = ["gemini-1.5-flash", "gemini-1.5-pro"]
19
- default_anthropic_chat_models = ["claude-3-5-sonnet-20240620", "claude-3-opus-20240229"]
19
+ default_anthropic_chat_models = ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022"]
20
20
 
21
21
  empty_config = {
22
22
  "search-type": {
khoj/utils/helpers.py CHANGED
@@ -365,7 +365,7 @@ tool_descriptions_for_llm = {
365
365
  ConversationCommand.Notes: "To search the user's personal knowledge base. Especially helpful if the question expects context from the user's notes or documents.",
366
366
  ConversationCommand.Online: "To search for the latest, up-to-date information from the internet. Note: **Questions about Khoj should always use this data source**",
367
367
  ConversationCommand.Webpage: "To use if the user has directly provided the webpage urls or you are certain of the webpage urls to read.",
368
- ConversationCommand.Code: "To run Python code in a Pyodide sandbox with no network access. Helpful when need to parse information, run complex calculations, create documents and charts for user. Matplotlib, bs4, pandas, numpy, etc. are available.",
368
+ ConversationCommand.Code: "To run Python code in a Pyodide sandbox with no network access. Helpful when need to parse information, run complex calculations, create plaintext documents, and create charts with quantitative data. Matplotlib, bs4, pandas, numpy, etc. are available.",
369
369
  ConversationCommand.Summarize: "To retrieve an answer that depends on the entire document or a large text.",
370
370
  }
371
371
 
@@ -373,14 +373,13 @@ function_calling_description_for_llm = {
373
373
  ConversationCommand.Notes: "To search the user's personal knowledge base. Especially helpful if the question expects context from the user's notes or documents.",
374
374
  ConversationCommand.Online: "To search the internet for information. Useful to get a quick, broad overview from the internet. Provide all relevant context to ensure new searches, not in previous iterations, are performed.",
375
375
  ConversationCommand.Webpage: "To extract information from webpages. Useful for more detailed research from the internet. Usually used when you know the webpage links to refer to. Share the webpage links and information to extract in your query.",
376
- ConversationCommand.Code: "To run Python code in a Pyodide sandbox with no network access. Helpful when need to parse information, run complex calculations, create charts for user. Matplotlib, bs4, pandas, numpy, etc. are available.",
376
+ ConversationCommand.Code: "To run Python code in a Pyodide sandbox with no network access. Helpful when need to parse information, run complex calculations, create plaintext documents, and create charts with quantitative data. Matplotlib, bs4, pandas, numpy, etc. are available.",
377
377
  }
378
378
 
379
379
  mode_descriptions_for_llm = {
380
- ConversationCommand.Image: "Use this if you are confident the user is requesting you to create a new picture based on their description. This does not support generating charts or graphs.",
381
- ConversationCommand.Automation: "Use this if you are confident the user is requesting a response at a scheduled date, time and frequency",
382
- ConversationCommand.Text: "Use this if a normal text response would be sufficient for accurately responding to the query.",
383
- ConversationCommand.Diagram: "Use this if the user is requesting a diagram or visual representation that requires primitives like lines, rectangles, and text.",
380
+ ConversationCommand.Image: "Use this if you are confident the user is requesting you to create a new picture based on their description. This DOES NOT support generating charts or graphs. It is for creative images.",
381
+ ConversationCommand.Text: "Use this if a normal text response would be sufficient for accurately responding to the query or you don't feel strongly about the other modes.",
382
+ ConversationCommand.Diagram: "Use this if the user is requesting a diagram or visual representation that requires primitives like lines, rectangles, and text. This does not work for charts, graphs, or quantitative data. It is for mind mapping, flowcharts, etc.",
384
383
  }
385
384
 
386
385
  mode_descriptions_for_agent = {
@@ -2,12 +2,13 @@ import logging
2
2
  import os
3
3
  from typing import Tuple
4
4
 
5
+ import openai
6
+
5
7
  from khoj.database.adapters import ConversationAdapters
6
8
  from khoj.database.models import (
7
9
  ChatModelOptions,
8
10
  KhojUser,
9
11
  OpenAIProcessorConversationConfig,
10
- ServerChatSettings,
11
12
  SpeechToTextModelOptions,
12
13
  TextToImageModelConfig,
13
14
  )
@@ -42,14 +43,32 @@ def initialization(interactive: bool = True):
42
43
  "🗣️ Configure chat models available to your server. You can always update these at /server/admin using your admin account"
43
44
  )
44
45
 
46
+ openai_api_base = os.getenv("OPENAI_API_BASE")
47
+ provider = "Ollama" if openai_api_base and openai_api_base.endswith(":11434/v1/") else "OpenAI"
48
+ openai_api_key = os.getenv("OPENAI_API_KEY", "placeholder" if openai_api_base else None)
49
+ default_chat_models = default_openai_chat_models
50
+ if openai_api_base:
51
+ # Get available chat models from OpenAI compatible API
52
+ try:
53
+ openai_client = openai.OpenAI(api_key=openai_api_key, base_url=openai_api_base)
54
+ default_chat_models = [model.id for model in openai_client.models.list()]
55
+ # Put the available default OpenAI models at the top
56
+ valid_default_models = [model for model in default_openai_chat_models if model in default_chat_models]
57
+ other_available_models = [model for model in default_chat_models if model not in valid_default_models]
58
+ default_chat_models = valid_default_models + other_available_models
59
+ except Exception:
60
+ logger.warning(f"⚠️ Failed to fetch {provider} chat models. Fallback to default models. Error: {e}")
61
+
45
62
  # Set up OpenAI's online chat models
46
63
  openai_configured, openai_provider = _setup_chat_model_provider(
47
64
  ChatModelOptions.ModelType.OPENAI,
48
- default_openai_chat_models,
49
- default_api_key=os.getenv("OPENAI_API_KEY"),
65
+ default_chat_models,
66
+ default_api_key=openai_api_key,
67
+ api_base_url=openai_api_base,
50
68
  vision_enabled=True,
51
69
  is_offline=False,
52
70
  interactive=interactive,
71
+ provider_name=provider,
53
72
  )
54
73
 
55
74
  # Setup OpenAI speech to text model
@@ -87,7 +106,7 @@ def initialization(interactive: bool = True):
87
106
  ChatModelOptions.ModelType.GOOGLE,
88
107
  default_gemini_chat_models,
89
108
  default_api_key=os.getenv("GEMINI_API_KEY"),
90
- vision_enabled=False,
109
+ vision_enabled=True,
91
110
  is_offline=False,
92
111
  interactive=interactive,
93
112
  provider_name="Google Gemini",
@@ -98,7 +117,7 @@ def initialization(interactive: bool = True):
98
117
  ChatModelOptions.ModelType.ANTHROPIC,
99
118
  default_anthropic_chat_models,
100
119
  default_api_key=os.getenv("ANTHROPIC_API_KEY"),
101
- vision_enabled=False,
120
+ vision_enabled=True,
102
121
  is_offline=False,
103
122
  interactive=interactive,
104
123
  )
@@ -154,11 +173,14 @@ def initialization(interactive: bool = True):
154
173
  default_chat_models: list,
155
174
  default_api_key: str,
156
175
  interactive: bool,
176
+ api_base_url: str = None,
157
177
  vision_enabled: bool = False,
158
178
  is_offline: bool = False,
159
179
  provider_name: str = None,
160
180
  ) -> Tuple[bool, OpenAIProcessorConversationConfig]:
161
- supported_vision_models = ["gpt-4o-mini", "gpt-4o"]
181
+ supported_vision_models = (
182
+ default_openai_chat_models + default_anthropic_chat_models + default_gemini_chat_models
183
+ )
162
184
  provider_name = provider_name or model_type.name.capitalize()
163
185
  default_use_model = {True: "y", False: "n"}[default_api_key is not None or is_offline]
164
186
  use_model_provider = (
@@ -170,14 +192,16 @@ def initialization(interactive: bool = True):
170
192
 
171
193
  logger.info(f"️💬 Setting up your {provider_name} chat configuration")
172
194
 
173
- chat_model_provider = None
195
+ chat_provider = None
174
196
  if not is_offline:
175
197
  if interactive:
176
198
  user_api_key = input(f"Enter your {provider_name} API key (default: {default_api_key}): ")
177
199
  api_key = user_api_key if user_api_key != "" else default_api_key
178
200
  else:
179
201
  api_key = default_api_key
180
- chat_model_provider = OpenAIProcessorConversationConfig.objects.create(api_key=api_key, name=provider_name)
202
+ chat_provider = OpenAIProcessorConversationConfig.objects.create(
203
+ api_key=api_key, name=provider_name, api_base_url=api_base_url
204
+ )
181
205
 
182
206
  if interactive:
183
207
  chat_model_names = input(
@@ -199,13 +223,53 @@ def initialization(interactive: bool = True):
199
223
  "max_prompt_size": default_max_tokens,
200
224
  "vision_enabled": vision_enabled,
201
225
  "tokenizer": default_tokenizer,
202
- "openai_config": chat_model_provider,
226
+ "openai_config": chat_provider,
203
227
  }
204
228
 
205
229
  ChatModelOptions.objects.create(**chat_model_options)
206
230
 
207
231
  logger.info(f"🗣️ {provider_name} chat model configuration complete")
208
- return True, chat_model_provider
232
+ return True, chat_provider
233
+
234
+ def _update_chat_model_options():
235
+ """Update available chat models for OpenAI-compatible APIs"""
236
+ try:
237
+ # Get OpenAI configs with custom base URLs
238
+ custom_configs = OpenAIProcessorConversationConfig.objects.exclude(api_base_url__isnull=True)
239
+
240
+ for config in custom_configs:
241
+ try:
242
+ # Create OpenAI client with custom base URL
243
+ openai_client = openai.OpenAI(api_key=config.api_key, base_url=config.api_base_url)
244
+
245
+ # Get available models
246
+ available_models = [model.id for model in openai_client.models.list()]
247
+
248
+ # Get existing chat model options for this config
249
+ existing_models = ChatModelOptions.objects.filter(
250
+ openai_config=config, model_type=ChatModelOptions.ModelType.OPENAI
251
+ )
252
+
253
+ # Add new models
254
+ for model in available_models:
255
+ if not existing_models.filter(chat_model=model).exists():
256
+ ChatModelOptions.objects.create(
257
+ chat_model=model,
258
+ model_type=ChatModelOptions.ModelType.OPENAI,
259
+ max_prompt_size=model_to_prompt_size.get(model),
260
+ vision_enabled=model in default_openai_chat_models,
261
+ tokenizer=model_to_tokenizer.get(model),
262
+ openai_config=config,
263
+ )
264
+
265
+ # Remove models that are no longer available
266
+ existing_models.exclude(chat_model__in=available_models).delete()
267
+
268
+ except Exception as e:
269
+ logger.warning(f"Failed to update models for {config.name}: {str(e)}")
270
+
271
+ except Exception as e:
272
+ logger.error(f"Failed to update chat model options: {str(e)}")
209
273
 
210
274
  admin_user = KhojUser.objects.filter(is_staff=True).first()
211
275
  if admin_user is None:
@@ -228,3 +292,6 @@ def initialization(interactive: bool = True):
228
292
  return
229
293
  except Exception as e:
230
294
  logger.error(f"🚨 Failed to create chat configuration: {e}", exc_info=True)
295
+ else:
296
+ _update_chat_model_options()
297
+ logger.info("🗣️ Chat model configuration updated")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: khoj
3
- Version: 1.29.2.dev5
3
+ Version: 1.29.2.dev35
4
4
  Summary: Your Second Brain
5
5
  Project-URL: Homepage, https://khoj.dev
6
6
  Project-URL: Documentation, https://docs.khoj.dev