khoj 1.26.2__py3-none-any.whl → 1.26.5.dev34__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. khoj/configure.py +3 -3
  2. khoj/database/adapters/__init__.py +40 -8
  3. khoj/database/migrations/0070_alter_agent_input_tools_alter_agent_output_modes.py +46 -0
  4. khoj/database/migrations/0071_subscription_enabled_trial_at_and_more.py +32 -0
  5. khoj/database/models/__init__.py +8 -3
  6. khoj/interface/compiled/404/index.html +1 -1
  7. khoj/interface/compiled/_next/static/chunks/1210.132a7e1910006bbb.js +1 -0
  8. khoj/interface/compiled/_next/static/chunks/1279-f37ee4a388ebf544.js +1 -0
  9. khoj/interface/compiled/_next/static/chunks/1459.690bf20e7d7b7090.js +1 -0
  10. khoj/interface/compiled/_next/static/chunks/1603-b9d95833e0e025e8.js +1 -0
  11. khoj/interface/compiled/_next/static/chunks/1970-1d6d0c1b00b4f343.js +1 -0
  12. khoj/interface/compiled/_next/static/chunks/2697-61fcba89fd87eab4.js +1 -0
  13. khoj/interface/compiled/_next/static/chunks/3423-8e9c420574a9fbe3.js +1 -0
  14. khoj/interface/compiled/_next/static/chunks/394-6bcb8c429f168f21.js +3 -0
  15. khoj/interface/compiled/_next/static/chunks/4602-8eeb4b76385ad159.js +1 -0
  16. khoj/interface/compiled/_next/static/chunks/5512-94c7c2bbcf58c19d.js +1 -0
  17. khoj/interface/compiled/_next/static/chunks/7113-f2e114d7034a0835.js +1 -0
  18. khoj/interface/compiled/_next/static/chunks/{4086-2c74808ba38a5a0f.js → 8840-b8d7b9f0923c6651.js} +1 -1
  19. khoj/interface/compiled/_next/static/chunks/9417-759984ad62caa3dc.js +1 -0
  20. khoj/interface/compiled/_next/static/chunks/9479-4b443fdcc99141c9.js +1 -0
  21. khoj/interface/compiled/_next/static/chunks/94ca1967.5584df65931cfe83.js +1 -0
  22. khoj/interface/compiled/_next/static/chunks/964ecbae.ea4eab2a3a835ffe.js +1 -0
  23. khoj/interface/compiled/_next/static/chunks/app/agents/page-2beaba7c9bb750bd.js +1 -0
  24. khoj/interface/compiled/_next/static/chunks/app/automations/{page-5480731341f34450.js → page-9b5c77e0b0dd772c.js} +1 -1
  25. khoj/interface/compiled/_next/static/chunks/app/chat/page-151232d8417a1ea1.js +1 -0
  26. khoj/interface/compiled/_next/static/chunks/app/factchecker/{page-e7b34316ec6f44de.js → page-798904432c2417c4.js} +1 -1
  27. khoj/interface/compiled/_next/static/chunks/app/{page-10a5aad6e04f3cf8.js → page-db4e38a5255af7ad.js} +1 -1
  28. khoj/interface/compiled/_next/static/chunks/app/search/{page-d56541c746fded7d.js → page-ab2995529ece3140.js} +1 -1
  29. khoj/interface/compiled/_next/static/chunks/app/settings/page-3e9cf5ed5ace4310.js +1 -0
  30. khoj/interface/compiled/_next/static/chunks/app/share/chat/page-6a01e07fb244c10c.js +1 -0
  31. khoj/interface/compiled/_next/static/chunks/webpack-313247d7eb764923.js +1 -0
  32. khoj/interface/compiled/_next/static/css/{c808691c459e3887.css → 3cf13271869a4aeb.css} +1 -1
  33. khoj/interface/compiled/_next/static/css/76d55eb435962b19.css +25 -0
  34. khoj/interface/compiled/_next/static/css/{3e1f1fdd70775091.css → 80bd6301fc657983.css} +1 -1
  35. khoj/interface/compiled/_next/static/css/{2de69f0be774c768.css → b70402177a7c3207.css} +1 -1
  36. khoj/interface/compiled/agents/index.html +1 -1
  37. khoj/interface/compiled/agents/index.txt +2 -2
  38. khoj/interface/compiled/automations/index.html +1 -1
  39. khoj/interface/compiled/automations/index.txt +2 -2
  40. khoj/interface/compiled/chat/index.html +1 -1
  41. khoj/interface/compiled/chat/index.txt +2 -2
  42. khoj/interface/compiled/factchecker/index.html +1 -1
  43. khoj/interface/compiled/factchecker/index.txt +2 -2
  44. khoj/interface/compiled/index.html +1 -1
  45. khoj/interface/compiled/index.txt +2 -2
  46. khoj/interface/compiled/search/index.html +1 -1
  47. khoj/interface/compiled/search/index.txt +2 -2
  48. khoj/interface/compiled/settings/index.html +1 -1
  49. khoj/interface/compiled/settings/index.txt +2 -2
  50. khoj/interface/compiled/share/chat/index.html +1 -1
  51. khoj/interface/compiled/share/chat/index.txt +2 -2
  52. khoj/processor/conversation/google/gemini_chat.py +28 -13
  53. khoj/processor/conversation/google/utils.py +34 -12
  54. khoj/processor/conversation/openai/gpt.py +4 -4
  55. khoj/processor/conversation/prompts.py +144 -0
  56. khoj/processor/conversation/utils.py +22 -13
  57. khoj/processor/image/generate.py +5 -5
  58. khoj/processor/tools/online_search.py +4 -4
  59. khoj/routers/api.py +13 -4
  60. khoj/routers/api_agents.py +41 -20
  61. khoj/routers/api_chat.py +85 -46
  62. khoj/routers/{subscription.py → api_subscription.py} +21 -3
  63. khoj/routers/auth.py +2 -2
  64. khoj/routers/helpers.py +235 -30
  65. khoj/routers/web_client.py +0 -11
  66. khoj/utils/helpers.py +7 -3
  67. {khoj-1.26.2.dist-info → khoj-1.26.5.dev34.dist-info}/METADATA +2 -2
  68. {khoj-1.26.2.dist-info → khoj-1.26.5.dev34.dist-info}/RECORD +73 -66
  69. khoj/interface/compiled/_next/static/chunks/121-7024f479c297aef0.js +0 -1
  70. khoj/interface/compiled/_next/static/chunks/1603-fa3ee48860b9dc5c.js +0 -1
  71. khoj/interface/compiled/_next/static/chunks/2697-a38d01981ad3bdf8.js +0 -1
  72. khoj/interface/compiled/_next/static/chunks/4051-2cf66369d6ca0f1d.js +0 -3
  73. khoj/interface/compiled/_next/static/chunks/477-ec86e93db10571c1.js +0 -1
  74. khoj/interface/compiled/_next/static/chunks/51-e8f5bdb69b5ea421.js +0 -1
  75. khoj/interface/compiled/_next/static/chunks/7762-79f2205740622b5c.js +0 -1
  76. khoj/interface/compiled/_next/static/chunks/9178-899fe9a6b754ecfe.js +0 -1
  77. khoj/interface/compiled/_next/static/chunks/9417-46ed3aaa639c85ef.js +0 -1
  78. khoj/interface/compiled/_next/static/chunks/9479-ea776e73f549090c.js +0 -1
  79. khoj/interface/compiled/_next/static/chunks/app/agents/page-88aa3042711107b7.js +0 -1
  80. khoj/interface/compiled/_next/static/chunks/app/chat/page-702057ccbcf27881.js +0 -1
  81. khoj/interface/compiled/_next/static/chunks/app/settings/page-e044a999468a7c5d.js +0 -1
  82. khoj/interface/compiled/_next/static/chunks/app/share/chat/page-fbbd66a4d4633438.js +0 -1
  83. khoj/interface/compiled/_next/static/chunks/webpack-64dc39af85cd2625.js +0 -1
  84. khoj/interface/compiled/_next/static/css/b9a6bf04305d98d7.css +0 -25
  85. /khoj/interface/compiled/_next/static/{eim4XajTfG4ub4ft5AEkJ → 7viHIza-WalEOzloM67l4}/_buildManifest.js +0 -0
  86. /khoj/interface/compiled/_next/static/{eim4XajTfG4ub4ft5AEkJ → 7viHIza-WalEOzloM67l4}/_ssgManifest.js +0 -0
  87. {khoj-1.26.2.dist-info → khoj-1.26.5.dev34.dist-info}/WHEEL +0 -0
  88. {khoj-1.26.2.dist-info → khoj-1.26.5.dev34.dist-info}/entry_points.txt +0 -0
  89. {khoj-1.26.2.dist-info → khoj-1.26.5.dev34.dist-info}/licenses/LICENSE +0 -0
