khoj 1.33.3.dev32__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 (393) hide show
  1. khoj/__init__.py +0 -0
  2. khoj/app/README.md +94 -0
  3. khoj/app/__init__.py +0 -0
  4. khoj/app/asgi.py +16 -0
  5. khoj/app/settings.py +218 -0
  6. khoj/app/urls.py +25 -0
  7. khoj/configure.py +452 -0
  8. khoj/database/__init__.py +0 -0
  9. khoj/database/adapters/__init__.py +1821 -0
  10. khoj/database/admin.py +417 -0
  11. khoj/database/apps.py +6 -0
  12. khoj/database/management/__init__.py +0 -0
  13. khoj/database/management/commands/__init__.py +0 -0
  14. khoj/database/management/commands/change_default_model.py +116 -0
  15. khoj/database/management/commands/change_generated_images_url.py +61 -0
  16. khoj/database/management/commands/convert_images_png_to_webp.py +99 -0
  17. khoj/database/migrations/0001_khojuser.py +98 -0
  18. khoj/database/migrations/0002_googleuser.py +32 -0
  19. khoj/database/migrations/0003_vector_extension.py +10 -0
  20. khoj/database/migrations/0004_content_types_and_more.py +181 -0
  21. khoj/database/migrations/0005_embeddings_corpus_id.py +19 -0
  22. khoj/database/migrations/0006_embeddingsdates.py +33 -0
  23. khoj/database/migrations/0007_add_conversation.py +27 -0
  24. khoj/database/migrations/0008_alter_conversation_conversation_log.py +17 -0
  25. khoj/database/migrations/0009_khojapiuser.py +24 -0
  26. khoj/database/migrations/0010_chatmodeloptions_and_more.py +83 -0
  27. khoj/database/migrations/0010_rename_embeddings_entry_and_more.py +30 -0
  28. khoj/database/migrations/0011_merge_20231102_0138.py +14 -0
  29. khoj/database/migrations/0012_entry_file_source.py +21 -0
  30. khoj/database/migrations/0013_subscription.py +37 -0
  31. khoj/database/migrations/0014_alter_googleuser_picture.py +17 -0
  32. khoj/database/migrations/0015_alter_subscription_user.py +21 -0
  33. khoj/database/migrations/0016_alter_subscription_renewal_date.py +17 -0
  34. khoj/database/migrations/0017_searchmodel.py +32 -0
  35. khoj/database/migrations/0018_searchmodelconfig_delete_searchmodel.py +30 -0
  36. khoj/database/migrations/0019_alter_googleuser_family_name_and_more.py +27 -0
  37. khoj/database/migrations/0020_reflectivequestion.py +36 -0
  38. khoj/database/migrations/0021_speechtotextmodeloptions_and_more.py +42 -0
  39. khoj/database/migrations/0022_texttoimagemodelconfig.py +25 -0
  40. khoj/database/migrations/0023_usersearchmodelconfig.py +33 -0
  41. khoj/database/migrations/0024_alter_entry_embeddings.py +18 -0
  42. khoj/database/migrations/0025_clientapplication_khojuser_phone_number_and_more.py +46 -0
  43. khoj/database/migrations/0025_searchmodelconfig_embeddings_inference_endpoint_and_more.py +22 -0
  44. khoj/database/migrations/0026_searchmodelconfig_cross_encoder_inference_endpoint_and_more.py +22 -0
  45. khoj/database/migrations/0027_merge_20240118_1324.py +13 -0
  46. khoj/database/migrations/0028_khojuser_verified_phone_number.py +17 -0
  47. khoj/database/migrations/0029_userrequests.py +27 -0
  48. khoj/database/migrations/0030_conversation_slug_and_title.py +38 -0
  49. khoj/database/migrations/0031_agent_conversation_agent.py +53 -0
  50. khoj/database/migrations/0031_alter_googleuser_locale.py +30 -0
  51. khoj/database/migrations/0032_merge_20240322_0427.py +14 -0
  52. khoj/database/migrations/0033_rename_tuning_agent_personality.py +17 -0
  53. khoj/database/migrations/0034_alter_chatmodeloptions_chat_model.py +32 -0
  54. khoj/database/migrations/0035_processlock.py +26 -0
  55. khoj/database/migrations/0036_alter_processlock_name.py +19 -0
  56. khoj/database/migrations/0036_delete_offlinechatprocessorconversationconfig.py +15 -0
  57. khoj/database/migrations/0036_publicconversation.py +42 -0
  58. khoj/database/migrations/0037_chatmodeloptions_openai_config_and_more.py +51 -0
  59. khoj/database/migrations/0037_searchmodelconfig_bi_encoder_docs_encode_config_and_more.py +32 -0
  60. khoj/database/migrations/0038_merge_20240425_0857.py +14 -0
  61. khoj/database/migrations/0038_merge_20240426_1640.py +12 -0
  62. khoj/database/migrations/0039_merge_20240501_0301.py +12 -0
  63. khoj/database/migrations/0040_alter_processlock_name.py +26 -0
  64. khoj/database/migrations/0040_merge_20240504_1010.py +14 -0
  65. khoj/database/migrations/0041_merge_20240505_1234.py +14 -0
  66. khoj/database/migrations/0042_serverchatsettings.py +46 -0
  67. khoj/database/migrations/0043_alter_chatmodeloptions_model_type.py +21 -0
  68. khoj/database/migrations/0044_conversation_file_filters.py +17 -0
  69. khoj/database/migrations/0045_fileobject.py +37 -0
  70. khoj/database/migrations/0046_khojuser_email_verification_code_and_more.py +22 -0
  71. khoj/database/migrations/0047_alter_entry_file_type.py +31 -0
  72. khoj/database/migrations/0048_voicemodeloption_uservoicemodelconfig.py +52 -0
  73. khoj/database/migrations/0049_datastore.py +38 -0
  74. khoj/database/migrations/0049_texttoimagemodelconfig_api_key_and_more.py +58 -0
  75. khoj/database/migrations/0050_alter_processlock_name.py +25 -0
  76. khoj/database/migrations/0051_merge_20240702_1220.py +14 -0
  77. khoj/database/migrations/0052_alter_searchmodelconfig_bi_encoder_docs_encode_config_and_more.py +27 -0
  78. khoj/database/migrations/0053_agent_style_color_agent_style_icon.py +61 -0
  79. khoj/database/migrations/0054_alter_agent_style_color.py +38 -0
  80. khoj/database/migrations/0055_alter_agent_style_icon.py +37 -0
  81. khoj/database/migrations/0056_chatmodeloptions_vision_enabled.py +17 -0
  82. khoj/database/migrations/0056_searchmodelconfig_cross_encoder_model_config.py +17 -0
  83. khoj/database/migrations/0057_merge_20240816_1409.py +13 -0
  84. khoj/database/migrations/0057_remove_serverchatsettings_default_model_and_more.py +51 -0
  85. khoj/database/migrations/0058_alter_chatmodeloptions_chat_model.py +17 -0
  86. khoj/database/migrations/0059_searchmodelconfig_bi_encoder_confidence_threshold.py +17 -0
  87. khoj/database/migrations/0060_merge_20240905_1828.py +14 -0
  88. khoj/database/migrations/0061_alter_chatmodeloptions_model_type.py +26 -0
  89. khoj/database/migrations/0061_alter_texttoimagemodelconfig_model_type.py +21 -0
  90. khoj/database/migrations/0062_merge_20240913_0222.py +14 -0
  91. khoj/database/migrations/0063_conversation_temp_id.py +36 -0
  92. khoj/database/migrations/0064_remove_conversation_temp_id_alter_conversation_id.py +86 -0
  93. khoj/database/migrations/0065_remove_agent_avatar_remove_agent_public_and_more.py +49 -0
  94. khoj/database/migrations/0066_remove_agent_tools_agent_input_tools_and_more.py +69 -0
  95. khoj/database/migrations/0067_alter_agent_style_icon.py +50 -0
  96. khoj/database/migrations/0068_alter_agent_output_modes.py +24 -0
  97. khoj/database/migrations/0069_webscraper_serverchatsettings_web_scraper.py +89 -0
  98. khoj/database/migrations/0070_alter_agent_input_tools_alter_agent_output_modes.py +46 -0
  99. khoj/database/migrations/0071_subscription_enabled_trial_at_and_more.py +32 -0
  100. khoj/database/migrations/0072_entry_search_model.py +24 -0
  101. khoj/database/migrations/0073_delete_usersearchmodelconfig.py +15 -0
  102. khoj/database/migrations/0074_alter_conversation_title.py +17 -0
  103. khoj/database/migrations/0075_migrate_generated_assets_and_validate.py +85 -0
  104. khoj/database/migrations/0076_rename_openaiprocessorconversationconfig_aimodelapi_and_more.py +26 -0
  105. khoj/database/migrations/0077_chatmodel_alter_agent_chat_model_and_more.py +62 -0
  106. khoj/database/migrations/0078_khojuser_email_verification_code_expiry.py +17 -0
  107. khoj/database/migrations/__init__.py +0 -0
  108. khoj/database/models/__init__.py +725 -0
  109. khoj/database/tests.py +3 -0
  110. khoj/interface/compiled/404/index.html +1 -0
  111. khoj/interface/compiled/_next/static/Tg-vU1p1B-YKT5Qv8KSHt/_buildManifest.js +1 -0
  112. khoj/interface/compiled/_next/static/Tg-vU1p1B-YKT5Qv8KSHt/_ssgManifest.js +1 -0
  113. khoj/interface/compiled/_next/static/chunks/1010-8f39bb4648b5ba10.js +1 -0
  114. khoj/interface/compiled/_next/static/chunks/182-f1c48a203dc91e0e.js +20 -0
  115. khoj/interface/compiled/_next/static/chunks/1915-d3c36ad6ce697ce7.js +1 -0
  116. khoj/interface/compiled/_next/static/chunks/2117-165ef4747a5b836b.js +2 -0
  117. khoj/interface/compiled/_next/static/chunks/2581-455000f8aeb08fc3.js +1 -0
  118. khoj/interface/compiled/_next/static/chunks/3727.dcea8f2193111552.js +1 -0
  119. khoj/interface/compiled/_next/static/chunks/3789-a09e37a819171a9d.js +1 -0
  120. khoj/interface/compiled/_next/static/chunks/4124-6c28322ce218d2d5.js +1 -0
  121. khoj/interface/compiled/_next/static/chunks/5427-b52d95253e692bfa.js +1 -0
  122. khoj/interface/compiled/_next/static/chunks/5473-b1cf56dedac6577a.js +1 -0
  123. khoj/interface/compiled/_next/static/chunks/5477-0bbddb79c25a54a7.js +1 -0
  124. khoj/interface/compiled/_next/static/chunks/6065-64db9ad305ba0bcd.js +1 -0
  125. khoj/interface/compiled/_next/static/chunks/6293-469dd16402ea8a6f.js +3 -0
  126. khoj/interface/compiled/_next/static/chunks/688-b5b4391bbc0376f1.js +1 -0
  127. khoj/interface/compiled/_next/static/chunks/8667-b6bf63c72b2d76eb.js +1 -0
  128. khoj/interface/compiled/_next/static/chunks/9259-1172dbaca0515237.js +1 -0
  129. khoj/interface/compiled/_next/static/chunks/94ca1967.1d9b42d929a1ee8c.js +1 -0
  130. khoj/interface/compiled/_next/static/chunks/9597.83583248dfbf6e73.js +1 -0
  131. khoj/interface/compiled/_next/static/chunks/964ecbae.51d6faf8801d15e6.js +1 -0
  132. khoj/interface/compiled/_next/static/chunks/9665-391df1e5c51c960a.js +1 -0
  133. khoj/interface/compiled/_next/static/chunks/app/_not-found/page-a834eddae3e235df.js +1 -0
  134. khoj/interface/compiled/_next/static/chunks/app/agents/layout-e00fb81dca656a10.js +1 -0
  135. khoj/interface/compiled/_next/static/chunks/app/agents/page-28ce086a1129bca2.js +1 -0
  136. khoj/interface/compiled/_next/static/chunks/app/automations/layout-1fe1537449f43496.js +1 -0
  137. khoj/interface/compiled/_next/static/chunks/app/automations/page-bf365a60829d347f.js +1 -0
  138. khoj/interface/compiled/_next/static/chunks/app/chat/layout-33934fc2d6ae6838.js +1 -0
  139. khoj/interface/compiled/_next/static/chunks/app/chat/page-0e476e57eb2015e3.js +1 -0
  140. khoj/interface/compiled/_next/static/chunks/app/layout-30e7fda7262713ce.js +1 -0
  141. khoj/interface/compiled/_next/static/chunks/app/page-a5515ea71aec5ef0.js +1 -0
  142. khoj/interface/compiled/_next/static/chunks/app/search/layout-c02531d586972d7d.js +1 -0
  143. khoj/interface/compiled/_next/static/chunks/app/search/page-9140541e67ea307d.js +1 -0
  144. khoj/interface/compiled/_next/static/chunks/app/settings/layout-d09d6510a45cd4bd.js +1 -0
  145. khoj/interface/compiled/_next/static/chunks/app/settings/page-951ba40b5b94b23a.js +1 -0
  146. khoj/interface/compiled/_next/static/chunks/app/share/chat/layout-e8e5db7830bf3f47.js +1 -0
  147. khoj/interface/compiled/_next/static/chunks/app/share/chat/page-1beb80d8d741c932.js +1 -0
  148. khoj/interface/compiled/_next/static/chunks/d3ac728e-44ebd2a0c99b12a0.js +1 -0
  149. khoj/interface/compiled/_next/static/chunks/fd9d1056-4482b99a36fd1673.js +1 -0
  150. khoj/interface/compiled/_next/static/chunks/framework-8e0e0f4a6b83a956.js +1 -0
  151. khoj/interface/compiled/_next/static/chunks/main-app-de1f09df97a3cfc7.js +1 -0
  152. khoj/interface/compiled/_next/static/chunks/main-db4bfac6b0a8d00b.js +1 -0
  153. khoj/interface/compiled/_next/static/chunks/pages/_app-3c9ca398d360b709.js +1 -0
  154. khoj/interface/compiled/_next/static/chunks/pages/_error-cf5ca766ac8f493f.js +1 -0
  155. khoj/interface/compiled/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  156. khoj/interface/compiled/_next/static/chunks/webpack-a03962458328b163.js +1 -0
  157. khoj/interface/compiled/_next/static/css/089de1d8526b96e9.css +1 -0
  158. khoj/interface/compiled/_next/static/css/37a73b87f02df402.css +1 -0
  159. khoj/interface/compiled/_next/static/css/4e4e6a4a1c920d06.css +1 -0
  160. khoj/interface/compiled/_next/static/css/8d02837c730f8d13.css +25 -0
  161. khoj/interface/compiled/_next/static/css/8e6a3ca11a60b189.css +1 -0
  162. khoj/interface/compiled/_next/static/css/9c164d9727dd8092.css +1 -0
  163. khoj/interface/compiled/_next/static/css/dac88c17aaee5fcf.css +1 -0
  164. khoj/interface/compiled/_next/static/css/df4b47a2d0d85eae.css +1 -0
  165. khoj/interface/compiled/_next/static/css/e4eb883b5265d372.css +1 -0
  166. khoj/interface/compiled/_next/static/media/1d8a05b60287ae6c-s.p.woff2 +0 -0
  167. khoj/interface/compiled/_next/static/media/6f22fce21a7c433c-s.woff2 +0 -0
  168. khoj/interface/compiled/_next/static/media/77c207b095007c34-s.p.woff2 +0 -0
  169. khoj/interface/compiled/_next/static/media/82ef96de0e8f4d8c-s.p.woff2 +0 -0
  170. khoj/interface/compiled/_next/static/media/KaTeX_AMS-Regular.1608a09b.woff +0 -0
  171. khoj/interface/compiled/_next/static/media/KaTeX_AMS-Regular.4aafdb68.ttf +0 -0
  172. khoj/interface/compiled/_next/static/media/KaTeX_AMS-Regular.a79f1c31.woff2 +0 -0
  173. khoj/interface/compiled/_next/static/media/KaTeX_Caligraphic-Bold.b6770918.woff +0 -0
  174. khoj/interface/compiled/_next/static/media/KaTeX_Caligraphic-Bold.cce5b8ec.ttf +0 -0
  175. khoj/interface/compiled/_next/static/media/KaTeX_Caligraphic-Bold.ec17d132.woff2 +0 -0
  176. khoj/interface/compiled/_next/static/media/KaTeX_Caligraphic-Regular.07ef19e7.ttf +0 -0
  177. khoj/interface/compiled/_next/static/media/KaTeX_Caligraphic-Regular.55fac258.woff2 +0 -0
  178. khoj/interface/compiled/_next/static/media/KaTeX_Caligraphic-Regular.dad44a7f.woff +0 -0
  179. khoj/interface/compiled/_next/static/media/KaTeX_Fraktur-Bold.9f256b85.woff +0 -0
  180. khoj/interface/compiled/_next/static/media/KaTeX_Fraktur-Bold.b18f59e1.ttf +0 -0
  181. khoj/interface/compiled/_next/static/media/KaTeX_Fraktur-Bold.d42a5579.woff2 +0 -0
  182. khoj/interface/compiled/_next/static/media/KaTeX_Fraktur-Regular.7c187121.woff +0 -0
  183. khoj/interface/compiled/_next/static/media/KaTeX_Fraktur-Regular.d3c882a6.woff2 +0 -0
  184. khoj/interface/compiled/_next/static/media/KaTeX_Fraktur-Regular.ed38e79f.ttf +0 -0
  185. khoj/interface/compiled/_next/static/media/KaTeX_Main-Bold.b74a1a8b.ttf +0 -0
  186. khoj/interface/compiled/_next/static/media/KaTeX_Main-Bold.c3fb5ac2.woff2 +0 -0
  187. khoj/interface/compiled/_next/static/media/KaTeX_Main-Bold.d181c465.woff +0 -0
  188. khoj/interface/compiled/_next/static/media/KaTeX_Main-BoldItalic.6f2bb1df.woff2 +0 -0
  189. khoj/interface/compiled/_next/static/media/KaTeX_Main-BoldItalic.70d8b0a5.ttf +0 -0
  190. khoj/interface/compiled/_next/static/media/KaTeX_Main-BoldItalic.e3f82f9d.woff +0 -0
  191. khoj/interface/compiled/_next/static/media/KaTeX_Main-Italic.47373d1e.ttf +0 -0
  192. khoj/interface/compiled/_next/static/media/KaTeX_Main-Italic.8916142b.woff2 +0 -0
  193. khoj/interface/compiled/_next/static/media/KaTeX_Main-Italic.9024d815.woff +0 -0
  194. khoj/interface/compiled/_next/static/media/KaTeX_Main-Regular.0462f03b.woff2 +0 -0
  195. khoj/interface/compiled/_next/static/media/KaTeX_Main-Regular.7f51fe03.woff +0 -0
  196. khoj/interface/compiled/_next/static/media/KaTeX_Main-Regular.b7f8fe9b.ttf +0 -0
  197. khoj/interface/compiled/_next/static/media/KaTeX_Math-BoldItalic.572d331f.woff2 +0 -0
  198. khoj/interface/compiled/_next/static/media/KaTeX_Math-BoldItalic.a879cf83.ttf +0 -0
  199. khoj/interface/compiled/_next/static/media/KaTeX_Math-BoldItalic.f1035d8d.woff +0 -0
  200. khoj/interface/compiled/_next/static/media/KaTeX_Math-Italic.5295ba48.woff +0 -0
  201. khoj/interface/compiled/_next/static/media/KaTeX_Math-Italic.939bc644.ttf +0 -0
  202. khoj/interface/compiled/_next/static/media/KaTeX_Math-Italic.f28c23ac.woff2 +0 -0
  203. khoj/interface/compiled/_next/static/media/KaTeX_SansSerif-Bold.8c5b5494.woff2 +0 -0
  204. khoj/interface/compiled/_next/static/media/KaTeX_SansSerif-Bold.94e1e8dc.ttf +0 -0
  205. khoj/interface/compiled/_next/static/media/KaTeX_SansSerif-Bold.bf59d231.woff +0 -0
  206. khoj/interface/compiled/_next/static/media/KaTeX_SansSerif-Italic.3b1e59b3.woff2 +0 -0
  207. khoj/interface/compiled/_next/static/media/KaTeX_SansSerif-Italic.7c9bc82b.woff +0 -0
  208. khoj/interface/compiled/_next/static/media/KaTeX_SansSerif-Italic.b4c20c84.ttf +0 -0
  209. khoj/interface/compiled/_next/static/media/KaTeX_SansSerif-Regular.74048478.woff +0 -0
  210. khoj/interface/compiled/_next/static/media/KaTeX_SansSerif-Regular.ba21ed5f.woff2 +0 -0
  211. khoj/interface/compiled/_next/static/media/KaTeX_SansSerif-Regular.d4d7ba48.ttf +0 -0
  212. khoj/interface/compiled/_next/static/media/KaTeX_Script-Regular.03e9641d.woff2 +0 -0
  213. khoj/interface/compiled/_next/static/media/KaTeX_Script-Regular.07505710.woff +0 -0
  214. khoj/interface/compiled/_next/static/media/KaTeX_Script-Regular.fe9cbbe1.ttf +0 -0
  215. khoj/interface/compiled/_next/static/media/KaTeX_Size1-Regular.e1e279cb.woff +0 -0
  216. khoj/interface/compiled/_next/static/media/KaTeX_Size1-Regular.eae34984.woff2 +0 -0
  217. khoj/interface/compiled/_next/static/media/KaTeX_Size1-Regular.fabc004a.ttf +0 -0
  218. khoj/interface/compiled/_next/static/media/KaTeX_Size2-Regular.57727022.woff +0 -0
  219. khoj/interface/compiled/_next/static/media/KaTeX_Size2-Regular.5916a24f.woff2 +0 -0
  220. khoj/interface/compiled/_next/static/media/KaTeX_Size2-Regular.d6b476ec.ttf +0 -0
  221. khoj/interface/compiled/_next/static/media/KaTeX_Size3-Regular.9acaf01c.woff +0 -0
  222. khoj/interface/compiled/_next/static/media/KaTeX_Size3-Regular.a144ef58.ttf +0 -0
  223. khoj/interface/compiled/_next/static/media/KaTeX_Size3-Regular.b4230e7e.woff2 +0 -0
  224. khoj/interface/compiled/_next/static/media/KaTeX_Size4-Regular.10d95fd3.woff2 +0 -0
  225. khoj/interface/compiled/_next/static/media/KaTeX_Size4-Regular.7a996c9d.woff +0 -0
  226. khoj/interface/compiled/_next/static/media/KaTeX_Size4-Regular.fbccdabe.ttf +0 -0
  227. khoj/interface/compiled/_next/static/media/KaTeX_Typewriter-Regular.6258592b.woff +0 -0
  228. khoj/interface/compiled/_next/static/media/KaTeX_Typewriter-Regular.a8709e36.woff2 +0 -0
  229. khoj/interface/compiled/_next/static/media/KaTeX_Typewriter-Regular.d97aaf4a.ttf +0 -0
  230. khoj/interface/compiled/_next/static/media/a6ecd16fa044d500-s.p.woff2 +0 -0
  231. khoj/interface/compiled/_next/static/media/bd82c78e5b7b3fe9-s.p.woff2 +0 -0
  232. khoj/interface/compiled/_next/static/media/c32c8052c071fc42-s.woff2 +0 -0
  233. khoj/interface/compiled/_next/static/media/c4250770ab8708b6-s.p.woff2 +0 -0
  234. khoj/interface/compiled/_next/static/media/e098aaaecc9cfbb2-s.p.woff2 +0 -0
  235. khoj/interface/compiled/_next/static/media/flags.3afdda2f.webp +0 -0
  236. khoj/interface/compiled/_next/static/media/flags@2x.5fbe9fc1.webp +0 -0
  237. khoj/interface/compiled/_next/static/media/globe.98e105ca.webp +0 -0
  238. khoj/interface/compiled/_next/static/media/globe@2x.974df6f8.webp +0 -0
  239. khoj/interface/compiled/agents/index.html +1 -0
  240. khoj/interface/compiled/agents/index.txt +7 -0
  241. khoj/interface/compiled/agents.svg +6 -0
  242. khoj/interface/compiled/assets/icons/khoj_lantern.ico +0 -0
  243. khoj/interface/compiled/assets/icons/khoj_lantern.svg +100 -0
  244. khoj/interface/compiled/assets/icons/khoj_lantern_1200x1200.png +0 -0
  245. khoj/interface/compiled/assets/icons/khoj_lantern_128x128.png +0 -0
  246. khoj/interface/compiled/assets/icons/khoj_lantern_128x128_dark.png +0 -0
  247. khoj/interface/compiled/assets/icons/khoj_lantern_256x256.png +0 -0
  248. khoj/interface/compiled/assets/icons/khoj_lantern_512x512.png +0 -0
  249. khoj/interface/compiled/assets/icons/khoj_lantern_logomarktype_1200x630.png +0 -0
  250. khoj/interface/compiled/assets/samples/desktop-browse-draw-sample.png +0 -0
  251. khoj/interface/compiled/assets/samples/desktop-plain-chat-sample.png +0 -0
  252. khoj/interface/compiled/assets/samples/desktop-remember-plan-sample.png +0 -0
  253. khoj/interface/compiled/assets/samples/phone-browse-draw-sample.png +0 -0
  254. khoj/interface/compiled/assets/samples/phone-plain-chat-sample.png +0 -0
  255. khoj/interface/compiled/assets/samples/phone-remember-plan-sample.png +0 -0
  256. khoj/interface/compiled/automation.svg +37 -0
  257. khoj/interface/compiled/automations/index.html +1 -0
  258. khoj/interface/compiled/automations/index.txt +8 -0
  259. khoj/interface/compiled/chat/index.html +1 -0
  260. khoj/interface/compiled/chat/index.txt +7 -0
  261. khoj/interface/compiled/chat.svg +24 -0
  262. khoj/interface/compiled/close.svg +5 -0
  263. khoj/interface/compiled/copy-button-success.svg +6 -0
  264. khoj/interface/compiled/copy-button.svg +5 -0
  265. khoj/interface/compiled/index.html +1 -0
  266. khoj/interface/compiled/index.txt +7 -0
  267. khoj/interface/compiled/khoj.webmanifest +76 -0
  268. khoj/interface/compiled/logo.svg +24 -0
  269. khoj/interface/compiled/search/index.html +1 -0
  270. khoj/interface/compiled/search/index.txt +7 -0
  271. khoj/interface/compiled/send.svg +1 -0
  272. khoj/interface/compiled/settings/index.html +1 -0
  273. khoj/interface/compiled/settings/index.txt +9 -0
  274. khoj/interface/compiled/share/chat/index.html +1 -0
  275. khoj/interface/compiled/share/chat/index.txt +7 -0
  276. khoj/interface/compiled/share.svg +8 -0
  277. khoj/interface/compiled/thumbs-down.svg +6 -0
  278. khoj/interface/compiled/thumbs-up.svg +6 -0
  279. khoj/interface/email/feedback.html +34 -0
  280. khoj/interface/email/magic_link.html +40 -0
  281. khoj/interface/email/task.html +37 -0
  282. khoj/interface/email/welcome.html +90 -0
  283. khoj/interface/web/.well-known/assetlinks.json +11 -0
  284. khoj/interface/web/assets/icons/agents.svg +19 -0
  285. khoj/interface/web/assets/icons/automation.svg +43 -0
  286. khoj/interface/web/assets/icons/chat.svg +24 -0
  287. khoj/interface/web/assets/icons/github.svg +1 -0
  288. khoj/interface/web/assets/icons/khoj-logo-sideways-200.png +0 -0
  289. khoj/interface/web/assets/icons/khoj-logo-sideways-500.png +0 -0
  290. khoj/interface/web/assets/icons/khoj-logo-sideways.svg +32 -0
  291. khoj/interface/web/assets/icons/khoj.svg +26 -0
  292. khoj/interface/web/assets/icons/logotype.svg +1 -0
  293. khoj/interface/web/assets/icons/search.svg +57 -0
  294. khoj/interface/web/assets/icons/sync.svg +4 -0
  295. khoj/interface/web/assets/khoj.css +237 -0
  296. khoj/interface/web/assets/utils.js +33 -0
  297. khoj/interface/web/base_config.html +445 -0
  298. khoj/interface/web/content_source_github_input.html +208 -0
  299. khoj/interface/web/login.html +310 -0
  300. khoj/interface/web/utils.html +48 -0
  301. khoj/main.py +249 -0
  302. khoj/manage.py +22 -0
  303. khoj/migrations/__init__.py +0 -0
  304. khoj/migrations/migrate_offline_chat_default_model.py +69 -0
  305. khoj/migrations/migrate_offline_chat_default_model_2.py +71 -0
  306. khoj/migrations/migrate_offline_chat_schema.py +83 -0
  307. khoj/migrations/migrate_offline_model.py +29 -0
  308. khoj/migrations/migrate_processor_config_openai.py +67 -0
  309. khoj/migrations/migrate_server_pg.py +132 -0
  310. khoj/migrations/migrate_version.py +17 -0
  311. khoj/processor/__init__.py +0 -0
  312. khoj/processor/content/__init__.py +0 -0
  313. khoj/processor/content/docx/__init__.py +0 -0
  314. khoj/processor/content/docx/docx_to_entries.py +111 -0
  315. khoj/processor/content/github/__init__.py +0 -0
  316. khoj/processor/content/github/github_to_entries.py +226 -0
  317. khoj/processor/content/images/__init__.py +0 -0
  318. khoj/processor/content/images/image_to_entries.py +117 -0
  319. khoj/processor/content/markdown/__init__.py +0 -0
  320. khoj/processor/content/markdown/markdown_to_entries.py +160 -0
  321. khoj/processor/content/notion/notion_to_entries.py +259 -0
  322. khoj/processor/content/org_mode/__init__.py +0 -0
  323. khoj/processor/content/org_mode/org_to_entries.py +226 -0
  324. khoj/processor/content/org_mode/orgnode.py +532 -0
  325. khoj/processor/content/pdf/__init__.py +0 -0
  326. khoj/processor/content/pdf/pdf_to_entries.py +119 -0
  327. khoj/processor/content/plaintext/__init__.py +0 -0
  328. khoj/processor/content/plaintext/plaintext_to_entries.py +117 -0
  329. khoj/processor/content/text_to_entries.py +296 -0
  330. khoj/processor/conversation/__init__.py +0 -0
  331. khoj/processor/conversation/anthropic/__init__.py +0 -0
  332. khoj/processor/conversation/anthropic/anthropic_chat.py +243 -0
  333. khoj/processor/conversation/anthropic/utils.py +217 -0
  334. khoj/processor/conversation/google/__init__.py +0 -0
  335. khoj/processor/conversation/google/gemini_chat.py +253 -0
  336. khoj/processor/conversation/google/utils.py +260 -0
  337. khoj/processor/conversation/offline/__init__.py +0 -0
  338. khoj/processor/conversation/offline/chat_model.py +308 -0
  339. khoj/processor/conversation/offline/utils.py +80 -0
  340. khoj/processor/conversation/offline/whisper.py +15 -0
  341. khoj/processor/conversation/openai/__init__.py +0 -0
  342. khoj/processor/conversation/openai/gpt.py +243 -0
  343. khoj/processor/conversation/openai/utils.py +232 -0
  344. khoj/processor/conversation/openai/whisper.py +13 -0
  345. khoj/processor/conversation/prompts.py +1188 -0
  346. khoj/processor/conversation/utils.py +867 -0
  347. khoj/processor/embeddings.py +122 -0
  348. khoj/processor/image/generate.py +215 -0
  349. khoj/processor/speech/__init__.py +0 -0
  350. khoj/processor/speech/text_to_speech.py +51 -0
  351. khoj/processor/tools/__init__.py +0 -0
  352. khoj/processor/tools/online_search.py +472 -0
  353. khoj/processor/tools/run_code.py +179 -0
  354. khoj/routers/__init__.py +0 -0
  355. khoj/routers/api.py +760 -0
  356. khoj/routers/api_agents.py +295 -0
  357. khoj/routers/api_chat.py +1273 -0
  358. khoj/routers/api_content.py +634 -0
  359. khoj/routers/api_model.py +123 -0
  360. khoj/routers/api_phone.py +86 -0
  361. khoj/routers/api_subscription.py +144 -0
  362. khoj/routers/auth.py +307 -0
  363. khoj/routers/email.py +135 -0
  364. khoj/routers/helpers.py +2333 -0
  365. khoj/routers/notion.py +85 -0
  366. khoj/routers/research.py +364 -0
  367. khoj/routers/storage.py +63 -0
  368. khoj/routers/twilio.py +36 -0
  369. khoj/routers/web_client.py +141 -0
  370. khoj/search_filter/__init__.py +0 -0
  371. khoj/search_filter/base_filter.py +15 -0
  372. khoj/search_filter/date_filter.py +215 -0
  373. khoj/search_filter/file_filter.py +32 -0
  374. khoj/search_filter/word_filter.py +29 -0
  375. khoj/search_type/__init__.py +0 -0
  376. khoj/search_type/text_search.py +255 -0
  377. khoj/utils/__init__.py +0 -0
  378. khoj/utils/cli.py +101 -0
  379. khoj/utils/config.py +81 -0
  380. khoj/utils/constants.py +51 -0
  381. khoj/utils/fs_syncer.py +252 -0
  382. khoj/utils/helpers.py +627 -0
  383. khoj/utils/initialization.py +301 -0
  384. khoj/utils/jsonl.py +43 -0
  385. khoj/utils/models.py +47 -0
  386. khoj/utils/rawconfig.py +208 -0
  387. khoj/utils/state.py +48 -0
  388. khoj/utils/yaml.py +47 -0
  389. khoj-1.33.3.dev32.dist-info/METADATA +190 -0
  390. khoj-1.33.3.dev32.dist-info/RECORD +393 -0
  391. khoj-1.33.3.dev32.dist-info/WHEEL +4 -0
  392. khoj-1.33.3.dev32.dist-info/entry_points.txt +2 -0
  393. khoj-1.33.3.dev32.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,1821 @@
