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.
- khoj/database/adapters/__init__.py +2 -0
- khoj/database/admin.py +32 -1
- khoj/interface/compiled/404/index.html +1 -1
- khoj/interface/compiled/_next/static/chunks/5538-b87b60ecc0c27ceb.js +1 -0
- khoj/interface/compiled/_next/static/chunks/796-68f9e87f9cdfda1d.js +3 -0
- khoj/interface/compiled/_next/static/chunks/8423-c0123d454681e03a.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/chat/{page-e95e87da53d725a7.js → page-e60a55d029b6216a.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/page-fcf7411ff80b6bf5.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/share/chat/{page-3a752baa5fb62e20.js → page-4a4c0f199b89bd80.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/{webpack-d37377886a1b4e56.js → webpack-323bbe2678102a2f.js} +1 -1
- khoj/interface/compiled/agents/index.html +1 -1
- khoj/interface/compiled/agents/index.txt +1 -1
- khoj/interface/compiled/automations/index.html +1 -1
- khoj/interface/compiled/automations/index.txt +1 -1
- khoj/interface/compiled/chat/index.html +1 -1
- khoj/interface/compiled/chat/index.txt +2 -2
- khoj/interface/compiled/index.html +1 -1
- khoj/interface/compiled/index.txt +2 -2
- khoj/interface/compiled/search/index.html +1 -1
- khoj/interface/compiled/search/index.txt +1 -1
- khoj/interface/compiled/settings/index.html +1 -1
- khoj/interface/compiled/settings/index.txt +1 -1
- khoj/interface/compiled/share/chat/index.html +1 -1
- khoj/interface/compiled/share/chat/index.txt +2 -2
- khoj/main.py +7 -3
- khoj/processor/content/pdf/pdf_to_entries.py +1 -1
- khoj/processor/conversation/anthropic/anthropic_chat.py +2 -3
- khoj/processor/conversation/google/gemini_chat.py +2 -3
- khoj/processor/conversation/offline/chat_model.py +2 -2
- khoj/processor/conversation/openai/gpt.py +2 -2
- khoj/processor/conversation/prompts.py +14 -69
- khoj/processor/conversation/utils.py +7 -0
- khoj/processor/tools/online_search.py +22 -4
- khoj/routers/api_chat.py +23 -15
- khoj/routers/helpers.py +36 -77
- khoj/routers/research.py +33 -30
- khoj/utils/constants.py +1 -1
- khoj/utils/helpers.py +5 -6
- khoj/utils/initialization.py +77 -10
- {khoj-1.29.2.dev5.dist-info → khoj-1.29.2.dev35.dist-info}/METADATA +1 -1
- {khoj-1.29.2.dev5.dist-info → khoj-1.29.2.dev35.dist-info}/RECORD +46 -47
- khoj/interface/compiled/_next/static/chunks/5538-32bd787d106700dc.js +0 -1
- khoj/interface/compiled/_next/static/chunks/5961-3c104d9736b7902b.js +0 -3
- khoj/interface/compiled/_next/static/chunks/8423-ffdc2b835629c7f8.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/page-774dcd8ca4459c7e.js +0 -1
- khoj/interface/web/assets/icons/favicon-128x128.ico +0 -0
- /khoj/interface/compiled/_next/static/{bIVLxe5g7EDG455p-cfe7 → bkshWraYdEa_w254xnxBc}/_buildManifest.js +0 -0
- /khoj/interface/compiled/_next/static/{bIVLxe5g7EDG455p-cfe7 → bkshWraYdEa_w254xnxBc}/_ssgManifest.js +0 -0
- {khoj-1.29.2.dev5.dist-info → khoj-1.29.2.dev35.dist-info}/WHEEL +0 -0
- {khoj-1.29.2.dev5.dist-info → khoj-1.29.2.dev35.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
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
|
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
|
-
|
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
|
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.
|
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
|
-
|
392
|
-
if not isinstance(
|
393
|
-
logger.error(f"Invalid response for determining relevant tools: {
|
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
|
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
|
-
|
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
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
"Detected previously run online search queries. Skipping iteration. Try something different."
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
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
|
-
|
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
|
-
|
315
|
-
|
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
|
-
|
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-
|
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
|
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
|
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
|
381
|
-
ConversationCommand.
|
382
|
-
ConversationCommand.
|
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 = {
|
khoj/utils/initialization.py
CHANGED
@@ -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
|
-
|
49
|
-
default_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=
|
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=
|
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 =
|
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
|
-
|
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
|
-
|
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":
|
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,
|
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")
|