khoj/routers/helpers.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import base64
2
3
  import hashlib
3
4
  import json
4
5
  import logging
@@ -14,6 +15,7 @@ from typing import (
14
15
  Annotated,
15
16
  Any,
16
17
  AsyncGenerator,
18
+ Callable,
17
19
  Dict,
18
20
  Iterator,
19
21
  List,
@@ -21,7 +23,7 @@ from typing import (
21
23
  Tuple,
22
24
  Union,
23
25
  )
24
- from urllib.parse import parse_qs, quote, urljoin, urlparse
26
+ from urllib.parse import parse_qs, quote, unquote, urljoin, urlparse
25
27
 
26
28
  import cron_descriptor
27
29
  import pytz
@@ -30,11 +32,13 @@ from apscheduler.job import Job
30
32
  from apscheduler.triggers.cron import CronTrigger
31
33
  from asgiref.sync import sync_to_async
32
34
  from fastapi import Depends, Header, HTTPException, Request, UploadFile
35
+ from pydantic import BaseModel
33
36
  from starlette.authentication import has_required_scope
34
37
  from starlette.requests import URL
35
38
 
36
39
  from khoj.database import adapters
37
40
  from khoj.database.adapters import (
41
+ LENGTH_OF_FREE_TRIAL,
38
42
  AgentAdapters,
39
43
  AutomationAdapters,
40
44
  ConversationAdapters,
@@ -215,6 +219,9 @@ def construct_chat_history(conversation_history: dict, n: int = 4, agent_name="A
215
219
  elif chat["by"] == "khoj" and ("text-to-image" in chat["intent"].get("type")):
216
220
  chat_history += f"User: {chat['intent']['query']}\n"
217
221
  chat_history += f"{agent_name}: [generated image redacted for space]\n"
222
+ elif chat["by"] == "khoj" and ("excalidraw" in chat["intent"].get("type")):
223
+ chat_history += f"User: {chat['intent']['query']}\n"
224
+ chat_history += f"{agent_name}: {chat['intent']['inferred-queries'][0]}\n"
218
225
  return chat_history
219
226
 
220
227
 
@@ -235,6 +242,8 @@ def get_conversation_command(query: str, any_references: bool = False) -> Conver
235
242
  return ConversationCommand.AutomatedTask
236
243
  elif query.startswith("/summarize"):
237
244
  return ConversationCommand.Summarize
245
+ elif query.startswith("/diagram"):
246
+ return ConversationCommand.Diagram
238
247
  # If no relevant notes found for the given query
239
248
  elif not any_references:
240
249
  return ConversationCommand.General
@@ -290,7 +299,7 @@ async def aget_relevant_information_sources(
290
299
  conversation_history: dict,
291
300
  is_task: bool,
292
301
  user: KhojUser,
293
- uploaded_image_url: str = None,
302
+ query_images: List[str] = None,
294
303
  agent: Agent = None,
295
304
  ):
296
305
  """
@@ -309,8 +318,8 @@ async def aget_relevant_information_sources(
309
318
 
310
319
  chat_history = construct_chat_history(conversation_history)
311
320
 
312
- if uploaded_image_url:
313
- query = f"[placeholder for user attached image]\n{query}"
321
+ if query_images:
322
+ query = f"[placeholder for {len(query_images)} user attached images]\n{query}"
314
323
 
315
324
  personality_context = (
316
325
  prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
@@ -367,7 +376,7 @@ async def aget_relevant_output_modes(
367
376
  conversation_history: dict,
368
377
  is_task: bool = False,
369
378
  user: KhojUser = None,
370
- uploaded_image_url: str = None,
379
+ query_images: List[str] = None,
371
380
  agent: Agent = None,
372
381
  ):
373
382
  """
@@ -389,8 +398,8 @@ async def aget_relevant_output_modes(
389
398
 
390
399
  chat_history = construct_chat_history(conversation_history)
391
400
 
392
- if uploaded_image_url:
393
- query = f"[placeholder for user attached image]\n{query}"
401
+ if query_images:
402
+ query = f"[placeholder for {len(query_images)} user attached images]\n{query}"
394
403
 
395
404
  personality_context = (
396
405
  prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
@@ -433,7 +442,7 @@ async def infer_webpage_urls(
433
442
  conversation_history: dict,
434
443
  location_data: LocationData,
435
444
  user: KhojUser,
436
- uploaded_image_url: str = None,
445
+ query_images: List[str] = None,
437
446
  agent: Agent = None,
438
447
  ) -> List[str]:
439
448
  """
@@ -459,7 +468,7 @@ async def infer_webpage_urls(
459
468
 
460
469
  with timer("Chat actor: Infer webpage urls to read", logger):
461
470
  response = await send_message_to_model_wrapper(
462
- online_queries_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object", user=user
471
+ online_queries_prompt, query_images=query_images, response_type="json_object", user=user
463
472
  )
464
473
 
465
474
  # Validate that the response is a non-empty, JSON-serializable list of URLs
@@ -479,7 +488,7 @@ async def generate_online_subqueries(
479
488
  conversation_history: dict,
480
489
  location_data: LocationData,
481
490
  user: KhojUser,
482
- uploaded_image_url: str = None,
491
+ query_images: List[str] = None,
483
492
  agent: Agent = None,
484
493
  ) -> List[str]:
485
494
  """
@@ -505,7 +514,7 @@ async def generate_online_subqueries(
505
514
 
506
515
  with timer("Chat actor: Generate online search subqueries", logger):
507
516
  response = await send_message_to_model_wrapper(
508
- online_queries_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object", user=user
517
+ online_queries_prompt, query_images=query_images, response_type="json_object", user=user
509
518
  )
510
519
 
511
520
  # Validate that the response is a non-empty, JSON-serializable list
@@ -524,7 +533,7 @@ async def generate_online_subqueries(
524
533
 
525
534
 
526
535
  async def schedule_query(
527
- q: str, conversation_history: dict, user: KhojUser, uploaded_image_url: str = None
536
+ q: str, conversation_history: dict, user: KhojUser, query_images: List[str] = None
528
537
  ) -> Tuple[str, ...]:
529
538
  """
530
539
  Schedule the date, time to run the query. Assume the server timezone is UTC.
@@ -537,7 +546,7 @@ async def schedule_query(
537
546
  )
538
547
 
539
548
  raw_response = await send_message_to_model_wrapper(
540
- crontime_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object", user=user
549
+ crontime_prompt, query_images=query_images, response_type="json_object", user=user
541
550
  )
542
551
 
543
552
  # Validate that the response is a non-empty, JSON-serializable list
@@ -583,7 +592,7 @@ async def extract_relevant_summary(
583
592
  q: str,
584
593
  corpus: str,
585
594
  conversation_history: dict,
586
- uploaded_image_url: str = None,
595
+ query_images: List[str] = None,
587
596
  user: KhojUser = None,
588
597
  agent: Agent = None,
589
598
  ) -> Union[str, None]:
@@ -612,11 +621,134 @@ async def extract_relevant_summary(
612
621
  extract_relevant_information,
613
622
  prompts.system_prompt_extract_relevant_summary,
614
623
  user=user,
615
- uploaded_image_url=uploaded_image_url,
624
+ query_images=query_images,
616
625
  )
617
626
  return response.strip()
618
627
 
619
628
 
629
+ async def generate_excalidraw_diagram(
630
+ q: str,
631
+ conversation_history: Dict[str, Any],
632
+ location_data: LocationData,
633
+ note_references: List[Dict[str, Any]],
634
+ online_results: Optional[dict] = None,
635
+ query_images: List[str] = None,
636
+ user: KhojUser = None,
637
+ agent: Agent = None,
638
+ send_status_func: Optional[Callable] = None,
639
+ ):
640
+ if send_status_func:
641
+ async for event in send_status_func("**Enhancing the Diagramming Prompt**"):
642
+ yield {ChatEvent.STATUS: event}
643
+
644
+ better_diagram_description_prompt = await generate_better_diagram_description(
645
+ q=q,
646
+ conversation_history=conversation_history,
647
+ location_data=location_data,
648
+ note_references=note_references,
649
+ online_results=online_results,
650
+ query_images=query_images,
651
+ user=user,
652
+ agent=agent,
653
+ )
654
+
655
+ if send_status_func:
656
+ async for event in send_status_func(f"**Diagram to Create:**:\n{better_diagram_description_prompt}"):
657
+ yield {ChatEvent.STATUS: event}
658
+
659
+ excalidraw_diagram_description = await generate_excalidraw_diagram_from_description(
660
+ q=better_diagram_description_prompt,
661
+ user=user,
662
+ agent=agent,
663
+ )
664
+
665
+ yield better_diagram_description_prompt, excalidraw_diagram_description
666
+
667
+
668
+ async def generate_better_diagram_description(
669
+ q: str,
670
+ conversation_history: Dict[str, Any],
671
+ location_data: LocationData,
672
+ note_references: List[Dict[str, Any]],
673
+ online_results: Optional[dict] = None,
674
+ query_images: List[str] = None,
675
+ user: KhojUser = None,
676
+ agent: Agent = None,
677
+ ) -> str:
678
+ """
679
+ Generate a diagram description from the given query and context
680
+ """
681
+
682
+ today_date = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d, %A")
683
+ personality_context = (
684
+ prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
685
+ )
686
+
687
+ if location_data:
688
+ location_prompt = prompts.user_location.format(location=f"{location_data}")
689
+ else:
690
+ location_prompt = "Unknown"
691
+
692
+ user_references = "\n\n".join([f"# {item['compiled']}" for item in note_references])
693
+
694
+ chat_history = construct_chat_history(conversation_history)
695
+
696
+ simplified_online_results = {}
697
+
698
+ if online_results:
699
+ for result in online_results:
700
+ if online_results[result].get("answerBox"):
701
+ simplified_online_results[result] = online_results[result]["answerBox"]
702
+ elif online_results[result].get("webpages"):
703
+ simplified_online_results[result] = online_results[result]["webpages"]
704
+
705
+ improve_diagram_description_prompt = prompts.improve_diagram_description_prompt.format(
706
+ query=q,
707
+ chat_history=chat_history,
708
+ location=location_prompt,
709
+ current_date=today_date,
710
+ references=user_references,
711
+ online_results=simplified_online_results,
712
+ personality_context=personality_context,
713
+ )
714
+
715
+ with timer("Chat actor: Generate better diagram description", logger):
716
+ response = await send_message_to_model_wrapper(
717
+ improve_diagram_description_prompt, query_images=query_images, user=user
718
+ )
719
+ response = response.strip()
720
+ if response.startswith(('"', "'")) and response.endswith(('"', "'")):
721
+ response = response[1:-1]
722
+
723
+ return response
724
+
725
+
726
+ async def generate_excalidraw_diagram_from_description(
727
+ q: str,
728
+ user: KhojUser = None,
729
+ agent: Agent = None,
730
+ ) -> str:
731
+ personality_context = (
732
+ prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
733
+ )
734
+
735
+ excalidraw_diagram_generation = prompts.excalidraw_diagram_generation_prompt.format(
736
+ personality_context=personality_context,
737
+ query=q,
738
+ )
739
+
740
+ with timer("Chat actor: Generate excalidraw diagram", logger):
741
+ raw_response = await send_message_to_model_wrapper(message=excalidraw_diagram_generation, user=user)
742
+ raw_response = raw_response.strip()
743
+ raw_response = remove_json_codeblock(raw_response)
744
+ response: Dict[str, str] = json.loads(raw_response)
745
+ if not response or not isinstance(response, List) or not isinstance(response[0], Dict):
746
+ # TODO Some additional validation here that it's a valid Excalidraw diagram
747
+ raise AssertionError(f"Invalid response for improving diagram description: {response}")
748
+
749
+ return response
750
+
751
+
620
752
  async def generate_better_image_prompt(
621
753
  q: str,
622
754
  conversation_history: str,
@@ -624,7 +756,7 @@ async def generate_better_image_prompt(
624
756
  note_references: List[Dict[str, Any]],
625
757
  online_results: Optional[dict] = None,
626
758
  model_type: Optional[str] = None,
627
- uploaded_image_url: Optional[str] = None,
759
+ query_images: Optional[List[str]] = None,
628
760
  user: KhojUser = None,
629
761
  agent: Agent = None,
630
762
  ) -> str:
@@ -676,7 +808,7 @@ async def generate_better_image_prompt(
676
808
  )
677
809
 
678
810
  with timer("Chat actor: Generate contextual image prompt", logger):
679
- response = await send_message_to_model_wrapper(image_prompt, uploaded_image_url=uploaded_image_url, user=user)
811
+ response = await send_message_to_model_wrapper(image_prompt, query_images=query_images, user=user)
680
812
  response = response.strip()
681
813
  if response.startswith(('"', "'")) and response.endswith(('"', "'")):
682
814
  response = response[1:-1]
@@ -689,11 +821,11 @@ async def send_message_to_model_wrapper(
689
821
  system_message: str = "",
690
822
  response_type: str = "text",
691
823
  user: KhojUser = None,
692
- uploaded_image_url: str = None,
824
+ query_images: List[str] = None,
693
825
  ):
694
826
  conversation_config: ChatModelOptions = await ConversationAdapters.aget_default_conversation_config(user)
695
827
  vision_available = conversation_config.vision_enabled
696
- if not vision_available and uploaded_image_url:
828
+ if not vision_available and query_images:
697
829
  vision_enabled_config = await ConversationAdapters.aget_vision_enabled_config()
698
830
  if vision_enabled_config:
699
831
  conversation_config = vision_enabled_config
@@ -746,7 +878,7 @@ async def send_message_to_model_wrapper(
746
878
  max_prompt_size=max_tokens,
747
879
  tokenizer_name=tokenizer,
748
880
  vision_enabled=vision_available,
749
- uploaded_image_url=uploaded_image_url,
881
+ query_images=query_images,
750
882
  model_type=conversation_config.model_type,
751
883
  )
752
884
 
@@ -766,7 +898,7 @@ async def send_message_to_model_wrapper(
766
898
  max_prompt_size=max_tokens,
767
899
  tokenizer_name=tokenizer,
768
900
  vision_enabled=vision_available,
769
- uploaded_image_url=uploaded_image_url,
901
+ query_images=query_images,
770
902
  model_type=conversation_config.model_type,
771
903
  )
772
904
 
@@ -784,7 +916,8 @@ async def send_message_to_model_wrapper(
784
916
  max_prompt_size=max_tokens,
785
917
  tokenizer_name=tokenizer,
786
918
  vision_enabled=vision_available,
787
- uploaded_image_url=uploaded_image_url,
919
+ query_images=query_images,
920
+ model_type=conversation_config.model_type,
788
921
  )
789
922
 
790
923
  return gemini_send_message_to_model(
@@ -875,6 +1008,7 @@ def send_message_to_model_wrapper_sync(
875
1008
  model_name=chat_model,
876
1009
  max_prompt_size=max_tokens,
877
1010
  vision_enabled=vision_available,
1011
+ model_type=conversation_config.model_type,
878
1012
  )
879
1013
 
880
1014
  return gemini_send_message_to_model(
@@ -900,7 +1034,7 @@ def generate_chat_response(
900
1034
  conversation_id: str = None,
901
1035
  location_data: LocationData = None,
902
1036
  user_name: Optional[str] = None,
903
- uploaded_image_url: Optional[str] = None,
1037
+ query_images: Optional[List[str]] = None,
904
1038
  ) -> Tuple[Union[ThreadedGenerator, Iterator[str]], Dict[str, str]]:
905
1039
  # Initialize Variables
906
1040
  chat_response = None
@@ -919,12 +1053,12 @@ def generate_chat_response(
919
1053
  inferred_queries=inferred_queries,
920
1054
  client_application=client_application,
921
1055
  conversation_id=conversation_id,
922
- uploaded_image_url=uploaded_image_url,
1056
+ query_images=query_images,
923
1057
  )
924
1058
 
925
1059
  conversation_config = ConversationAdapters.get_valid_conversation_config(user, conversation)
926
1060
  vision_available = conversation_config.vision_enabled
927
- if not vision_available and uploaded_image_url:
1061
+ if not vision_available and query_images:
928
1062
  vision_enabled_config = ConversationAdapters.get_vision_enabled_config()
929
1063
  if vision_enabled_config:
930
1064
  conversation_config = vision_enabled_config
@@ -955,7 +1089,7 @@ def generate_chat_response(
955
1089
  chat_response = converse(
956
1090
  compiled_references,
957
1091
  q,
958
- image_url=uploaded_image_url,
1092
+ query_images=query_images,
959
1093
  online_results=online_results,
960
1094
  conversation_log=meta_log,
961
1095
  model=chat_model,
@@ -993,8 +1127,9 @@ def generate_chat_response(
993
1127
  chat_response = converse_gemini(
994
1128
  compiled_references,
995
1129
  q,
996
- online_results,
997
- meta_log,
1130
+ query_images=query_images,
1131
+ online_results=online_results,
1132
+ conversation_log=meta_log,
998
1133
  model=conversation_config.chat_model,
999
1134
  api_key=api_key,
1000
1135
  completion_func=partial_completion,
@@ -1004,6 +1139,7 @@ def generate_chat_response(
1004
1139
  location_data=location_data,
1005
1140
  user_name=user_name,
1006
1141
  agent=agent,
1142
+ vision_available=vision_available,
1007
1143
  )
1008
1144
 
1009
1145
  metadata.update({"chat_model": conversation_config.chat_model})
@@ -1015,6 +1151,22 @@ def generate_chat_response(
1015
1151
  return chat_response, metadata
1016
1152
 
1017
1153
 
1154
+ class ChatRequestBody(BaseModel):
1155
+ q: str
1156
+ n: Optional[int] = 7
1157
+ d: Optional[float] = None
1158
+ stream: Optional[bool] = False
1159
+ title: Optional[str] = None
1160
+ conversation_id: Optional[str] = None
1161
+ city: Optional[str] = None
1162
+ region: Optional[str] = None
1163
+ country: Optional[str] = None
1164
+ country_code: Optional[str] = None
1165
+ timezone: Optional[str] = None
1166
+ images: Optional[list[str]] = None
1167
+ create_new: Optional[bool] = False
1168
+
1169
+
1018
1170
  class ApiUserRateLimiter:
1019
1171
  def __init__(self, requests: int, subscribed_requests: int, window: int, slug: str):
1020
1172
  self.requests = requests
@@ -1060,13 +1212,58 @@ class ApiUserRateLimiter:
1060
1212
  )
1061
1213
  raise HTTPException(
1062
1214
  status_code=429,
1063
- detail="We're glad you're enjoying Khoj! You've exceeded your usage limit for today. Come back tomorrow or subscribe to increase your usage limit via [your settings](https://app.khoj.dev/settings).",
1215
+ detail="I'm glad you're enjoying interacting with me! But you've exceeded your usage limit for today. Come back tomorrow or subscribe to increase your usage limit via [your settings](https://app.khoj.dev/settings).",
1064
1216
  )
1065
1217
 
1066
1218
  # Add the current request to the cache
1067
1219
  UserRequests.objects.create(user=user, slug=self.slug)
1068
1220
 
1069
1221
 
1222
+ class ApiImageRateLimiter:
1223
+ def __init__(self, max_images: int = 10, max_combined_size_mb: float = 10):
1224
+ self.max_images = max_images
1225
+ self.max_combined_size_mb = max_combined_size_mb
1226
+
1227
+ def __call__(self, request: Request, body: ChatRequestBody):
1228
+ if state.billing_enabled is False:
1229
+ return
1230
+
1231
+ # Rate limiting is disabled if user unauthenticated.
1232
+ # Other systems handle authentication
1233
+ if not request.user.is_authenticated:
1234
+ return
1235
+
1236
+ if not body.images:
1237
+ return
1238
+
1239
+ # Check number of images
1240
+ if len(body.images) > self.max_images:
1241
+ raise HTTPException(
1242
+ status_code=429,
1243
+ detail=f"Those are way too many images for me! I can handle up to {self.max_images} images per message.",
1244
+ )
1245
+
1246
+ # Check total size of images
1247
+ total_size_mb = 0.0
1248
+ for image in body.images:
1249
+ # Unquote the image in case it's URL encoded
1250
+ image = unquote(image)
1251
+ # Assuming the image is a base64 encoded string
1252
+ # Remove the data:image/jpeg;base64, part if present
1253
+ if "," in image:
1254
+ image = image.split(",", 1)[1]
1255
+
1256
+ # Decode base64 to get the actual size
1257
+ image_bytes = base64.b64decode(image)
1258
+ total_size_mb += len(image_bytes) / (1024 * 1024) # Convert bytes to MB
1259
+
1260
+ if total_size_mb > self.max_combined_size_mb:
1261
+ raise HTTPException(
1262
+ status_code=429,
1263
+ detail=f"Those images are way too large for me! I can handle up to {self.max_combined_size_mb}MB of images per message.",
1264
+ )
1265
+
1266
+
1070
1267
  class ConversationCommandRateLimiter:
1071
1268
  def __init__(self, trial_rate_limit: int, subscribed_rate_limit: int, slug: str):
1072
1269
  self.slug = slug
@@ -1477,10 +1674,16 @@ def get_user_config(user: KhojUser, request: Request, is_detailed: bool = False)
1477
1674
 
1478
1675
  user_subscription_state = get_user_subscription_state(user.email)
1479
1676
  user_subscription = adapters.get_user_subscription(user.email)
1677
+
1480
1678
  subscription_renewal_date = (
1481
1679
  user_subscription.renewal_date.strftime("%d %b %Y")
1482
1680
  if user_subscription and user_subscription.renewal_date
1483
- else (user_subscription.created_at + timedelta(days=7)).strftime("%d %b %Y")
1681
+ else None
1682
+ )
1683
+ subscription_enabled_trial_at = (
1684
+ user_subscription.enabled_trial_at.strftime("%d %b %Y")
1685
+ if user_subscription and user_subscription.enabled_trial_at
1686
+ else None
1484
1687
  )
1485
1688
  given_name = get_user_name(user)
1486
1689
 
@@ -1553,6 +1756,7 @@ def get_user_config(user: KhojUser, request: Request, is_detailed: bool = False)
1553
1756
  # user billing info
1554
1757
  "subscription_state": user_subscription_state,
1555
1758
  "subscription_renewal_date": subscription_renewal_date,
1759
+ "subscription_enabled_trial_at": subscription_enabled_trial_at,
1556
1760
  # server settings
1557
1761
  "khoj_cloud_subscription_url": os.getenv("KHOJ_CLOUD_SUBSCRIPTION_URL"),
1558
1762
  "billing_enabled": state.billing_enabled,
@@ -1561,6 +1765,7 @@ def get_user_config(user: KhojUser, request: Request, is_detailed: bool = False)
1561
1765
  "khoj_version": state.khoj_version,
1562
1766
  "anonymous_mode": state.anonymous_mode,
1563
1767
  "notion_oauth_url": notion_oauth_url,
1768
+ "length_of_free_trial": LENGTH_OF_FREE_TRIAL,
1564
1769
  }
1565
1770
 
1566
1771
 
@@ -51,17 +51,6 @@ def chat_page(request: Request):
51
51
  )
52
52
 
53
53
 
54
- @web_client.get("/experimental", response_class=FileResponse)
55
- @requires(["authenticated"], redirect="login_page")
56
- def experimental_page(request: Request):
57
- return templates.TemplateResponse(
58
- "index.html",
59
- context={
60
- "request": request,
61
- },
62
- )
63
-
64
-
65
54
  @web_client.get("/factchecker", response_class=FileResponse)
66
55
  def fact_checker_page(request: Request):
67
56
  return templates.TemplateResponse(
khoj/utils/helpers.py CHANGED
@@ -318,6 +318,7 @@ class ConversationCommand(str, Enum):
318
318
  Automation = "automation"
319
319
  AutomatedTask = "automated_task"
320
320
  Summarize = "summarize"
321
+ Diagram = "diagram"
321
322
 
322
323
 
323
324
  command_descriptions = {
@@ -326,10 +327,11 @@ command_descriptions = {
326
327
  ConversationCommand.Default: "The default command when no command specified. It intelligently auto-switches between general and notes mode.",
327
328
  ConversationCommand.Online: "Search for information on the internet.",
328
329
  ConversationCommand.Webpage: "Get information from webpage suggested by you.",
329
- ConversationCommand.Image: "Generate images by describing your imagination in words.",
330
+ ConversationCommand.Image: "Generate illustrative, creative images by describing your imagination in words.",
330
331
  ConversationCommand.Automation: "Automatically run your query at a specified time or interval.",
331
332
  ConversationCommand.Help: "Get help with how to use or setup Khoj from the documentation",
332
333
  ConversationCommand.Summarize: "Get help with a question pertaining to an entire document.",
334
+ ConversationCommand.Diagram: "Draw a flowchart, diagram, or any other visual representation best expressed with primitives like lines, rectangles, and text.",
333
335
  }
334
336
 
335
337
  command_descriptions_for_agent = {
@@ -350,15 +352,17 @@ tool_descriptions_for_llm = {
350
352
  }
351
353
 
352
354
  mode_descriptions_for_llm = {
353
- ConversationCommand.Image: "Use this if the user is requesting you to generate a picture based on their description.",
355
+ ConversationCommand.Image: "Use this if the user is requesting you to create a new picture based on their description.",
354
356
  ConversationCommand.Automation: "Use this if you are confident the user is requesting a response at a scheduled date, time and frequency",
355
- ConversationCommand.Text: "Use this if the other response modes don't seem to fit the query.",
357
+ ConversationCommand.Text: "Use this if a normal text response would be sufficient for accurately responding to the query.",
358
+ ConversationCommand.Diagram: "Use this if the user is requesting a visual representation that requires primitives like lines, rectangles, and text.",
356
359
  }
357
360
 
358
361
  mode_descriptions_for_agent = {
359
362
  ConversationCommand.Image: "Agent can generate image in response.",
360
363
  ConversationCommand.Automation: "Agent can schedule a task to run at a scheduled date, time and frequency in response.",
361
364
  ConversationCommand.Text: "Agent can generate text in response.",
365
+ ConversationCommand.Diagram: "Agent can generate a visual representation that requires primitives like lines, rectangles, and text.",
362
366
  }
363
367
 
364
368
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: khoj
3
- Version: 1.26.2
3
+ Version: 1.26.5.dev34
4
4
  Summary: Your Second Brain
5
5
  Project-URL: Homepage, https://khoj.dev
6
6
  Project-URL: Documentation, https://docs.khoj.dev
@@ -56,7 +56,7 @@ Requires-Dist: pillow~=10.0.0
56
56
  Requires-Dist: psutil>=5.8.0
57
57
  Requires-Dist: psycopg2-binary==2.9.9
58
58
  Requires-Dist: pydantic[email]>=2.0.0
59
- Requires-Dist: pymupdf>=1.23.5
59
+ Requires-Dist: pymupdf==1.24.11
60
60
  Requires-Dist: python-multipart>=0.0.7
61
61
  Requires-Dist: pytz~=2024.1
62
62
  Requires-Dist: pyyaml~=6.0