1
+ import json
2
+ import logging
3
+ import math
4
+ import os
5
+ import random
6
+ import re
7
+ import secrets
8
+ import sys
9
+ from datetime import date, datetime, timedelta, timezone
10
+ from enum import Enum
11
+ from functools import wraps
12
+ from typing import (
13
+ Any,
14
+ Callable,
15
+ Coroutine,
16
+ Iterable,
17
+ List,
18
+ Optional,
19
+ ParamSpec,
20
+ TypeVar,
21
+ )
22
+
23
+ import cron_descriptor
24
+ from apscheduler.job import Job
25
+ from asgiref.sync import sync_to_async
26
+ from django.contrib.sessions.backends.db import SessionStore
27
+ from django.db.models import Prefetch, Q
28
+ from django.db.models.manager import BaseManager
29
+ from django.db.utils import IntegrityError
30
+ from django_apscheduler import util
31
+ from django_apscheduler.models import DjangoJob, DjangoJobExecution
32
+ from fastapi import HTTPException
33
+ from pgvector.django import CosineDistance
34
+ from torch import Tensor
35
+
36
+ from khoj.database.models import (
37
+ Agent,
38
+ AiModelApi,
39
+ ChatModel,
40
+ ClientApplication,
41
+ Conversation,
42
+ Entry,
43
+ FileObject,
44
+ GithubConfig,
45
+ GithubRepoConfig,
46
+ GoogleUser,
47
+ KhojApiUser,
48
+ KhojUser,
49
+ NotionConfig,
50
+ ProcessLock,
51
+ PublicConversation,
52
+ ReflectiveQuestion,
53
+ SearchModelConfig,
54
+ ServerChatSettings,
55
+ SpeechToTextModelOptions,
56
+ Subscription,
57
+ TextToImageModelConfig,
58
+ UserConversationConfig,
59
+ UserRequests,
60
+ UserTextToImageModelConfig,
61
+ UserVoiceModelConfig,
62
+ VoiceModelOption,
63
+ WebScraper,
64
+ )
65
+ from khoj.processor.conversation import prompts
66
+ from khoj.search_filter.date_filter import DateFilter
67
+ from khoj.search_filter.file_filter import FileFilter
68
+ from khoj.search_filter.word_filter import WordFilter
69
+ from khoj.utils import state
70
+ from khoj.utils.config import OfflineChatProcessorModel
71
+ from khoj.utils.helpers import (
72
+ generate_random_name,
73
+ in_debug_mode,
74
+ is_none_or_empty,
75
+ normalize_email,
76
+ timer,
77
+ )
78
+
79
+ logger = logging.getLogger(__name__)
80
+
81
+
82
+ LENGTH_OF_FREE_TRIAL = 7 #
83
+
84
+
85
+ class SubscriptionState(Enum):
86
+ TRIAL = "trial"
87
+ SUBSCRIBED = "subscribed"
88
+ UNSUBSCRIBED = "unsubscribed"
89
+ EXPIRED = "expired"
90
+ INVALID = "invalid"
91
+
92
+
93
+ P = ParamSpec("P")
94
+ T = TypeVar("T")
95
+
96
+
97
+ def require_valid_user(func: Callable[P, T]) -> Callable[P, T]:
98
+ @wraps(func)
99
+ def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
100
+ # Extract user from args/kwargs
101
+ user = next((arg for arg in args if isinstance(arg, KhojUser)), None)
102
+ if not user:
103
+ user = next((val for val in kwargs.values() if isinstance(val, KhojUser)), None)
104
+
105
+ # Throw error if user is not found
106
+ if not user:
107
+ raise ValueError("Khoj user argument required but not provided.")
108
+
109
+ return func(*args, **kwargs)
110
+
111
+ return sync_wrapper
112
+
113
+
114
+ def arequire_valid_user(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, Coroutine[Any, Any, T]]:
115
+ @wraps(func)
116
+ async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
117
+ # Extract user from args/kwargs
118
+ user = next((arg for arg in args if isinstance(arg, KhojUser)), None)
119
+ if not user:
120
+ user = next((v for v in kwargs.values() if isinstance(v, KhojUser)), None)
121
+
122
+ # Throw error if user is not found
123
+ if not user:
124
+ raise ValueError("Khoj user argument required but not provided.")
125
+
126
+ return await func(*args, **kwargs)
127
+
128
+ return async_wrapper
129
+
130
+
131
+ @arequire_valid_user
132
+ async def set_notion_config(token: str, user: KhojUser):
133
+ notion_config = await NotionConfig.objects.filter(user=user).afirst()
134
+ if not notion_config:
135
+ notion_config = await NotionConfig.objects.acreate(token=token, user=user)
136
+ else:
137
+ notion_config.token = token
138
+ await notion_config.asave()
139
+ return notion_config
140
+
141
+
142
+ @require_valid_user
143
+ def create_khoj_token(user: KhojUser, name=None):
144
+ "Create Khoj API key for user"
145
+ token = f"kk-{secrets.token_urlsafe(32)}"
146
+ name = name or f"{generate_random_name().title()}"
147
+ return KhojApiUser.objects.create(token=token, user=user, name=name)
148
+
149
+
150
+ @arequire_valid_user
151
+ async def acreate_khoj_token(user: KhojUser, name=None):
152
+ "Create Khoj API key for user"
153
+ token = f"kk-{secrets.token_urlsafe(32)}"
154
+ name = name or f"{generate_random_name().title()}"
155
+ return await KhojApiUser.objects.acreate(token=token, user=user, name=name)
156
+
157
+
158
+ @require_valid_user
159
+ def get_khoj_tokens(user: KhojUser):
160
+ "Get all Khoj API keys for user"
161
+ return list(KhojApiUser.objects.filter(user=user))
162
+
163
+
164
+ @arequire_valid_user
165
+ async def delete_khoj_token(user: KhojUser, token: str):
166
+ "Delete Khoj API Key for user"
167
+ await KhojApiUser.objects.filter(token=token, user=user).adelete()
168
+
169
+
170
+ async def get_or_create_user(token: dict) -> KhojUser:
171
+ user = await get_user_by_token(token)
172
+ if not user:
173
+ user = await create_user_by_google_token(token)
174
+ return user
175
+
176
+
177
+ async def aget_or_create_user_by_phone_number(phone_number: str) -> tuple[KhojUser, bool]:
178
+ is_new = False
179
+ if is_none_or_empty(phone_number):
180
+ return None, is_new
181
+ user = await aget_user_by_phone_number(phone_number)
182
+ if not user:
183
+ user = await acreate_user_by_phone_number(phone_number)
184
+ is_new = True
185
+ return user, is_new
186
+
187
+
188
+ @arequire_valid_user
189
+ async def aset_user_phone_number(user: KhojUser, phone_number: str) -> KhojUser:
190
+ if is_none_or_empty(phone_number):
191
+ return None
192
+ phone_number = phone_number.strip()
193
+ if not phone_number.startswith("+"):
194
+ phone_number = f"+{phone_number}"
195
+ existing_user_with_phone_number = await aget_user_by_phone_number(phone_number)
196
+ if existing_user_with_phone_number and existing_user_with_phone_number.id != user.id:
197
+ if is_none_or_empty(existing_user_with_phone_number.email):
198
+ # Transfer conversation history to the new user. If they don't have an associated email, they are effectively a new user
199
+ async for conversation in Conversation.objects.filter(user=existing_user_with_phone_number).aiterator():
200
+ conversation.user = user
201
+ await conversation.asave()
202
+
203
+ await existing_user_with_phone_number.adelete()
204
+ else:
205
+ raise HTTPException(status_code=400, detail="Phone number already exists")
206
+
207
+ user.phone_number = phone_number
208
+ await user.asave()
209
+ return user
210
+
211
+
212
+ @arequire_valid_user
213
+ async def aremove_phone_number(user: KhojUser) -> KhojUser:
214
+ user.phone_number = None
215
+ user.verified_phone_number = False
216
+ await user.asave()
217
+ return user
218
+
219
+
220
+ async def acreate_user_by_phone_number(phone_number: str) -> KhojUser:
221
+ if is_none_or_empty(phone_number):
222
+ return None
223
+ user, _ = await KhojUser.objects.filter(phone_number=phone_number).aupdate_or_create(
224
+ defaults={"username": phone_number, "phone_number": phone_number}
225
+ )
226
+ await user.asave()
227
+
228
+ user_subscription = await Subscription.objects.filter(user=user).afirst()
229
+ if not user_subscription:
230
+ await Subscription.objects.acreate(user=user, type=Subscription.Type.STANDARD)
231
+
232
+ return user
233
+
234
+
235
+ async def aget_or_create_user_by_email(input_email: str) -> tuple[KhojUser, bool]:
236
+ email, is_valid_email = normalize_email(input_email)
237
+ is_existing_user = await KhojUser.objects.filter(email=email).aexists()
238
+ # Validate email address of new users
239
+ if not is_existing_user and not is_valid_email:
240
+ logger.error(f"Account creation failed. Invalid email address: {email}")
241
+ return None, False
242
+
243
+ user, is_new = await KhojUser.objects.filter(email=email).aupdate_or_create(
244
+ defaults={"username": email, "email": email}
245
+ )
246
+
247
+ # Generate a secure 6-digit numeric code
248
+ user.email_verification_code = f"{secrets.randbelow(1000000):06}"
249
+ user.email_verification_code_expiry = datetime.now(tz=timezone.utc) + timedelta(minutes=5)
250
+ await user.asave()
251
+
252
+ user_subscription = await Subscription.objects.filter(user=user).afirst()
253
+ if not user_subscription:
254
+ await Subscription.objects.acreate(user=user, type=Subscription.Type.STANDARD)
255
+
256
+ return user, is_new
257
+
258
+
259
+ @arequire_valid_user
260
+ async def astart_trial_subscription(user: KhojUser) -> Subscription:
261
+ subscription = await Subscription.objects.filter(user=user).afirst()
262
+ if not subscription:
263
+ raise HTTPException(status_code=400, detail="User does not have a subscription")
264
+
265
+ if subscription.type == Subscription.Type.TRIAL:
266
+ raise HTTPException(status_code=400, detail="User already has a trial subscription")
267
+
268
+ if subscription.enabled_trial_at:
269
+ raise HTTPException(status_code=400, detail="User already has a trial subscription")
270
+
271
+ subscription.type = Subscription.Type.TRIAL
272
+ subscription.enabled_trial_at = datetime.now(tz=timezone.utc)
273
+ subscription.renewal_date = datetime.now(tz=timezone.utc) + timedelta(days=LENGTH_OF_FREE_TRIAL)
274
+ await subscription.asave()
275
+ return subscription
276
+
277
+
278
+ async def aget_user_validated_by_email_verification_code(code: str, email: str) -> tuple[Optional[KhojUser], bool]:
279
+ # Normalize the email address
280
+ normalized_email, _ = normalize_email(email)
281
+
282
+ # Check if verification code exists for the user
283
+ user = await KhojUser.objects.filter(email_verification_code=code, email=normalized_email).afirst()
284
+ if not user:
285
+ return None, False
286
+
287
+ # Check if the code has expired
288
+ if user.email_verification_code_expiry < datetime.now(tz=timezone.utc):
289
+ return user, True
290
+
291
+ user.email_verification_code = None
292
+ user.verified_email = True
293
+ await user.asave()
294
+
295
+ return user, False
296
+
297
+
298
+ async def create_user_by_google_token(token: dict) -> KhojUser:
299
+ user, _ = await KhojUser.objects.filter(email=token.get("email")).aupdate_or_create(
300
+ defaults={"username": token.get("email"), "email": token.get("email")}
301
+ )
302
+ user.verified_email = True
303
+ await user.asave()
304
+
305
+ await GoogleUser.objects.acreate(
306
+ sub=token.get("sub"),
307
+ azp=token.get("azp"),
308
+ email=token.get("email"),
309
+ name=token.get("name"),
310
+ given_name=token.get("given_name"),
311
+ family_name=token.get("family_name"),
312
+ picture=token.get("picture"),
313
+ locale=token.get("locale"),
314
+ user=user,
315
+ )
316
+
317
+ user_subscription = await Subscription.objects.filter(user=user).afirst()
318
+ if not user_subscription:
319
+ await Subscription.objects.acreate(user=user, type=Subscription.Type.STANDARD)
320
+
321
+ return user
322
+
323
+
324
+ @require_valid_user
325
+ def set_user_name(user: KhojUser, first_name: str, last_name: str) -> KhojUser:
326
+ user.first_name = first_name
327
+ user.last_name = last_name
328
+ user.save()
329
+ return user
330
+
331
+
332
+ @require_valid_user
333
+ def get_user_name(user: KhojUser):
334
+ full_name = user.get_full_name()
335
+ if not is_none_or_empty(full_name):
336
+ return full_name
337
+ google_profile: GoogleUser = GoogleUser.objects.filter(user=user).first()
338
+ if google_profile:
339
+ return google_profile.given_name
340
+
341
+ return None
342
+
343
+
344
+ @require_valid_user
345
+ def get_user_photo(user: KhojUser):
346
+ google_profile: GoogleUser = GoogleUser.objects.filter(user=user).first()
347
+ if google_profile:
348
+ return google_profile.picture
349
+
350
+ return None
351
+
352
+
353
+ def get_user_subscription(email: str) -> Optional[Subscription]:
354
+ return Subscription.objects.filter(user__email=email).first()
355
+
356
+
357
+ async def set_user_subscription(
358
+ email: str, is_recurring=None, renewal_date=None, type="standard"
359
+ ) -> tuple[Optional[Subscription], bool]:
360
+ # Get or create the user object and their subscription
361
+ user, is_new = await aget_or_create_user_by_email(email)
362
+ if not user:
363
+ return None, is_new
364
+ user_subscription = await Subscription.objects.filter(user=user).afirst()
365
+
366
+ # Update the user subscription state
367
+ user_subscription.type = type
368
+ if is_recurring is not None:
369
+ user_subscription.is_recurring = is_recurring
370
+ if renewal_date is None:
371
+ user_subscription.renewal_date = None
372
+ elif renewal_date is not None:
373
+ user_subscription.renewal_date = renewal_date
374
+ await user_subscription.asave()
375
+ return user_subscription, is_new
376
+
377
+
378
+ def subscription_to_state(subscription: Subscription) -> str:
379
+ if not subscription:
380
+ return SubscriptionState.INVALID.value
381
+ elif subscription.type == Subscription.Type.TRIAL:
382
+ # Check if the trial has expired
383
+ if not subscription.renewal_date:
384
+ # If the renewal date is not set, set it to the current date + trial length and evaluate
385
+ subscription.renewal_date = subscription.created_at + timedelta(days=LENGTH_OF_FREE_TRIAL)
386
+ subscription.save()
387
+
388
+ if subscription.renewal_date and datetime.now(tz=timezone.utc) > subscription.renewal_date:
389
+ return SubscriptionState.EXPIRED.value
390
+ return SubscriptionState.TRIAL.value
391
+ elif subscription.is_recurring and subscription.renewal_date > datetime.now(tz=timezone.utc):
392
+ return SubscriptionState.SUBSCRIBED.value
393
+ elif not subscription.is_recurring and subscription.renewal_date is None:
394
+ return SubscriptionState.EXPIRED.value
395
+ elif not subscription.is_recurring and subscription.renewal_date > datetime.now(tz=timezone.utc):
396
+ return SubscriptionState.UNSUBSCRIBED.value
397
+ elif not subscription.is_recurring and subscription.renewal_date < datetime.now(tz=timezone.utc):
398
+ return SubscriptionState.EXPIRED.value
399
+ return SubscriptionState.INVALID.value
400
+
401
+
402
+ def get_user_subscription_state(email: str) -> str:
403
+ """Get subscription state of user
404
+ Valid state transitions: trial -> subscribed <-> unsubscribed OR expired
405
+ """
406
+ user_subscription = Subscription.objects.filter(user__email=email).first()
407
+ return subscription_to_state(user_subscription)
408
+
409
+
410
+ @arequire_valid_user
411
+ async def aget_user_subscription_state(user: KhojUser) -> str:
412
+ """Get subscription state of user
413
+ Valid state transitions: trial -> subscribed <-> unsubscribed OR expired
414
+ """
415
+ user_subscription = await Subscription.objects.filter(user=user).afirst()
416
+ return await sync_to_async(subscription_to_state)(user_subscription)
417
+
418
+
419
+ @arequire_valid_user
420
+ async def ais_user_subscribed(user: KhojUser) -> bool:
421
+ """
422
+ Get whether the user is subscribed
423
+ """
424
+ if not state.billing_enabled or state.anonymous_mode:
425
+ return True
426
+
427
+ subscription_state = await aget_user_subscription_state(user)
428
+ subscribed = (
429
+ subscription_state == SubscriptionState.SUBSCRIBED.value
430
+ or subscription_state == SubscriptionState.TRIAL.value
431
+ or subscription_state == SubscriptionState.UNSUBSCRIBED.value
432
+ )
433
+ return subscribed
434
+
435
+
436
+ @require_valid_user
437
+ def is_user_subscribed(user: KhojUser) -> bool:
438
+ """
439
+ Get whether the user is subscribed
440
+ """
441
+ if not state.billing_enabled or state.anonymous_mode:
442
+ return True
443
+
444
+ subscription_state = get_user_subscription_state(user.email)
445
+ subscribed = (
446
+ subscription_state == SubscriptionState.SUBSCRIBED.value
447
+ or subscription_state == SubscriptionState.TRIAL.value
448
+ or subscription_state == SubscriptionState.UNSUBSCRIBED.value
449
+ )
450
+ return subscribed
451
+
452
+
453
+ async def aget_user_by_email(email: str) -> KhojUser:
454
+ return await KhojUser.objects.filter(email=email).afirst()
455
+
456
+
457
+ def get_user_by_email(email: str) -> KhojUser:
458
+ return KhojUser.objects.filter(email=email).first()
459
+
460
+
461
+ async def aget_user_by_uuid(uuid: str) -> KhojUser:
462
+ return await KhojUser.objects.filter(uuid=uuid).afirst()
463
+
464
+
465
+ async def get_user_by_token(token: dict) -> KhojUser:
466
+ google_user = await GoogleUser.objects.filter(sub=token.get("sub")).select_related("user").afirst()
467
+ if not google_user:
468
+ return None
469
+ return google_user.user
470
+
471
+
472
+ async def aget_user_by_phone_number(phone_number: str) -> KhojUser:
473
+ if is_none_or_empty(phone_number):
474
+ return None
475
+ matched_user = await KhojUser.objects.filter(phone_number=phone_number).prefetch_related("subscription").afirst()
476
+
477
+ if not matched_user:
478
+ return None
479
+
480
+ # If the user with this phone number does not have an email account with Khoj, return the user
481
+ if is_none_or_empty(matched_user.email):
482
+ return matched_user
483
+
484
+ # If the user has an email account with Khoj and a verified number, return the user
485
+ if matched_user.verified_phone_number:
486
+ return matched_user
487
+
488
+ return None
489
+
490
+
491
+ async def retrieve_user(session_id: str) -> KhojUser:
492
+ session = SessionStore(session_key=session_id)
493
+ if not await sync_to_async(session.exists)(session_key=session_id):
494
+ raise HTTPException(status_code=401, detail="Invalid session")
495
+ session_data = await sync_to_async(session.load)()
496
+ user = await KhojUser.objects.filter(id=session_data.get("_auth_user_id")).afirst()
497
+ if not user:
498
+ raise HTTPException(status_code=401, detail="Invalid user")
499
+ return user
500
+
501
+
502
+ def get_all_users() -> BaseManager[KhojUser]:
503
+ return KhojUser.objects.all()
504
+
505
+
506
+ @require_valid_user
507
+ def get_user_github_config(user: KhojUser):
508
+ config = GithubConfig.objects.filter(user=user).prefetch_related("githubrepoconfig").first()
509
+ return config
510
+
511
+
512
+ @require_valid_user
513
+ def get_user_notion_config(user: KhojUser):
514
+ config = NotionConfig.objects.filter(user=user).first()
515
+ return config
516
+
517
+
518
+ def delete_user_requests(window: timedelta = timedelta(days=1)):
519
+ return UserRequests.objects.filter(created_at__lte=datetime.now(tz=timezone.utc) - window).delete()
520
+
521
+
522
+ @arequire_valid_user
523
+ async def aget_user_name(user: KhojUser):
524
+ full_name = user.get_full_name()
525
+ if not is_none_or_empty(full_name):
526
+ return full_name
527
+ google_profile: GoogleUser = await GoogleUser.objects.filter(user=user).afirst()
528
+ if google_profile:
529
+ return google_profile.given_name
530
+
531
+ return None
532
+
533
+
534
+ @arequire_valid_user
535
+ async def set_user_github_config(user: KhojUser, pat_token: str, repos: list):
536
+ config = await GithubConfig.objects.filter(user=user).afirst()
537
+
538
+ if not config:
539
+ config = await GithubConfig.objects.acreate(pat_token=pat_token, user=user)
540
+ else:
541
+ config.pat_token = pat_token
542
+ await config.asave()
543
+ await config.githubrepoconfig.all().adelete()
544
+
545
+ for repo in repos:
546
+ await GithubRepoConfig.objects.acreate(
547
+ name=repo["name"], owner=repo["owner"], branch=repo["branch"], github_config=config
548
+ )
549
+ return config
550
+
551
+
552
+ def get_default_search_model() -> SearchModelConfig:
553
+ default_search_model = SearchModelConfig.objects.filter(name="default").first()
554
+
555
+ if default_search_model:
556
+ return default_search_model
557
+ elif SearchModelConfig.objects.count() == 0:
558
+ SearchModelConfig.objects.create()
559
+ return SearchModelConfig.objects.first()
560
+
561
+
562
+ def get_or_create_search_models():
563
+ search_models = SearchModelConfig.objects.all()
564
+ if search_models.count() == 0:
565
+ SearchModelConfig.objects.create()
566
+ search_models = SearchModelConfig.objects.all()
567
+
568
+ return search_models
569
+
570
+
571
+ class ProcessLockAdapters:
572
+ @staticmethod
573
+ def get_process_lock(process_name: str):
574
+ return ProcessLock.objects.filter(name=process_name).first()
575
+
576
+ @staticmethod
577
+ def set_process_lock(process_name: str, max_duration_in_seconds: int = 600):
578
+ return ProcessLock.objects.create(name=process_name, max_duration_in_seconds=max_duration_in_seconds)
579
+
580
+ @staticmethod
581
+ def is_process_locked_by_name(process_name: str):
582
+ process_lock = ProcessLock.objects.filter(name=process_name).first()
583
+ if not process_lock:
584
+ return False
585
+ return ProcessLockAdapters.is_process_locked(process_lock)
586
+
587
+ @staticmethod
588
+ def is_process_locked(process_lock: ProcessLock):
589
+ if process_lock.started_at + timedelta(seconds=process_lock.max_duration_in_seconds) < datetime.now(
590
+ tz=timezone.utc
591
+ ):
592
+ process_lock.delete()
593
+ logger.info(f"🔓 Deleted stale {process_lock.name} process lock on timeout")
594
+ return False
595
+ return True
596
+
597
+ @staticmethod
598
+ def remove_process_lock(process_lock: ProcessLock):
599
+ return process_lock.delete()
600
+
601
+ @staticmethod
602
+ def run_with_lock(func: Callable, operation: ProcessLock.Operation, max_duration_in_seconds: int = 600, **kwargs):
603
+ # Exit early if process lock is already taken
604
+ if ProcessLockAdapters.is_process_locked_by_name(operation):
605
+ logger.debug(f"🔒 Skip executing {func} as {operation} lock is already taken")
606
+ return
607
+
608
+ success = False
609
+ process_lock = None
610
+ try:
611
+ # Set process lock
612
+ process_lock = ProcessLockAdapters.set_process_lock(operation, max_duration_in_seconds)
613
+ logger.info(f"🔐 Locked {operation} to execute {func}")
614
+
615
+ # Execute Function
616
+ with timer(f"🔒 Run {func} with {operation} process lock", logger):
617
+ func(**kwargs)
618
+ success = True
619
+ except IntegrityError as e:
620
+ logger.debug(f"⚠️ Unable to create the process lock for {func} with {operation}: {e}")
621
+ success = False
622
+ except Exception as e:
623
+ logger.error(f"🚨 Error executing {func} with {operation} process lock: {e}", exc_info=True)
624
+ success = False
625
+ finally:
626
+ # Remove Process Lock
627
+ if process_lock:
628
+ ProcessLockAdapters.remove_process_lock(process_lock)
629
+ logger.info(
630
+ f"🔓 Unlocked {operation} process after executing {func} {'Succeeded' if success else 'Failed'}"
631
+ )
632
+ else:
633
+ logger.debug(f"Skip removing {operation} process lock as it was not set")
634
+
635
+
636
+ @util.close_old_connections
637
+ def run_with_process_lock(*args, **kwargs):
638
+ """Wrapper function used for scheduling jobs.
639
+ Required as APScheduler can't discover the `ProcessLockAdapter.run_with_lock' method on its own.
640
+ """
641
+ return ProcessLockAdapters.run_with_lock(*args, **kwargs)
642
+
643
+
644
+ class ClientApplicationAdapters:
645
+ @staticmethod
646
+ async def aget_client_application_by_id(client_id: str, client_secret: str):
647
+ return await ClientApplication.objects.filter(client_id=client_id, client_secret=client_secret).afirst()
648
+
649
+
650
+ class AgentAdapters:
651
+ DEFAULT_AGENT_NAME = "Khoj"
652
+ DEFAULT_AGENT_SLUG = "khoj"
653
+
654
+ @staticmethod
655
+ async def aget_readonly_agent_by_slug(agent_slug: str, user: KhojUser):
656
+ return (
657
+ await Agent.objects.filter(
658
+ (Q(slug__iexact=agent_slug.lower()))
659
+ & (
660
+ Q(privacy_level=Agent.PrivacyLevel.PUBLIC)
661
+ | Q(privacy_level=Agent.PrivacyLevel.PROTECTED)
662
+ | Q(creator=user)
663
+ )
664
+ )
665
+ .prefetch_related("creator", "chat_model", "fileobject_set")
666
+ .afirst()
667
+ )
668
+
669
+ @staticmethod
670
+ @arequire_valid_user
671
+ async def adelete_agent_by_slug(agent_slug: str, user: KhojUser):
672
+ agent = await AgentAdapters.aget_agent_by_slug(agent_slug, user)
673
+ if agent.creator != user:
674
+ return False
675
+
676
+ async for entry in Entry.objects.filter(agent=agent).aiterator():
677
+ await entry.adelete()
678
+
679
+ if agent:
680
+ await agent.adelete()
681
+ return True
682
+ return False
683
+
684
+ @staticmethod
685
+ async def aget_agent_by_slug(agent_slug: str, user: KhojUser):
686
+ return (
687
+ await Agent.objects.filter(
688
+ (Q(slug__iexact=agent_slug.lower())) & (Q(privacy_level=Agent.PrivacyLevel.PUBLIC) | Q(creator=user))
689
+ )
690
+ .prefetch_related("creator", "chat_model", "fileobject_set")
691
+ .afirst()
692
+ )
693
+
694
+ @staticmethod
695
+ async def aget_agent_by_name(agent_name: str, user: KhojUser):
696
+ return (
697
+ await Agent.objects.filter(
698
+ (Q(name__iexact=agent_name.lower())) & (Q(privacy_level=Agent.PrivacyLevel.PUBLIC) | Q(creator=user))
699
+ )
700
+ .prefetch_related("creator", "chat_model", "fileobject_set")
701
+ .afirst()
702
+ )
703
+
704
+ @staticmethod
705
+ def get_agent_by_slug(slug: str, user: KhojUser = None):
706
+ if user:
707
+ return Agent.objects.filter(
708
+ (Q(slug__iexact=slug.lower())) & (Q(privacy_level=Agent.PrivacyLevel.PUBLIC) | Q(creator=user))
709
+ ).first()
710
+ return Agent.objects.filter(slug__iexact=slug.lower(), privacy_level=Agent.PrivacyLevel.PUBLIC).first()
711
+
712
+ @staticmethod
713
+ def get_all_accessible_agents(user: KhojUser = None):
714
+ public_query = Q(privacy_level=Agent.PrivacyLevel.PUBLIC)
715
+ # TODO Update this to allow any public agent that's officially approved once that experience is launched
716
+ public_query &= Q(managed_by_admin=True)
717
+ if user:
718
+ return (
719
+ Agent.objects.filter(public_query | Q(creator=user))
720
+ .distinct()
721
+ .order_by("created_at")
722
+ .prefetch_related("creator", "chat_model", "fileobject_set")
723
+ )
724
+ return (
725
+ Agent.objects.filter(public_query)
726
+ .order_by("created_at")
727
+ .prefetch_related("creator", "chat_model", "fileobject_set")
728
+ )
729
+
730
+ @staticmethod
731
+ async def aget_all_accessible_agents(user: KhojUser = None) -> List[Agent]:
732
+ agents = await sync_to_async(AgentAdapters.get_all_accessible_agents)(user)
733
+ return await sync_to_async(list)(agents)
734
+
735
+ @staticmethod
736
+ async def ais_agent_accessible(agent: Agent, user: KhojUser) -> bool:
737
+ agent = await Agent.objects.select_related("creator").aget(pk=agent.pk)
738
+
739
+ if agent.privacy_level == Agent.PrivacyLevel.PUBLIC:
740
+ return True
741
+ if agent.creator == user:
742
+ return True
743
+ if agent.privacy_level == Agent.PrivacyLevel.PROTECTED:
744
+ return True
745
+ return False
746
+
747
+ @staticmethod
748
+ def get_conversation_agent_by_id(agent_id: int):
749
+ agent = Agent.objects.filter(id=agent_id).first()
750
+ if agent == AgentAdapters.get_default_agent():
751
+ # If the agent is set to the default agent, then return None and let the default application code be used
752
+ return None
753
+ return agent
754
+
755
+ @staticmethod
756
+ def get_default_agent():
757
+ return Agent.objects.filter(name=AgentAdapters.DEFAULT_AGENT_NAME).first()
758
+
759
+ @staticmethod
760
+ def create_default_agent(user: KhojUser):
761
+ default_chat_model = ConversationAdapters.get_default_chat_model(user)
762
+ if default_chat_model is None:
763
+ logger.info("No default conversation config found, skipping default agent creation")
764
+ return None
765
+ default_personality = prompts.personality.format(current_date="placeholder", day_of_week="placeholder")
766
+
767
+ agent = Agent.objects.filter(name=AgentAdapters.DEFAULT_AGENT_NAME).first()
768
+
769
+ if agent:
770
+ agent.personality = default_personality
771
+ agent.chat_model = default_chat_model
772
+ agent.slug = AgentAdapters.DEFAULT_AGENT_SLUG
773
+ agent.name = AgentAdapters.DEFAULT_AGENT_NAME
774
+ agent.privacy_level = Agent.PrivacyLevel.PUBLIC
775
+ agent.managed_by_admin = True
776
+ agent.input_tools = []
777
+ agent.output_modes = []
778
+ agent.save()
779
+ else:
780
+ # The default agent is public and managed by the admin. It's handled a little differently than other agents.
781
+ agent = Agent.objects.create(
782
+ name=AgentAdapters.DEFAULT_AGENT_NAME,
783
+ privacy_level=Agent.PrivacyLevel.PUBLIC,
784
+ managed_by_admin=True,
785
+ chat_model=default_chat_model,
786
+ personality=default_personality,
787
+ slug=AgentAdapters.DEFAULT_AGENT_SLUG,
788
+ )
789
+ Conversation.objects.filter(agent=None).update(agent=agent)
790
+
791
+ return agent
792
+
793
+ @staticmethod
794
+ async def aget_default_agent():
795
+ return await Agent.objects.filter(name=AgentAdapters.DEFAULT_AGENT_NAME).afirst()
796
+
797
+ @staticmethod
798
+ @arequire_valid_user
799
+ async def aupdate_agent(
800
+ user: KhojUser,
801
+ name: str,
802
+ personality: str,
803
+ privacy_level: str,
804
+ icon: str,
805
+ color: str,
806
+ chat_model: str,
807
+ files: List[str],
808
+ input_tools: List[str],
809
+ output_modes: List[str],
810
+ slug: Optional[str] = None,
811
+ ):
812
+ chat_model_option = await ChatModel.objects.filter(name=chat_model).afirst()
813
+
814
+ # Slug will be None for new agents, which will trigger a new agent creation with a generated, immutable slug
815
+ agent, created = await Agent.objects.filter(slug=slug, creator=user).aupdate_or_create(
816
+ defaults={
817
+ "name": name,
818
+ "creator": user,
819
+ "personality": personality,
820
+ "privacy_level": privacy_level,
821
+ "style_icon": icon,
822
+ "style_color": color,
823
+ "chat_model": chat_model_option,
824
+ "input_tools": input_tools,
825
+ "output_modes": output_modes,
826
+ }
827
+ )
828
+
829
+ # Delete all existing files and entries
830
+ await FileObject.objects.filter(agent=agent).adelete()
831
+ await Entry.objects.filter(agent=agent).adelete()
832
+
833
+ for file in files:
834
+ reference_file = await FileObject.objects.filter(file_name=file, user=agent.creator).afirst()
835
+ if reference_file:
836
+ await FileObject.objects.acreate(file_name=file, agent=agent, raw_text=reference_file.raw_text)
837
+
838
+ # Duplicate all entries associated with the file
839
+ entries: List[Entry] = []
840
+ async for entry in Entry.objects.filter(file_path=file, user=agent.creator).aiterator():
841
+ entries.append(
842
+ Entry(
843
+ agent=agent,
844
+ embeddings=entry.embeddings,
845
+ raw=entry.raw,
846
+ compiled=entry.compiled,
847
+ heading=entry.heading,
848
+ file_source=entry.file_source,
849
+ file_type=entry.file_type,
850
+ file_path=entry.file_path,
851
+ file_name=entry.file_name,
852
+ url=entry.url,
853
+ hashed_value=entry.hashed_value,
854
+ )
855
+ )
856
+
857
+ # Bulk create entries
858
+ await Entry.objects.abulk_create(entries)
859
+
860
+ return agent
861
+
862
+
863
+ class PublicConversationAdapters:
864
+ @staticmethod
865
+ def get_public_conversation_by_slug(slug: str):
866
+ return PublicConversation.objects.filter(slug=slug).first()
867
+
868
+ @staticmethod
869
+ def get_public_conversation_url(public_conversation: PublicConversation):
870
+ # Public conversations are viewable by anyone, but not editable.
871
+ return f"/share/chat/{public_conversation.slug}/"
872
+
873
+
874
+ class ConversationAdapters:
875
+ @staticmethod
876
+ def make_public_conversation_copy(conversation: Conversation):
877
+ return PublicConversation.objects.create(
878
+ source_owner=conversation.user,
879
+ agent=conversation.agent,
880
+ conversation_log=conversation.conversation_log,
881
+ slug=conversation.slug,
882
+ title=conversation.title if conversation.title else conversation.slug,
883
+ )
884
+
885
+ @staticmethod
886
+ @require_valid_user
887
+ def get_conversation_by_user(
888
+ user: KhojUser, client_application: ClientApplication = None, conversation_id: str = None
889
+ ) -> Optional[Conversation]:
890
+ if conversation_id:
891
+ conversation = (
892
+ Conversation.objects.filter(user=user, client=client_application, id=conversation_id)
893
+ .order_by("-updated_at")
894
+ .first()
895
+ )
896
+ else:
897
+ agent = AgentAdapters.get_default_agent()
898
+ conversation = (
899
+ Conversation.objects.filter(user=user, client=client_application).order_by("-updated_at").first()
900
+ ) or Conversation.objects.create(user=user, client=client_application, agent=agent)
901
+
902
+ return conversation
903
+
904
+ @staticmethod
905
+ @require_valid_user
906
+ def get_conversation_sessions(user: KhojUser, client_application: ClientApplication = None):
907
+ return (
908
+ Conversation.objects.filter(user=user, client=client_application)
909
+ .prefetch_related("agent")
910
+ .order_by("-updated_at")
911
+ )
912
+
913
+ @staticmethod
914
+ @arequire_valid_user
915
+ async def aset_conversation_title(
916
+ user: KhojUser, client_application: ClientApplication, conversation_id: str, title: str
917
+ ):
918
+ conversation = await Conversation.objects.filter(
919
+ user=user, client=client_application, id=conversation_id
920
+ ).afirst()
921
+ if conversation:
922
+ conversation.title = title
923
+ await conversation.asave()
924
+ return conversation
925
+ return None
926
+
927
+ @staticmethod
928
+ def get_conversation_by_id(conversation_id: str):
929
+ return Conversation.objects.filter(id=conversation_id).first()
930
+
931
+ @staticmethod
932
+ @arequire_valid_user
933
+ async def acreate_conversation_session(
934
+ user: KhojUser, client_application: ClientApplication = None, agent_slug: str = None, title: str = None
935
+ ):
936
+ if agent_slug:
937
+ agent = await AgentAdapters.aget_readonly_agent_by_slug(agent_slug, user)
938
+ if agent is None:
939
+ raise HTTPException(status_code=400, detail="No such agent currently exists.")
940
+ return await Conversation.objects.select_related("agent", "agent__creator", "agent__chat_model").acreate(
941
+ user=user, client=client_application, agent=agent, title=title
942
+ )
943
+ agent = await AgentAdapters.aget_default_agent()
944
+ return await Conversation.objects.select_related("agent", "agent__creator", "agent__chat_model").acreate(
945
+ user=user, client=client_application, agent=agent, title=title
946
+ )
947
+
948
+ @staticmethod
949
+ @require_valid_user
950
+ def create_conversation_session(
951
+ user: KhojUser, client_application: ClientApplication = None, agent_slug: str = None, title: str = None
952
+ ):
953
+ if agent_slug:
954
+ agent = AgentAdapters.aget_readonly_agent_by_slug(agent_slug, user)
955
+ if agent is None:
956
+ raise HTTPException(status_code=400, detail="No such agent currently exists.")
957
+ return Conversation.objects.create(user=user, client=client_application, agent=agent, title=title)
958
+ agent = AgentAdapters.get_default_agent()
959
+ return Conversation.objects.create(user=user, client=client_application, agent=agent, title=title)
960
+
961
+ @staticmethod
962
+ @arequire_valid_user
963
+ async def aget_conversation_by_user(
964
+ user: KhojUser,
965
+ client_application: ClientApplication = None,
966
+ conversation_id: str = None,
967
+ title: str = None,
968
+ create_new: bool = False,
969
+ ) -> Optional[Conversation]:
970
+ if create_new:
971
+ return await ConversationAdapters.acreate_conversation_session(user, client_application)
972
+
973
+ query = Conversation.objects.filter(user=user, client=client_application).prefetch_related("agent")
974
+
975
+ if conversation_id:
976
+ return await query.filter(id=conversation_id).afirst()
977
+ elif title:
978
+ return await query.filter(title=title).afirst()
979
+
980
+ conversation = await query.order_by("-updated_at").afirst()
981
+
982
+ return conversation or await Conversation.objects.prefetch_related("agent").acreate(
983
+ user=user, client=client_application
984
+ )
985
+
986
+ @staticmethod
987
+ @arequire_valid_user
988
+ async def adelete_conversation_by_user(
989
+ user: KhojUser, client_application: ClientApplication = None, conversation_id: str = None
990
+ ):
991
+ if conversation_id:
992
+ return await Conversation.objects.filter(user=user, client=client_application, id=conversation_id).adelete()
993
+ return await Conversation.objects.filter(user=user, client=client_application).adelete()
994
+
995
+ @staticmethod
996
+ @require_valid_user
997
+ def has_any_chat_model(user: KhojUser):
998
+ return ChatModel.objects.filter(user=user).exists()
999
+
1000
+ @staticmethod
1001
+ def get_all_chat_models():
1002
+ return ChatModel.objects.all()
1003
+
1004
+ @staticmethod
1005
+ async def aget_all_chat_models():
1006
+ return await sync_to_async(list)(ChatModel.objects.prefetch_related("ai_model_api").all())
1007
+
1008
+ @staticmethod
1009
+ def get_vision_enabled_config():
1010
+ chat_models = ConversationAdapters.get_all_chat_models()
1011
+ for config in chat_models:
1012
+ if config.vision_enabled:
1013
+ return config
1014
+ return None
1015
+
1016
+ @staticmethod
1017
+ async def aget_vision_enabled_config():
1018
+ chat_models = await ConversationAdapters.aget_all_chat_models()
1019
+ for config in chat_models:
1020
+ if config.vision_enabled:
1021
+ return config
1022
+ return None
1023
+
1024
+ @staticmethod
1025
+ def get_ai_model_api():
1026
+ return AiModelApi.objects.filter().first()
1027
+
1028
+ @staticmethod
1029
+ def has_valid_ai_model_api():
1030
+ return AiModelApi.objects.filter().exists()
1031
+
1032
+ @staticmethod
1033
+ @arequire_valid_user
1034
+ async def aset_user_conversation_processor(user: KhojUser, conversation_processor_config_id: int):
1035
+ config = await ChatModel.objects.filter(id=conversation_processor_config_id).afirst()
1036
+ if not config:
1037
+ return None
1038
+ new_config = await UserConversationConfig.objects.aupdate_or_create(user=user, defaults={"setting": config})
1039
+ return new_config
1040
+
1041
+ @staticmethod
1042
+ @arequire_valid_user
1043
+ async def aset_user_voice_model(user: KhojUser, model_id: str):
1044
+ config = await VoiceModelOption.objects.filter(model_id=model_id).afirst()
1045
+ if not config:
1046
+ return None
1047
+ new_config = await UserVoiceModelConfig.objects.aupdate_or_create(user=user, defaults={"setting": config})
1048
+ return new_config
1049
+
1050
+ @staticmethod
1051
+ def get_chat_model(user: KhojUser):
1052
+ subscribed = is_user_subscribed(user)
1053
+ if not subscribed:
1054
+ return ConversationAdapters.get_default_chat_model(user)
1055
+ config = UserConversationConfig.objects.filter(user=user).first()
1056
+ if config:
1057
+ return config.setting
1058
+ return ConversationAdapters.get_advanced_chat_model(user)
1059
+
1060
+ @staticmethod
1061
+ async def aget_chat_model(user: KhojUser):
1062
+ subscribed = await ais_user_subscribed(user)
1063
+ if not subscribed:
1064
+ return await ConversationAdapters.aget_default_chat_model(user)
1065
+ config = await UserConversationConfig.objects.filter(user=user).prefetch_related("setting").afirst()
1066
+ if config:
1067
+ return config.setting
1068
+ return ConversationAdapters.aget_advanced_chat_model(user)
1069
+
1070
+ @staticmethod
1071
+ async def aget_voice_model_config(user: KhojUser) -> Optional[VoiceModelOption]:
1072
+ voice_model_config = await UserVoiceModelConfig.objects.filter(user=user).prefetch_related("setting").afirst()
1073
+ if voice_model_config:
1074
+ return voice_model_config.setting
1075
+ return await VoiceModelOption.objects.afirst()
1076
+
1077
+ @staticmethod
1078
+ def get_voice_model_options():
1079
+ return VoiceModelOption.objects.all()
1080
+
1081
+ @staticmethod
1082
+ def get_voice_model_config(user: KhojUser) -> Optional[VoiceModelOption]:
1083
+ voice_model_config = UserVoiceModelConfig.objects.filter(user=user).prefetch_related("setting").first()
1084
+ if voice_model_config:
1085
+ return voice_model_config.setting
1086
+ return VoiceModelOption.objects.first()
1087
+
1088
+ @staticmethod
1089
+ def get_default_chat_model(user: KhojUser = None):
1090
+ """Get default conversation config. Prefer chat model by server admin > user > first created chat model"""
1091
+ # Get the server chat settings
1092
+ server_chat_settings = ServerChatSettings.objects.first()
1093
+
1094
+ is_subscribed = is_user_subscribed(user) if user else False
1095
+ if server_chat_settings:
1096
+ # If the user is subscribed and the advanced model is enabled, return the advanced model
1097
+ if is_subscribed and server_chat_settings.chat_advanced:
1098
+ return server_chat_settings.chat_advanced
1099
+ # If the default model is set, return it
1100
+ if server_chat_settings.chat_default:
1101
+ return server_chat_settings.chat_default
1102
+
1103
+ # Get the user's chat settings, if the server chat settings are not set
1104
+ user_chat_settings = UserConversationConfig.objects.filter(user=user).first() if user else None
1105
+ if user_chat_settings is not None and user_chat_settings.setting is not None:
1106
+ return user_chat_settings.setting
1107
+
1108
+ # Get the first chat model if even the user chat settings are not set
1109
+ return ChatModel.objects.filter().first()
1110
+
1111
+ @staticmethod
1112
+ async def aget_default_chat_model(user: KhojUser = None):
1113
+ """Get default conversation config. Prefer chat model by server admin > user > first created chat model"""
1114
+ # Get the server chat settings
1115
+ server_chat_settings: ServerChatSettings = (
1116
+ await ServerChatSettings.objects.filter()
1117
+ .prefetch_related(
1118
+ "chat_default", "chat_default__ai_model_api", "chat_advanced", "chat_advanced__ai_model_api"
1119
+ )
1120
+ .afirst()
1121
+ )
1122
+ is_subscribed = await ais_user_subscribed(user) if user else False
1123
+
1124
+ if server_chat_settings:
1125
+ # If the user is subscribed and the advanced model is enabled, return the advanced model
1126
+ if is_subscribed and server_chat_settings.chat_advanced:
1127
+ return server_chat_settings.chat_advanced
1128
+ # If the default model is set, return it
1129
+ if server_chat_settings.chat_default:
1130
+ return server_chat_settings.chat_default
1131
+
1132
+ # Get the user's chat settings, if the server chat settings are not set
1133
+ user_chat_settings = (
1134
+ (await UserConversationConfig.objects.filter(user=user).prefetch_related("setting__ai_model_api").afirst())
1135
+ if user
1136
+ else None
1137
+ )
1138
+ if user_chat_settings is not None and user_chat_settings.setting is not None:
1139
+ return user_chat_settings.setting
1140
+
1141
+ # Get the first chat model if even the user chat settings are not set
1142
+ return await ChatModel.objects.filter().prefetch_related("ai_model_api").afirst()
1143
+
1144
+ @staticmethod
1145
+ def get_advanced_chat_model(user: KhojUser):
1146
+ server_chat_settings = ServerChatSettings.objects.first()
1147
+ if server_chat_settings is not None and server_chat_settings.chat_advanced is not None:
1148
+ return server_chat_settings.chat_advanced
1149
+ return ConversationAdapters.get_default_chat_model(user)
1150
+
1151
+ @staticmethod
1152
+ async def aget_advanced_chat_model(user: KhojUser = None):
1153
+ server_chat_settings: ServerChatSettings = (
1154
+ await ServerChatSettings.objects.filter()
1155
+ .prefetch_related("chat_advanced", "chat_advanced__ai_model_api")
1156
+ .afirst()
1157
+ )
1158
+ if server_chat_settings is not None and server_chat_settings.chat_advanced is not None:
1159
+ return server_chat_settings.chat_advanced
1160
+ return await ConversationAdapters.aget_default_chat_model(user)
1161
+
1162
+ @staticmethod
1163
+ async def aget_server_webscraper():
1164
+ server_chat_settings = await ServerChatSettings.objects.filter().prefetch_related("web_scraper").afirst()
1165
+ if server_chat_settings is not None and server_chat_settings.web_scraper is not None:
1166
+ return server_chat_settings.web_scraper
1167
+ return None
1168
+
1169
+ @staticmethod
1170
+ async def aget_enabled_webscrapers() -> list[WebScraper]:
1171
+ enabled_scrapers: list[WebScraper] = []
1172
+ server_webscraper = await ConversationAdapters.aget_server_webscraper()
1173
+ if server_webscraper:
1174
+ # Only use the webscraper set in the server chat settings
1175
+ enabled_scrapers = [server_webscraper]
1176
+ if not enabled_scrapers:
1177
+ # Use the enabled web scrapers, ordered by priority, until get web page content
1178
+ enabled_scrapers = [scraper async for scraper in WebScraper.objects.all().order_by("priority").aiterator()]
1179
+ if not enabled_scrapers:
1180
+ # Use scrapers enabled via environment variables
1181
+ if os.getenv("FIRECRAWL_API_KEY"):
1182
+ api_url = os.getenv("FIRECRAWL_API_URL", "https://api.firecrawl.dev")
1183
+ enabled_scrapers.append(
1184
+ WebScraper(
1185
+ type=WebScraper.WebScraperType.FIRECRAWL,
1186
+ name=WebScraper.WebScraperType.FIRECRAWL.capitalize(),
1187
+ api_key=os.getenv("FIRECRAWL_API_KEY"),
1188
+ api_url=api_url,
1189
+ )
1190
+ )
1191
+ if os.getenv("OLOSTEP_API_KEY"):
1192
+ api_url = os.getenv("OLOSTEP_API_URL", "https://agent.olostep.com/olostep-p2p-incomingAPI")
1193
+ enabled_scrapers.append(
1194
+ WebScraper(
1195
+ type=WebScraper.WebScraperType.OLOSTEP,
1196
+ name=WebScraper.WebScraperType.OLOSTEP.capitalize(),
1197
+ api_key=os.getenv("OLOSTEP_API_KEY"),
1198
+ api_url=api_url,
1199
+ )
1200
+ )
1201
+ # Jina is the default fallback scrapers to use as it does not require an API key
1202
+ api_url = os.getenv("JINA_READER_API_URL", "https://r.jina.ai/")
1203
+ enabled_scrapers.append(
1204
+ WebScraper(
1205
+ type=WebScraper.WebScraperType.JINA,
1206
+ name=WebScraper.WebScraperType.JINA.capitalize(),
1207
+ api_key=os.getenv("JINA_API_KEY"),
1208
+ api_url=api_url,
1209
+ )
1210
+ )
1211
+
1212
+ # Only enable the direct web page scraper by default in self-hosted single user setups.
1213
+ # Useful for reading webpages on your intranet.
1214
+ if state.anonymous_mode or in_debug_mode():
1215
+ enabled_scrapers.append(
1216
+ WebScraper(
1217
+ type=WebScraper.WebScraperType.DIRECT,
1218
+ name=WebScraper.WebScraperType.DIRECT.capitalize(),
1219
+ api_key=None,
1220
+ api_url=None,
1221
+ )
1222
+ )
1223
+
1224
+ return enabled_scrapers
1225
+
1226
+ @staticmethod
1227
+ @require_valid_user
1228
+ def create_conversation_from_public_conversation(
1229
+ user: KhojUser, public_conversation: PublicConversation, client_app: ClientApplication
1230
+ ):
1231
+ scrubbed_title = public_conversation.title if public_conversation.title else public_conversation.slug
1232
+ if scrubbed_title:
1233
+ scrubbed_title = scrubbed_title.replace("-", " ")
1234
+ return Conversation.objects.create(
1235
+ user=user,
1236
+ conversation_log=public_conversation.conversation_log,
1237
+ client=client_app,
1238
+ slug=scrubbed_title,
1239
+ title=public_conversation.title,
1240
+ agent=public_conversation.agent,
1241
+ )
1242
+
1243
+ @staticmethod
1244
+ @require_valid_user
1245
+ def save_conversation(
1246
+ user: KhojUser,
1247
+ conversation_log: dict,
1248
+ client_application: ClientApplication = None,
1249
+ conversation_id: str = None,
1250
+ user_message: str = None,
1251
+ ):
1252
+ slug = user_message.strip()[:200] if user_message else None
1253
+ if conversation_id:
1254
+ conversation = Conversation.objects.filter(user=user, client=client_application, id=conversation_id).first()
1255
+ else:
1256
+ conversation = (
1257
+ Conversation.objects.filter(user=user, client=client_application).order_by("-updated_at").first()
1258
+ )
1259
+
1260
+ if conversation:
1261
+ conversation.conversation_log = conversation_log
1262
+ conversation.slug = slug
1263
+ conversation.updated_at = datetime.now(tz=timezone.utc)
1264
+ conversation.save()
1265
+ else:
1266
+ Conversation.objects.create(
1267
+ user=user, conversation_log=conversation_log, client=client_application, slug=slug
1268
+ )
1269
+
1270
+ @staticmethod
1271
+ def get_conversation_processor_options():
1272
+ return ChatModel.objects.all()
1273
+
1274
+ @staticmethod
1275
+ def set_user_chat_model(user: KhojUser, chat_model: ChatModel):
1276
+ user_conversation_config, _ = UserConversationConfig.objects.get_or_create(user=user)
1277
+ user_conversation_config.setting = chat_model
1278
+ user_conversation_config.save()
1279
+
1280
+ @staticmethod
1281
+ async def aget_user_chat_model(user: KhojUser):
1282
+ config = (
1283
+ await UserConversationConfig.objects.filter(user=user).prefetch_related("setting__ai_model_api").afirst()
1284
+ )
1285
+ if not config:
1286
+ return None
1287
+ return config.setting
1288
+
1289
+ @staticmethod
1290
+ async def get_speech_to_text_config():
1291
+ return await SpeechToTextModelOptions.objects.filter().afirst()
1292
+
1293
+ @staticmethod
1294
+ @arequire_valid_user
1295
+ async def aget_conversation_starters(user: KhojUser, max_results=3):
1296
+ all_questions = []
1297
+ if await ReflectiveQuestion.objects.filter(user=user).aexists():
1298
+ all_questions = await sync_to_async(ReflectiveQuestion.objects.filter(user=user).values_list)(
1299
+ "question", flat=True
1300
+ )
1301
+
1302
+ all_questions = await sync_to_async(ReflectiveQuestion.objects.filter(user=None).values_list)(
1303
+ "question", flat=True
1304
+ )
1305
+
1306
+ all_questions = await sync_to_async(list)(all_questions) # type: ignore
1307
+ if len(all_questions) < max_results:
1308
+ return all_questions
1309
+
1310
+ return random.sample(all_questions, max_results)
1311
+
1312
+ @staticmethod
1313
+ def get_valid_chat_model(user: KhojUser, conversation: Conversation):
1314
+ agent: Agent = conversation.agent if AgentAdapters.get_default_agent() != conversation.agent else None
1315
+ if agent and agent.chat_model:
1316
+ chat_model = conversation.agent.chat_model
1317
+ else:
1318
+ chat_model = ConversationAdapters.get_chat_model(user)
1319
+
1320
+ if chat_model is None:
1321
+ chat_model = ConversationAdapters.get_default_chat_model()
1322
+
1323
+ if chat_model.model_type == ChatModel.ModelType.OFFLINE:
1324
+ if state.offline_chat_processor_config is None or state.offline_chat_processor_config.loaded_model is None:
1325
+ chat_model_name = chat_model.name
1326
+ max_tokens = chat_model.max_prompt_size
1327
+ state.offline_chat_processor_config = OfflineChatProcessorModel(chat_model_name, max_tokens)
1328
+
1329
+ return chat_model
1330
+
1331
+ if (
1332
+ chat_model.model_type
1333
+ in [
1334
+ ChatModel.ModelType.ANTHROPIC,
1335
+ ChatModel.ModelType.OPENAI,
1336
+ ChatModel.ModelType.GOOGLE,
1337
+ ]
1338
+ ) and chat_model.ai_model_api:
1339
+ return chat_model
1340
+
1341
+ else:
1342
+ raise ValueError("Invalid conversation config - either configure offline chat or openai chat")
1343
+
1344
+ @staticmethod
1345
+ async def aget_text_to_image_model_config():
1346
+ return await TextToImageModelConfig.objects.filter().prefetch_related("ai_model_api").afirst()
1347
+
1348
+ @staticmethod
1349
+ def get_text_to_image_model_config():
1350
+ return TextToImageModelConfig.objects.filter().first()
1351
+
1352
+ @staticmethod
1353
+ def get_text_to_image_model_options():
1354
+ return TextToImageModelConfig.objects.all()
1355
+
1356
+ @staticmethod
1357
+ def get_user_text_to_image_model_config(user: KhojUser):
1358
+ config = UserTextToImageModelConfig.objects.filter(user=user).first()
1359
+ if not config:
1360
+ default_config = ConversationAdapters.get_text_to_image_model_config()
1361
+ if not default_config:
1362
+ return None
1363
+ return default_config
1364
+ return config.setting
1365
+
1366
+ @staticmethod
1367
+ async def aget_user_text_to_image_model(user: KhojUser) -> Optional[TextToImageModelConfig]:
1368
+ # Create a custom queryset for prefetching settings__ai_model_api, handling null cases
1369
+ settings_prefetch = Prefetch(
1370
+ "setting", queryset=TextToImageModelConfig.objects.prefetch_related("ai_model_api")
1371
+ )
1372
+
1373
+ config = await UserTextToImageModelConfig.objects.filter(user=user).prefetch_related(settings_prefetch).afirst()
1374
+ if not config:
1375
+ default_config = await ConversationAdapters.aget_text_to_image_model_config()
1376
+ if not default_config:
1377
+ return None
1378
+ return default_config
1379
+ return config.setting
1380
+
1381
+ @staticmethod
1382
+ async def aset_user_text_to_image_model(user: KhojUser, text_to_image_model_config_id: int):
1383
+ config = await TextToImageModelConfig.objects.filter(id=text_to_image_model_config_id).afirst()
1384
+ if not config:
1385
+ return None
1386
+ new_config, _ = await UserTextToImageModelConfig.objects.aupdate_or_create(
1387
+ user=user, defaults={"setting": config}
1388
+ )
1389
+ return new_config
1390
+
1391
+ @staticmethod
1392
+ def add_files_to_filter(user: KhojUser, conversation_id: str, files: List[str]):
1393
+ conversation = ConversationAdapters.get_conversation_by_user(user, conversation_id=conversation_id)
1394
+ file_list = EntryAdapters.get_all_filenames_by_source(user, "computer")
1395
+ if not conversation:
1396
+ return []
1397
+ for filename in files:
1398
+ if filename in file_list and filename not in conversation.file_filters:
1399
+ conversation.file_filters.append(filename)
1400
+ conversation.save()
1401
+
1402
+ # remove files from conversation.file_filters that are not in file_list
1403
+ conversation.file_filters = [file for file in conversation.file_filters if file in file_list]
1404
+ conversation.save()
1405
+ return conversation.file_filters
1406
+
1407
+ @staticmethod
1408
+ def remove_files_from_filter(user: KhojUser, conversation_id: str, files: List[str]):
1409
+ conversation = ConversationAdapters.get_conversation_by_user(user, conversation_id=conversation_id)
1410
+ if not conversation:
1411
+ return []
1412
+ for filename in files:
1413
+ if filename in conversation.file_filters:
1414
+ conversation.file_filters.remove(filename)
1415
+ conversation.save()
1416
+
1417
+ # remove files from conversation.file_filters that are not in file_list
1418
+ file_list = EntryAdapters.get_all_filenames_by_source(user, "computer")
1419
+ conversation.file_filters = [file for file in conversation.file_filters if file in file_list]
1420
+ conversation.save()
1421
+ return conversation.file_filters
1422
+
1423
+ @staticmethod
1424
+ @require_valid_user
1425
+ def delete_message_by_turn_id(user: KhojUser, conversation_id: str, turn_id: str):
1426
+ conversation = ConversationAdapters.get_conversation_by_user(user, conversation_id=conversation_id)
1427
+ if not conversation or not conversation.conversation_log or not conversation.conversation_log.get("chat"):
1428
+ return False
1429
+ conversation_log = conversation.conversation_log
1430
+ updated_log = [msg for msg in conversation_log["chat"] if msg.get("turnId") != turn_id]
1431
+ conversation.conversation_log["chat"] = updated_log
1432
+ conversation.save()
1433
+ return True
1434
+
1435
+
1436
+ class FileObjectAdapters:
1437
+ @staticmethod
1438
+ def update_raw_text(file_object: FileObject, new_raw_text: str):
1439
+ file_object.raw_text = new_raw_text
1440
+ file_object.save()
1441
+
1442
+ @staticmethod
1443
+ @require_valid_user
1444
+ def create_file_object(user: KhojUser, file_name: str, raw_text: str):
1445
+ return FileObject.objects.create(user=user, file_name=file_name, raw_text=raw_text)
1446
+
1447
+ @staticmethod
1448
+ @require_valid_user
1449
+ def get_file_object_by_name(user: KhojUser, file_name: str):
1450
+ return FileObject.objects.filter(user=user, file_name=file_name).first()
1451
+
1452
+ @staticmethod
1453
+ @require_valid_user
1454
+ def get_all_file_objects(user: KhojUser):
1455
+ return FileObject.objects.filter(user=user).all()
1456
+
1457
+ @staticmethod
1458
+ @require_valid_user
1459
+ def delete_file_object_by_name(user: KhojUser, file_name: str):
1460
+ return FileObject.objects.filter(user=user, file_name=file_name).delete()
1461
+
1462
+ @staticmethod
1463
+ @require_valid_user
1464
+ def delete_all_file_objects(user: KhojUser):
1465
+ return FileObject.objects.filter(user=user).delete()
1466
+
1467
+ @staticmethod
1468
+ async def aupdate_raw_text(file_object: FileObject, new_raw_text: str):
1469
+ file_object.raw_text = new_raw_text
1470
+ await file_object.asave()
1471
+
1472
+ @staticmethod
1473
+ @arequire_valid_user
1474
+ async def acreate_file_object(user: KhojUser, file_name: str, raw_text: str):
1475
+ return await FileObject.objects.acreate(user=user, file_name=file_name, raw_text=raw_text)
1476
+
1477
+ @staticmethod
1478
+ @arequire_valid_user
1479
+ async def aget_file_objects_by_name(user: KhojUser, file_name: str, agent: Agent = None):
1480
+ return await sync_to_async(list)(FileObject.objects.filter(user=user, file_name=file_name, agent=agent))
1481
+
1482
+ @staticmethod
1483
+ @arequire_valid_user
1484
+ async def aget_file_objects_by_names(user: KhojUser, file_names: List[str]):
1485
+ return await sync_to_async(list)(FileObject.objects.filter(user=user, file_name__in=file_names))
1486
+
1487
+ @staticmethod
1488
+ @arequire_valid_user
1489
+ async def aget_all_file_objects(user: KhojUser):
1490
+ return await sync_to_async(list)(FileObject.objects.filter(user=user))
1491
+
1492
+ @staticmethod
1493
+ @arequire_valid_user
1494
+ async def adelete_file_object_by_name(user: KhojUser, file_name: str):
1495
+ return await FileObject.objects.filter(user=user, file_name=file_name).adelete()
1496
+
1497
+ @staticmethod
1498
+ @arequire_valid_user
1499
+ async def adelete_all_file_objects(user: KhojUser):
1500
+ return await FileObject.objects.filter(user=user).adelete()
1501
+
1502
+
1503
+ class EntryAdapters:
1504
+ word_filter = WordFilter()
1505
+ file_filter = FileFilter()
1506
+ date_filter = DateFilter()
1507
+
1508
+ @staticmethod
1509
+ @require_valid_user
1510
+ def does_entry_exist(user: KhojUser, hashed_value: str) -> bool:
1511
+ return Entry.objects.filter(user=user, hashed_value=hashed_value).exists()
1512
+
1513
+ @staticmethod
1514
+ @require_valid_user
1515
+ def delete_entry_by_file(user: KhojUser, file_path: str):
1516
+ deleted_count, _ = Entry.objects.filter(user=user, file_path=file_path).delete()
1517
+ return deleted_count
1518
+
1519
+ @staticmethod
1520
+ @require_valid_user
1521
+ def get_filtered_entries(user: KhojUser, file_type: str = None, file_source: str = None):
1522
+ queryset = Entry.objects.filter(user=user)
1523
+
1524
+ if file_type is not None:
1525
+ queryset = queryset.filter(file_type=file_type)
1526
+
1527
+ if file_source is not None:
1528
+ queryset = queryset.filter(file_source=file_source)
1529
+
1530
+ return queryset
1531
+
1532
+ @staticmethod
1533
+ @require_valid_user
1534
+ def delete_all_entries(user: KhojUser, file_type: str = None, file_source: str = None, batch_size=1000):
1535
+ deleted_count = 0
1536
+ queryset = EntryAdapters.get_filtered_entries(user, file_type, file_source)
1537
+ while queryset.exists():
1538
+ batch_ids = list(queryset.values_list("id", flat=True)[:batch_size])
1539
+ batch = Entry.objects.filter(id__in=batch_ids, user=user)
1540
+ count, _ = batch.delete()
1541
+ deleted_count += count
1542
+ return deleted_count
1543
+
1544
+ @staticmethod
1545
+ @arequire_valid_user
1546
+ async def adelete_all_entries(user: KhojUser, file_type: str = None, file_source: str = None, batch_size=1000):
1547
+ deleted_count = 0
1548
+ queryset = EntryAdapters.get_filtered_entries(user, file_type, file_source)
1549
+ while await queryset.aexists():
1550
+ batch_ids = await sync_to_async(list)(queryset.values_list("id", flat=True)[:batch_size])
1551
+ batch = Entry.objects.filter(id__in=batch_ids, user=user)
1552
+ count, _ = await batch.adelete()
1553
+ deleted_count += count
1554
+ return deleted_count
1555
+
1556
+ @staticmethod
1557
+ @require_valid_user
1558
+ def get_existing_entry_hashes_by_file(user: KhojUser, file_path: str):
1559
+ return Entry.objects.filter(user=user, file_path=file_path).values_list("hashed_value", flat=True)
1560
+
1561
+ @staticmethod
1562
+ @require_valid_user
1563
+ def delete_entry_by_hash(user: KhojUser, hashed_values: List[str]):
1564
+ Entry.objects.filter(user=user, hashed_value__in=hashed_values).delete()
1565
+
1566
+ @staticmethod
1567
+ def get_entries_by_date_filter(entry: BaseManager[Entry], start_date: date, end_date: date):
1568
+ return entry.filter(
1569
+ entrydates__date__gte=start_date,
1570
+ entrydates__date__lte=end_date,
1571
+ )
1572
+
1573
+ @staticmethod
1574
+ @require_valid_user
1575
+ def user_has_entries(user: KhojUser):
1576
+ return Entry.objects.filter(user=user).exists()
1577
+
1578
+ @staticmethod
1579
+ def agent_has_entries(agent: Agent):
1580
+ return Entry.objects.filter(agent=agent).exists()
1581
+
1582
+ @staticmethod
1583
+ @arequire_valid_user
1584
+ async def auser_has_entries(user: KhojUser):
1585
+ return await Entry.objects.filter(user=user).aexists()
1586
+
1587
+ @staticmethod
1588
+ async def aagent_has_entries(agent: Agent):
1589
+ if agent is None:
1590
+ return False
1591
+ return await Entry.objects.filter(agent=agent).aexists()
1592
+
1593
+ @staticmethod
1594
+ @arequire_valid_user
1595
+ async def adelete_entry_by_file(user: KhojUser, file_path: str):
1596
+ return await Entry.objects.filter(user=user, file_path=file_path).adelete()
1597
+
1598
+ @staticmethod
1599
+ @arequire_valid_user
1600
+ async def adelete_entries_by_filenames(user: KhojUser, filenames: List[str], batch_size=1000):
1601
+ deleted_count = 0
1602
+ for i in range(0, len(filenames), batch_size):
1603
+ batch = filenames[i : i + batch_size]
1604
+ count, _ = await Entry.objects.filter(user=user, file_path__in=batch).adelete()
1605
+ deleted_count += count
1606
+
1607
+ return deleted_count
1608
+
1609
+ @staticmethod
1610
+ async def aget_agent_entry_filepaths(agent: Agent):
1611
+ if agent is None:
1612
+ return []
1613
+ return await sync_to_async(set)(
1614
+ Entry.objects.filter(agent=agent).distinct("file_path").values_list("file_path", flat=True)
1615
+ )
1616
+
1617
+ @staticmethod
1618
+ @require_valid_user
1619
+ def get_all_filenames_by_source(user: KhojUser, file_source: str):
1620
+ return (
1621
+ Entry.objects.filter(user=user, file_source=file_source)
1622
+ .distinct("file_path")
1623
+ .values_list("file_path", flat=True)
1624
+ )
1625
+
1626
+ @staticmethod
1627
+ @require_valid_user
1628
+ def get_size_of_indexed_data_in_mb(user: KhojUser):
1629
+ entries = Entry.objects.filter(user=user).iterator()
1630
+ total_size = sum(sys.getsizeof(entry.compiled) for entry in entries)
1631
+ return total_size / 1024 / 1024
1632
+
1633
+ @staticmethod
1634
+ def apply_filters(user: KhojUser, query: str, file_type_filter: str = None, agent: Agent = None):
1635
+ q_filter_terms = Q()
1636
+
1637
+ word_filters = EntryAdapters.word_filter.get_filter_terms(query)
1638
+ file_filters = EntryAdapters.file_filter.get_filter_terms(query)
1639
+ date_filters = EntryAdapters.date_filter.get_query_date_range(query)
1640
+
1641
+ owner_filter = Q()
1642
+
1643
+ if user != None:
1644
+ owner_filter = Q(user=user)
1645
+ if agent != None:
1646
+ owner_filter |= Q(agent=agent)
1647
+
1648
+ if owner_filter == Q():
1649
+ return Entry.objects.none()
1650
+
1651
+ if len(word_filters) == 0 and len(file_filters) == 0 and len(date_filters) == 0:
1652
+ return Entry.objects.filter(owner_filter)
1653
+
1654
+ for term in word_filters:
1655
+ if term.startswith("+"):
1656
+ q_filter_terms &= Q(raw__icontains=term[1:])
1657
+ elif term.startswith("-"):
1658
+ q_filter_terms &= ~Q(raw__icontains=term[1:])
1659
+
1660
+ q_file_filter_terms = Q()
1661
+
1662
+ if len(file_filters) > 0:
1663
+ for term in file_filters:
1664
+ if term.startswith("-"):
1665
+ # Convert the glob term to a regex pattern
1666
+ regex_term = re.escape(term[1:]).replace(r"\*", ".*").replace(r"\?", ".")
1667
+ # Exclude all files that match the regex term
1668
+ q_file_filter_terms &= ~Q(file_path__regex=regex_term)
1669
+ else:
1670
+ # Convert the glob term to a regex pattern
1671
+ regex_term = re.escape(term).replace(r"\*", ".*").replace(r"\?", ".")
1672
+ # Include any files that match the regex term
1673
+ q_file_filter_terms |= Q(file_path__regex=regex_term)
1674
+
1675
+ q_filter_terms &= q_file_filter_terms
1676
+
1677
+ if len(date_filters) > 0:
1678
+ min_date, max_date = date_filters
1679
+ if min_date is not None:
1680
+ # Convert the min_date timestamp to yyyy-mm-dd format
1681
+ formatted_min_date = date.fromtimestamp(min_date).strftime("%Y-%m-%d")
1682
+ q_filter_terms &= Q(embeddings_dates__date__gte=formatted_min_date)
1683
+ if max_date is not None:
1684
+ # Convert the max_date timestamp to yyyy-mm-dd format
1685
+ formatted_max_date = date.fromtimestamp(max_date).strftime("%Y-%m-%d")
1686
+ q_filter_terms &= Q(embeddings_dates__date__lte=formatted_max_date)
1687
+
1688
+ relevant_entries = Entry.objects.filter(owner_filter).filter(q_filter_terms)
1689
+ if file_type_filter:
1690
+ relevant_entries = relevant_entries.filter(file_type=file_type_filter)
1691
+ return relevant_entries
1692
+
1693
+ @staticmethod
1694
+ def search_with_embeddings(
1695
+ raw_query: str,
1696
+ embeddings: Tensor,
1697
+ user: KhojUser,
1698
+ max_results: int = 10,
1699
+ file_type_filter: str = None,
1700
+ max_distance: float = math.inf,
1701
+ agent: Agent = None,
1702
+ ):
1703
+ owner_filter = Q()
1704
+
1705
+ if user != None:
1706
+ owner_filter = Q(user=user)
1707
+ if agent != None:
1708
+ owner_filter |= Q(agent=agent)
1709
+
1710
+ if owner_filter == Q():
1711
+ return Entry.objects.none()
1712
+
1713
+ relevant_entries = EntryAdapters.apply_filters(user, raw_query, file_type_filter, agent)
1714
+ relevant_entries = relevant_entries.filter(owner_filter).annotate(
1715
+ distance=CosineDistance("embeddings", embeddings)
1716
+ )
1717
+ relevant_entries = relevant_entries.filter(distance__lte=max_distance)
1718
+
1719
+ if file_type_filter:
1720
+ relevant_entries = relevant_entries.filter(file_type=file_type_filter)
1721
+ relevant_entries = relevant_entries.order_by("distance")
1722
+ return relevant_entries[:max_results]
1723
+
1724
+ @staticmethod
1725
+ @require_valid_user
1726
+ def get_unique_file_types(user: KhojUser):
1727
+ return Entry.objects.filter(user=user).values_list("file_type", flat=True).distinct()
1728
+
1729
+ @staticmethod
1730
+ @require_valid_user
1731
+ def get_unique_file_sources(user: KhojUser):
1732
+ return Entry.objects.filter(user=user).values_list("file_source", flat=True).distinct().all()
1733
+
1734
+
1735
+ class AutomationAdapters:
1736
+ @staticmethod
1737
+ def get_automations(user: KhojUser) -> Iterable[Job]:
1738
+ all_automations: Iterable[Job] = state.scheduler.get_jobs()
1739
+ for automation in all_automations:
1740
+ if automation.id.startswith(f"automation_{user.uuid}_"):
1741
+ yield automation
1742
+
1743
+ @staticmethod
1744
+ def get_automation_metadata(user: KhojUser, automation: Job):
1745
+ # Perform validation checks
1746
+ # Check if user is allowed to delete this automation id
1747
+ if not automation.id.startswith(f"automation_{user.uuid}_"):
1748
+ raise ValueError("Invalid automation id")
1749
+
1750
+ automation_metadata = json.loads(automation.name)
1751
+ crontime = automation_metadata["crontime"]
1752
+ timezone = automation.next_run_time.strftime("%Z")
1753
+ schedule = f"{cron_descriptor.get_description(crontime)} {timezone}"
1754
+ return {
1755
+ "id": automation.id,
1756
+ "subject": automation_metadata["subject"],
1757
+ "query_to_run": automation_metadata["query_to_run"],
1758
+ "scheduling_request": automation_metadata["scheduling_request"],
1759
+ "schedule": schedule,
1760
+ "crontime": crontime,
1761
+ "next": automation.next_run_time.strftime("%Y-%m-%d %I:%M %p %Z"),
1762
+ }
1763
+
1764
+ @staticmethod
1765
+ def get_job_last_run(user: KhojUser, automation: Job):
1766
+ # Perform validation checks
1767
+ # Check if user is allowed to delete this automation id
1768
+ if not automation.id.startswith(f"automation_{user.uuid}_"):
1769
+ raise ValueError("Invalid automation id")
1770
+
1771
+ django_job = DjangoJob.objects.filter(id=automation.id).first()
1772
+ execution = DjangoJobExecution.objects.filter(job=django_job, status="Executed")
1773
+
1774
+ last_run_time = None
1775
+
1776
+ if execution.exists():
1777
+ last_run_time = execution.latest("run_time").run_time
1778
+
1779
+ return last_run_time.strftime("%Y-%m-%d %I:%M %p %Z") if last_run_time else None
1780
+
1781
+ @staticmethod
1782
+ def get_automations_metadata(user: KhojUser):
1783
+ for automation in AutomationAdapters.get_automations(user):
1784
+ yield AutomationAdapters.get_automation_metadata(user, automation)
1785
+
1786
+ @staticmethod
1787
+ def get_automation(user: KhojUser, automation_id: str) -> Job:
1788
+ # Perform validation checks
1789
+ # Check if user is allowed to delete this automation id
1790
+ if not automation_id.startswith(f"automation_{user.uuid}_"):
1791
+ raise ValueError("Invalid automation id")
1792
+ # Check if automation with this id exist
1793
+ automation: Job = state.scheduler.get_job(job_id=automation_id)
1794
+ if not automation:
1795
+ raise ValueError("Invalid automation id")
1796
+
1797
+ return automation
1798
+
1799
+ @staticmethod
1800
+ async def aget_automation(user: KhojUser, automation_id: str) -> Job:
1801
+ # Perform validation checks
1802
+ # Check if user is allowed to delete this automation id
1803
+ if not automation_id.startswith(f"automation_{user.uuid}_"):
1804
+ raise ValueError("Invalid automation id")
1805
+ # Check if automation with this id exist
1806
+ automation: Job = await sync_to_async(state.scheduler.get_job)(job_id=automation_id)
1807
+ if not automation:
1808
+ raise ValueError("Invalid automation id")
1809
+
1810
+ return automation
1811
+
1812
+ @staticmethod
1813
+ def delete_automation(user: KhojUser, automation_id: str):
1814
+ # Get valid, user-owned automation
1815
+ automation: Job = AutomationAdapters.get_automation(user, automation_id)
1816
+
1817
+ # Collate info about user automation to be deleted
1818
+ automation_metadata = AutomationAdapters.get_automation_metadata(user, automation)
1819
+
1820
+ automation.remove()
1821
+ return automation_metadata