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,1273 @@
1
+ import asyncio
2
+ import base64
3
+ import json
4
+ import logging
5
+ import time
6
+ import uuid
7
+ from datetime import datetime
8
+ from functools import partial
9
+ from typing import Any, Dict, List, Optional
10
+ from urllib.parse import unquote
11
+
12
+ from asgiref.sync import sync_to_async
13
+ from fastapi import APIRouter, Depends, HTTPException, Request
14
+ from fastapi.responses import Response, StreamingResponse
15
+ from starlette.authentication import requires
16
+
17
+ from khoj.app.settings import ALLOWED_HOSTS
18
+ from khoj.database.adapters import (
19
+ AgentAdapters,
20
+ ConversationAdapters,
21
+ EntryAdapters,
22
+ PublicConversationAdapters,
23
+ aget_user_name,
24
+ )
25
+ from khoj.database.models import Agent, KhojUser
26
+ from khoj.processor.conversation import prompts
27
+ from khoj.processor.conversation.prompts import help_message, no_entries_found
28
+ from khoj.processor.conversation.utils import defilter_query, save_to_conversation_log
29
+ from khoj.processor.image.generate import text_to_image
30
+ from khoj.processor.speech.text_to_speech import generate_text_to_speech
31
+ from khoj.processor.tools.online_search import (
32
+ deduplicate_organic_results,
33
+ read_webpages,
34
+ search_online,
35
+ )
36
+ from khoj.processor.tools.run_code import run_code
37
+ from khoj.routers.api import extract_references_and_questions
38
+ from khoj.routers.email import send_query_feedback
39
+ from khoj.routers.helpers import (
40
+ ApiImageRateLimiter,
41
+ ApiUserRateLimiter,
42
+ ChatEvent,
43
+ ChatRequestBody,
44
+ CommonQueryParams,
45
+ ConversationCommandRateLimiter,
46
+ DeleteMessageRequestBody,
47
+ FeedbackData,
48
+ acreate_title_from_history,
49
+ agenerate_chat_response,
50
+ aget_data_sources_and_output_format,
51
+ construct_automation_created_message,
52
+ create_automation,
53
+ gather_raw_query_files,
54
+ generate_excalidraw_diagram,
55
+ generate_summary_from_files,
56
+ get_conversation_command,
57
+ is_query_empty,
58
+ is_ready_to_chat,
59
+ read_chat_stream,
60
+ update_telemetry_state,
61
+ validate_chat_model,
62
+ )
63
+ from khoj.routers.research import (
64
+ InformationCollectionIteration,
65
+ execute_information_collection,
66
+ )
67
+ from khoj.routers.storage import upload_image_to_bucket
68
+ from khoj.utils import state
69
+ from khoj.utils.helpers import (
70
+ AsyncIteratorWrapper,
71
+ ConversationCommand,
72
+ command_descriptions,
73
+ convert_image_to_webp,
74
+ get_country_code_from_timezone,
75
+ get_country_name_from_timezone,
76
+ get_device,
77
+ is_none_or_empty,
78
+ )
79
+ from khoj.utils.rawconfig import (
80
+ ChatRequestBody,
81
+ FileAttachment,
82
+ FileFilterRequest,
83
+ FilesFilterRequest,
84
+ LocationData,
85
+ )
86
+
87
+ # Initialize Router
88
+ logger = logging.getLogger(__name__)
89
+ conversation_command_rate_limiter = ConversationCommandRateLimiter(
90
+ trial_rate_limit=20, subscribed_rate_limit=75, slug="command"
91
+ )
92
+
93
+
94
+ api_chat = APIRouter()
95
+
96
+
97
+ @api_chat.get("/conversation/file-filters/{conversation_id}", response_class=Response)
98
+ @requires(["authenticated"])
99
+ def get_file_filter(request: Request, conversation_id: str) -> Response:
100
+ conversation = ConversationAdapters.get_conversation_by_user(request.user.object, conversation_id=conversation_id)
101
+ if not conversation:
102
+ return Response(content=json.dumps({"status": "error", "message": "Conversation not found"}), status_code=404)
103
+
104
+ # get all files from "computer"
105
+ file_list = EntryAdapters.get_all_filenames_by_source(request.user.object, "computer")
106
+ file_filters = []
107
+ for file in conversation.file_filters:
108
+ if file in file_list:
109
+ file_filters.append(file)
110
+ return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200)
111
+
112
+
113
+ @api_chat.delete("/conversation/file-filters/bulk", response_class=Response)
114
+ @requires(["authenticated"])
115
+ def remove_files_filter(request: Request, filter: FilesFilterRequest) -> Response:
116
+ conversation_id = filter.conversation_id
117
+ files_filter = filter.filenames
118
+ file_filters = ConversationAdapters.remove_files_from_filter(request.user.object, conversation_id, files_filter)
119
+ return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200)
120
+
121
+
122
+ @api_chat.post("/conversation/file-filters/bulk", response_class=Response)
123
+ @requires(["authenticated"])
124
+ def add_files_filter(request: Request, filter: FilesFilterRequest):
125
+ try:
126
+ conversation_id = filter.conversation_id
127
+ files_filter = filter.filenames
128
+ file_filters = ConversationAdapters.add_files_to_filter(request.user.object, conversation_id, files_filter)
129
+ return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200)
130
+ except Exception as e:
131
+ logger.error(f"Error adding file filter {filter.filenames}: {e}", exc_info=True)
132
+ raise HTTPException(status_code=422, detail=str(e))
133
+
134
+
135
+ @api_chat.post("/conversation/file-filters", response_class=Response)
136
+ @requires(["authenticated"])
137
+ def add_file_filter(request: Request, filter: FileFilterRequest):
138
+ try:
139
+ conversation_id = filter.conversation_id
140
+ files_filter = [filter.filename]
141
+ file_filters = ConversationAdapters.add_files_to_filter(request.user.object, conversation_id, files_filter)
142
+ return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200)
143
+ except Exception as e:
144
+ logger.error(f"Error adding file filter {filter.filename}: {e}", exc_info=True)
145
+ raise HTTPException(status_code=422, detail=str(e))
146
+
147
+
148
+ @api_chat.delete("/conversation/file-filters", response_class=Response)
149
+ @requires(["authenticated"])
150
+ def remove_file_filter(request: Request, filter: FileFilterRequest) -> Response:
151
+ conversation_id = filter.conversation_id
152
+ files_filter = [filter.filename]
153
+ file_filters = ConversationAdapters.remove_files_from_filter(request.user.object, conversation_id, files_filter)
154
+ return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200)
155
+
156
+
157
+ @api_chat.post("/feedback")
158
+ @requires(["authenticated"])
159
+ async def sendfeedback(request: Request, data: FeedbackData):
160
+ user: KhojUser = request.user.object
161
+ await send_query_feedback(data.uquery, data.kquery, data.sentiment, user.email)
162
+
163
+
164
+ @api_chat.post("/speech")
165
+ @requires(["authenticated"])
166
+ async def text_to_speech(
167
+ request: Request,
168
+ common: CommonQueryParams,
169
+ text: str,
170
+ rate_limiter_per_minute=Depends(
171
+ ApiUserRateLimiter(requests=30, subscribed_requests=30, window=60, slug="chat_minute")
172
+ ),
173
+ rate_limiter_per_day=Depends(
174
+ ApiUserRateLimiter(requests=100, subscribed_requests=600, window=60 * 60 * 24, slug="chat_day")
175
+ ),
176
+ ) -> Response:
177
+ voice_model = await ConversationAdapters.aget_voice_model_config(request.user.object)
178
+
179
+ params = {"text_to_speak": text}
180
+
181
+ if voice_model:
182
+ params["voice_id"] = voice_model.model_id
183
+
184
+ speech_stream = generate_text_to_speech(**params)
185
+ return StreamingResponse(speech_stream.iter_content(chunk_size=1024), media_type="audio/mpeg")
186
+
187
+
188
+ @api_chat.get("/starters", response_class=Response)
189
+ @requires(["authenticated"])
190
+ async def chat_starters(
191
+ request: Request,
192
+ common: CommonQueryParams,
193
+ ) -> Response:
194
+ user: KhojUser = request.user.object
195
+ starter_questions = await ConversationAdapters.aget_conversation_starters(user)
196
+ return Response(content=json.dumps(starter_questions), media_type="application/json", status_code=200)
197
+
198
+
199
+ @api_chat.get("/history")
200
+ @requires(["authenticated"])
201
+ def chat_history(
202
+ request: Request,
203
+ common: CommonQueryParams,
204
+ conversation_id: Optional[str] = None,
205
+ n: Optional[int] = None,
206
+ ):
207
+ user = request.user.object
208
+ validate_chat_model(user)
209
+
210
+ # Load Conversation History
211
+ conversation = ConversationAdapters.get_conversation_by_user(
212
+ user=user, client_application=request.user.client_app, conversation_id=conversation_id
213
+ )
214
+
215
+ if conversation is None:
216
+ return Response(
217
+ content=json.dumps({"status": "error", "message": f"Conversation: {conversation_id} not found"}),
218
+ status_code=404,
219
+ )
220
+
221
+ agent_metadata = None
222
+ if conversation.agent:
223
+ if conversation.agent.privacy_level == Agent.PrivacyLevel.PRIVATE and conversation.agent.creator != user:
224
+ conversation.agent = None
225
+ else:
226
+ agent_metadata = {
227
+ "slug": conversation.agent.slug,
228
+ "name": conversation.agent.name,
229
+ "isCreator": conversation.agent.creator == user,
230
+ "color": conversation.agent.style_color,
231
+ "icon": conversation.agent.style_icon,
232
+ "persona": conversation.agent.personality,
233
+ }
234
+
235
+ meta_log = conversation.conversation_log
236
+ meta_log.update(
237
+ {
238
+ "conversation_id": conversation.id,
239
+ "slug": conversation.title if conversation.title else conversation.slug,
240
+ "agent": agent_metadata,
241
+ }
242
+ )
243
+
244
+ if n:
245
+ # Get latest N messages if N > 0
246
+ if n > 0 and meta_log.get("chat"):
247
+ meta_log["chat"] = meta_log["chat"][-n:]
248
+ # Else return all messages except latest N
249
+ elif n < 0 and meta_log.get("chat"):
250
+ meta_log["chat"] = meta_log["chat"][:n]
251
+
252
+ update_telemetry_state(
253
+ request=request,
254
+ telemetry_type="api",
255
+ api="chat_history",
256
+ **common.__dict__,
257
+ )
258
+
259
+ return {"status": "ok", "response": meta_log}
260
+
261
+
262
+ @api_chat.get("/share/history")
263
+ def get_shared_chat(
264
+ request: Request,
265
+ common: CommonQueryParams,
266
+ public_conversation_slug: str,
267
+ n: Optional[int] = None,
268
+ ):
269
+ user = request.user.object if request.user.is_authenticated else None
270
+
271
+ # Load Conversation History
272
+ conversation = PublicConversationAdapters.get_public_conversation_by_slug(public_conversation_slug)
273
+
274
+ if conversation is None:
275
+ return Response(
276
+ content=json.dumps({"status": "error", "message": f"Conversation: {public_conversation_slug} not found"}),
277
+ status_code=404,
278
+ )
279
+
280
+ agent_metadata = None
281
+ if conversation.agent:
282
+ if conversation.agent.privacy_level == Agent.PrivacyLevel.PRIVATE:
283
+ conversation.agent = None
284
+ else:
285
+ agent_metadata = {
286
+ "slug": conversation.agent.slug,
287
+ "name": conversation.agent.name,
288
+ "isCreator": conversation.agent.creator == user,
289
+ "color": conversation.agent.style_color,
290
+ "icon": conversation.agent.style_icon,
291
+ "persona": conversation.agent.personality,
292
+ }
293
+
294
+ meta_log = conversation.conversation_log
295
+ scrubbed_title = conversation.title if conversation.title else conversation.slug
296
+
297
+ if scrubbed_title:
298
+ scrubbed_title = scrubbed_title.replace("-", " ")
299
+
300
+ meta_log.update(
301
+ {
302
+ "conversation_id": conversation.id,
303
+ "slug": scrubbed_title,
304
+ "agent": agent_metadata,
305
+ }
306
+ )
307
+
308
+ if n:
309
+ # Get latest N messages if N > 0
310
+ if n > 0 and meta_log.get("chat"):
311
+ meta_log["chat"] = meta_log["chat"][-n:]
312
+ # Else return all messages except latest N
313
+ elif n < 0 and meta_log.get("chat"):
314
+ meta_log["chat"] = meta_log["chat"][:n]
315
+
316
+ update_telemetry_state(
317
+ request=request,
318
+ telemetry_type="api",
319
+ api="get_shared_chat_history",
320
+ **common.__dict__,
321
+ )
322
+
323
+ return {"status": "ok", "response": meta_log}
324
+
325
+
326
+ @api_chat.delete("/history")
327
+ @requires(["authenticated"])
328
+ async def clear_chat_history(
329
+ request: Request,
330
+ common: CommonQueryParams,
331
+ conversation_id: Optional[str] = None,
332
+ ):
333
+ user = request.user.object
334
+
335
+ # Clear Conversation History
336
+ await ConversationAdapters.adelete_conversation_by_user(user, request.user.client_app, conversation_id)
337
+
338
+ update_telemetry_state(
339
+ request=request,
340
+ telemetry_type="api",
341
+ api="clear_chat_history",
342
+ **common.__dict__,
343
+ )
344
+
345
+ return {"status": "ok", "message": "Conversation history cleared"}
346
+
347
+
348
+ @api_chat.post("/share/fork")
349
+ @requires(["authenticated"])
350
+ def fork_public_conversation(
351
+ request: Request,
352
+ common: CommonQueryParams,
353
+ public_conversation_slug: str,
354
+ ):
355
+ user = request.user.object
356
+
357
+ # Load Conversation History
358
+ public_conversation = PublicConversationAdapters.get_public_conversation_by_slug(public_conversation_slug)
359
+
360
+ # Duplicate Public Conversation to User's Private Conversation
361
+ new_conversation = ConversationAdapters.create_conversation_from_public_conversation(
362
+ user, public_conversation, request.user.client_app
363
+ )
364
+
365
+ chat_metadata = {"forked_conversation": public_conversation.slug}
366
+
367
+ update_telemetry_state(
368
+ request=request,
369
+ telemetry_type="api",
370
+ api="fork_public_conversation",
371
+ **common.__dict__,
372
+ metadata=chat_metadata,
373
+ )
374
+
375
+ redirect_uri = str(request.app.url_path_for("chat_page"))
376
+
377
+ return Response(
378
+ status_code=200,
379
+ content=json.dumps(
380
+ {
381
+ "status": "ok",
382
+ "next_url": redirect_uri,
383
+ "conversation_id": str(new_conversation.id),
384
+ }
385
+ ),
386
+ )
387
+
388
+
389
+ @api_chat.post("/share")
390
+ @requires(["authenticated"])
391
+ def duplicate_chat_history_public_conversation(
392
+ request: Request,
393
+ common: CommonQueryParams,
394
+ conversation_id: str,
395
+ ):
396
+ user = request.user.object
397
+ domain = request.headers.get("host")
398
+ scheme = request.url.scheme
399
+
400
+ # Throw unauthorized exception if domain not in ALLOWED_HOSTS
401
+ host_domain = domain.split(":")[0]
402
+ if host_domain not in ALLOWED_HOSTS:
403
+ raise HTTPException(status_code=401, detail="Unauthorized domain")
404
+
405
+ # Duplicate Conversation History to Public Conversation
406
+ conversation = ConversationAdapters.get_conversation_by_user(user, request.user.client_app, conversation_id)
407
+ public_conversation = ConversationAdapters.make_public_conversation_copy(conversation)
408
+ public_conversation_url = PublicConversationAdapters.get_public_conversation_url(public_conversation)
409
+
410
+ update_telemetry_state(
411
+ request=request,
412
+ telemetry_type="api",
413
+ api="post_chat_share",
414
+ **common.__dict__,
415
+ )
416
+
417
+ return Response(
418
+ status_code=200, content=json.dumps({"status": "ok", "url": f"{scheme}://{domain}{public_conversation_url}"})
419
+ )
420
+
421
+
422
+ @api_chat.get("/sessions")
423
+ @requires(["authenticated"])
424
+ def chat_sessions(
425
+ request: Request,
426
+ common: CommonQueryParams,
427
+ recent: Optional[bool] = False,
428
+ ):
429
+ user = request.user.object
430
+
431
+ # Load Conversation Sessions
432
+ conversations = ConversationAdapters.get_conversation_sessions(user, request.user.client_app)
433
+ if recent:
434
+ conversations = conversations[:8]
435
+
436
+ sessions = conversations.values_list(
437
+ "id",
438
+ "slug",
439
+ "title",
440
+ "agent__slug",
441
+ "agent__name",
442
+ "created_at",
443
+ "updated_at",
444
+ "agent__style_icon",
445
+ "agent__style_color",
446
+ )
447
+
448
+ session_values = [
449
+ {
450
+ "conversation_id": str(session[0]),
451
+ "slug": session[2] or session[1],
452
+ "agent_name": session[4],
453
+ "created": session[5].strftime("%Y-%m-%d %H:%M:%S"),
454
+ "updated": session[6].strftime("%Y-%m-%d %H:%M:%S"),
455
+ "agent_icon": session[7],
456
+ "agent_color": session[8],
457
+ }
458
+ for session in sessions
459
+ ]
460
+
461
+ update_telemetry_state(
462
+ request=request,
463
+ telemetry_type="api",
464
+ api="chat_sessions",
465
+ **common.__dict__,
466
+ )
467
+
468
+ return Response(content=json.dumps(session_values), media_type="application/json", status_code=200)
469
+
470
+
471
+ @api_chat.post("/sessions")
472
+ @requires(["authenticated"])
473
+ async def create_chat_session(
474
+ request: Request,
475
+ common: CommonQueryParams,
476
+ agent_slug: Optional[str] = None,
477
+ ):
478
+ user = request.user.object
479
+
480
+ # Create new Conversation Session
481
+ conversation = await ConversationAdapters.acreate_conversation_session(user, request.user.client_app, agent_slug)
482
+
483
+ response = {"conversation_id": str(conversation.id)}
484
+
485
+ conversation_metadata = {
486
+ "agent": agent_slug,
487
+ }
488
+
489
+ update_telemetry_state(
490
+ request=request,
491
+ telemetry_type="api",
492
+ api="create_chat_sessions",
493
+ metadata=conversation_metadata,
494
+ **common.__dict__,
495
+ )
496
+
497
+ return Response(content=json.dumps(response), media_type="application/json", status_code=200)
498
+
499
+
500
+ @api_chat.get("/options", response_class=Response)
501
+ async def chat_options(
502
+ request: Request,
503
+ common: CommonQueryParams,
504
+ ) -> Response:
505
+ cmd_options = {}
506
+ for cmd in ConversationCommand:
507
+ if cmd in command_descriptions:
508
+ cmd_options[cmd.value] = command_descriptions[cmd]
509
+
510
+ update_telemetry_state(
511
+ request=request,
512
+ telemetry_type="api",
513
+ api="chat_options",
514
+ **common.__dict__,
515
+ )
516
+ return Response(content=json.dumps(cmd_options), media_type="application/json", status_code=200)
517
+
518
+
519
+ @api_chat.patch("/title", response_class=Response)
520
+ @requires(["authenticated"])
521
+ async def set_conversation_title(
522
+ request: Request,
523
+ common: CommonQueryParams,
524
+ title: str,
525
+ conversation_id: Optional[str] = None,
526
+ ) -> Response:
527
+ user = request.user.object
528
+ title = title.strip()[:200]
529
+
530
+ # Set Conversation Title
531
+ conversation = await ConversationAdapters.aset_conversation_title(
532
+ user, request.user.client_app, conversation_id, title
533
+ )
534
+
535
+ success = True if conversation else False
536
+
537
+ update_telemetry_state(
538
+ request=request,
539
+ telemetry_type="api",
540
+ api="set_conversation_title",
541
+ **common.__dict__,
542
+ )
543
+
544
+ return Response(
545
+ content=json.dumps({"status": "ok", "success": success}), media_type="application/json", status_code=200
546
+ )
547
+
548
+
549
+ @api_chat.post("/title")
550
+ @requires(["authenticated"])
551
+ async def generate_chat_title(
552
+ request: Request,
553
+ common: CommonQueryParams,
554
+ conversation_id: str,
555
+ ):
556
+ user: KhojUser = request.user.object
557
+ conversation = await ConversationAdapters.aget_conversation_by_user(user=user, conversation_id=conversation_id)
558
+
559
+ # Conversation.title is explicitly set by the user. Do not override.
560
+ if conversation.title:
561
+ return {"status": "ok", "title": conversation.title}
562
+
563
+ if not conversation:
564
+ raise HTTPException(status_code=404, detail="Conversation not found")
565
+
566
+ new_title = await acreate_title_from_history(request.user.object, conversation=conversation)
567
+ conversation.slug = new_title[:200]
568
+
569
+ await conversation.asave()
570
+
571
+ return {"status": "ok", "title": new_title}
572
+
573
+
574
+ @api_chat.delete("/conversation/message", response_class=Response)
575
+ @requires(["authenticated"])
576
+ def delete_message(request: Request, delete_request: DeleteMessageRequestBody) -> Response:
577
+ user = request.user.object
578
+ success = ConversationAdapters.delete_message_by_turn_id(
579
+ user, delete_request.conversation_id, delete_request.turn_id
580
+ )
581
+ if success:
582
+ return Response(content=json.dumps({"status": "ok"}), media_type="application/json", status_code=200)
583
+ else:
584
+ return Response(content=json.dumps({"status": "error", "message": "Message not found"}), status_code=404)
585
+
586
+
587
+ @api_chat.post("")
588
+ @requires(["authenticated"])
589
+ async def chat(
590
+ request: Request,
591
+ common: CommonQueryParams,
592
+ body: ChatRequestBody,
593
+ rate_limiter_per_minute=Depends(
594
+ ApiUserRateLimiter(requests=20, subscribed_requests=20, window=60, slug="chat_minute")
595
+ ),
596
+ rate_limiter_per_day=Depends(
597
+ ApiUserRateLimiter(requests=100, subscribed_requests=600, window=60 * 60 * 24, slug="chat_day")
598
+ ),
599
+ image_rate_limiter=Depends(ApiImageRateLimiter(max_images=10, max_combined_size_mb=20)),
600
+ ):
601
+ # Access the parameters from the body
602
+ q = body.q
603
+ n = body.n
604
+ d = body.d
605
+ stream = body.stream
606
+ title = body.title
607
+ conversation_id = body.conversation_id
608
+ turn_id = str(body.turn_id or uuid.uuid4())
609
+ city = body.city
610
+ region = body.region
611
+ country = body.country or get_country_name_from_timezone(body.timezone)
612
+ country_code = body.country_code or get_country_code_from_timezone(body.timezone)
613
+ timezone = body.timezone
614
+ raw_images = body.images
615
+ raw_query_files = body.files
616
+
617
+ async def event_generator(q: str, images: list[str]):
618
+ start_time = time.perf_counter()
619
+ ttft = None
620
+ chat_metadata: dict = {}
621
+ connection_alive = True
622
+ user: KhojUser = request.user.object
623
+ event_delimiter = "␃🔚␗"
624
+ q = unquote(q)
625
+ train_of_thought = []
626
+ nonlocal conversation_id
627
+ nonlocal raw_query_files
628
+
629
+ tracer: dict = {
630
+ "mid": turn_id,
631
+ "cid": conversation_id,
632
+ "uid": user.id,
633
+ "khoj_version": state.khoj_version,
634
+ }
635
+
636
+ uploaded_images: list[str] = []
637
+ if images:
638
+ for image in images:
639
+ decoded_string = unquote(image)
640
+ base64_data = decoded_string.split(",", 1)[1]
641
+ image_bytes = base64.b64decode(base64_data)
642
+ webp_image_bytes = convert_image_to_webp(image_bytes)
643
+ uploaded_image = upload_image_to_bucket(webp_image_bytes, request.user.object.id)
644
+ if uploaded_image:
645
+ uploaded_images.append(uploaded_image)
646
+
647
+ query_files: Dict[str, str] = {}
648
+ if raw_query_files:
649
+ for file in raw_query_files:
650
+ query_files[file.name] = file.content
651
+
652
+ async def send_event(event_type: ChatEvent, data: str | dict):
653
+ nonlocal connection_alive, ttft, train_of_thought
654
+ if not connection_alive or await request.is_disconnected():
655
+ connection_alive = False
656
+ logger.warning(f"User {user} disconnected from {common.client} client")
657
+ return
658
+ try:
659
+ if event_type == ChatEvent.END_LLM_RESPONSE:
660
+ collect_telemetry()
661
+ elif event_type == ChatEvent.START_LLM_RESPONSE:
662
+ ttft = time.perf_counter() - start_time
663
+ elif event_type == ChatEvent.STATUS:
664
+ train_of_thought.append({"type": event_type.value, "data": data})
665
+
666
+ if event_type == ChatEvent.MESSAGE:
667
+ yield data
668
+ elif event_type == ChatEvent.REFERENCES or ChatEvent.METADATA or stream:
669
+ yield json.dumps({"type": event_type.value, "data": data}, ensure_ascii=False)
670
+ except asyncio.CancelledError as e:
671
+ connection_alive = False
672
+ logger.warn(f"User {user} disconnected from {common.client} client: {e}")
673
+ return
674
+ except Exception as e:
675
+ connection_alive = False
676
+ logger.error(f"Failed to stream chat API response to {user} on {common.client}: {e}", exc_info=True)
677
+ return
678
+ finally:
679
+ yield event_delimiter
680
+
681
+ async def send_llm_response(response: str, usage: dict = None):
682
+ # Send Chat Response
683
+ async for result in send_event(ChatEvent.START_LLM_RESPONSE, ""):
684
+ yield result
685
+ async for result in send_event(ChatEvent.MESSAGE, response):
686
+ yield result
687
+ async for result in send_event(ChatEvent.END_LLM_RESPONSE, ""):
688
+ yield result
689
+ # Send Usage Metadata once llm interactions are complete
690
+ if usage:
691
+ async for event in send_event(ChatEvent.USAGE, usage):
692
+ yield event
693
+ async for result in send_event(ChatEvent.END_RESPONSE, ""):
694
+ yield result
695
+
696
+ def collect_telemetry():
697
+ # Gather chat response telemetry
698
+ nonlocal chat_metadata
699
+ latency = time.perf_counter() - start_time
700
+ cmd_set = set([cmd.value for cmd in conversation_commands])
701
+ cost = (tracer.get("usage", {}) or {}).get("cost", 0)
702
+ chat_metadata = chat_metadata or {}
703
+ chat_metadata["conversation_command"] = cmd_set
704
+ chat_metadata["agent"] = conversation.agent.slug if conversation and conversation.agent else None
705
+ chat_metadata["latency"] = f"{latency:.3f}"
706
+ chat_metadata["ttft_latency"] = f"{ttft:.3f}"
707
+ chat_metadata["cost"] = f"{cost:.5f}"
708
+
709
+ logger.info(f"Chat response time to first token: {ttft:.3f} seconds")
710
+ logger.info(f"Chat response total time: {latency:.3f} seconds")
711
+ logger.info(f"Chat response cost: ${cost:.5f}")
712
+ update_telemetry_state(
713
+ request=request,
714
+ telemetry_type="api",
715
+ api="chat",
716
+ client=common.client,
717
+ user_agent=request.headers.get("user-agent"),
718
+ host=request.headers.get("host"),
719
+ metadata=chat_metadata,
720
+ )
721
+
722
+ if is_query_empty(q):
723
+ async for result in send_llm_response("Please ask your query to get started.", tracer.get("usage")):
724
+ yield result
725
+ return
726
+
727
+ # Automated tasks are handled before to allow mixing them with other conversation commands
728
+ cmds_to_rate_limit = []
729
+ is_automated_task = False
730
+ if q.startswith("/automated_task"):
731
+ is_automated_task = True
732
+ q = q.replace("/automated_task", "").lstrip()
733
+ cmds_to_rate_limit += [ConversationCommand.AutomatedTask]
734
+
735
+ # Extract conversation command from query
736
+ conversation_commands = [get_conversation_command(query=q)]
737
+
738
+ conversation = await ConversationAdapters.aget_conversation_by_user(
739
+ user,
740
+ client_application=request.user.client_app,
741
+ conversation_id=conversation_id,
742
+ title=title,
743
+ create_new=body.create_new,
744
+ )
745
+ if not conversation:
746
+ async for result in send_llm_response(f"Conversation {conversation_id} not found", tracer.get("usage")):
747
+ yield result
748
+ return
749
+ conversation_id = conversation.id
750
+
751
+ async for event in send_event(ChatEvent.METADATA, {"conversationId": str(conversation_id), "turnId": turn_id}):
752
+ yield event
753
+
754
+ agent: Agent | None = None
755
+ default_agent = await AgentAdapters.aget_default_agent()
756
+ if conversation.agent and conversation.agent != default_agent:
757
+ agent = conversation.agent
758
+
759
+ if not conversation.agent:
760
+ conversation.agent = default_agent
761
+ await conversation.asave()
762
+ agent = default_agent
763
+
764
+ await is_ready_to_chat(user)
765
+ user_name = await aget_user_name(user)
766
+ location = None
767
+ if city or region or country or country_code:
768
+ location = LocationData(city=city, region=region, country=country, country_code=country_code)
769
+ user_message_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
770
+ meta_log = conversation.conversation_log
771
+
772
+ researched_results = ""
773
+ online_results: Dict = dict()
774
+ code_results: Dict = dict()
775
+ generated_asset_results: Dict = dict()
776
+ ## Extract Document References
777
+ compiled_references: List[Any] = []
778
+ inferred_queries: List[Any] = []
779
+ file_filters = conversation.file_filters if conversation and conversation.file_filters else []
780
+ attached_file_context = gather_raw_query_files(query_files)
781
+
782
+ generated_images: List[str] = []
783
+ generated_files: List[FileAttachment] = []
784
+ generated_excalidraw_diagram: str = None
785
+ program_execution_context: List[str] = []
786
+
787
+ if conversation_commands == [ConversationCommand.Default]:
788
+ try:
789
+ chosen_io = await aget_data_sources_and_output_format(
790
+ q,
791
+ meta_log,
792
+ is_automated_task,
793
+ user=user,
794
+ query_images=uploaded_images,
795
+ agent=agent,
796
+ query_files=attached_file_context,
797
+ tracer=tracer,
798
+ )
799
+ except ValueError as e:
800
+ logger.error(f"Error getting data sources and output format: {e}. Falling back to default.")
801
+ conversation_commands = [ConversationCommand.General]
802
+
803
+ conversation_commands = chosen_io.get("sources") + [chosen_io.get("output")]
804
+
805
+ # If we're doing research, we don't want to do anything else
806
+ if ConversationCommand.Research in conversation_commands:
807
+ conversation_commands = [ConversationCommand.Research]
808
+
809
+ conversation_commands_str = ", ".join([cmd.value for cmd in conversation_commands])
810
+ async for result in send_event(ChatEvent.STATUS, f"**Selected Tools:** {conversation_commands_str}"):
811
+ yield result
812
+
813
+ cmds_to_rate_limit += conversation_commands
814
+ for cmd in cmds_to_rate_limit:
815
+ try:
816
+ await conversation_command_rate_limiter.update_and_check_if_valid(request, cmd)
817
+ q = q.replace(f"/{cmd.value}", "").strip()
818
+ except HTTPException as e:
819
+ async for result in send_llm_response(str(e.detail), tracer.get("usage")):
820
+ yield result
821
+ return
822
+
823
+ defiltered_query = defilter_query(q)
824
+
825
+ if conversation_commands == [ConversationCommand.Research]:
826
+ async for research_result in execute_information_collection(
827
+ request=request,
828
+ user=user,
829
+ query=defiltered_query,
830
+ conversation_id=conversation_id,
831
+ conversation_history=meta_log,
832
+ query_images=uploaded_images,
833
+ agent=agent,
834
+ send_status_func=partial(send_event, ChatEvent.STATUS),
835
+ user_name=user_name,
836
+ location=location,
837
+ file_filters=conversation.file_filters if conversation else [],
838
+ query_files=attached_file_context,
839
+ tracer=tracer,
840
+ ):
841
+ if isinstance(research_result, InformationCollectionIteration):
842
+ if research_result.summarizedResult:
843
+ if research_result.onlineContext:
844
+ online_results.update(research_result.onlineContext)
845
+ if research_result.codeContext:
846
+ code_results.update(research_result.codeContext)
847
+ if research_result.context:
848
+ compiled_references.extend(research_result.context)
849
+
850
+ researched_results += research_result.summarizedResult
851
+
852
+ else:
853
+ yield research_result
854
+
855
+ # researched_results = await extract_relevant_info(q, researched_results, agent)
856
+ if state.verbose > 1:
857
+ logger.debug(f"Researched Results: {researched_results}")
858
+
859
+ used_slash_summarize = conversation_commands == [ConversationCommand.Summarize]
860
+ file_filters = conversation.file_filters if conversation else []
861
+ # Skip trying to summarize if
862
+ if (
863
+ # summarization intent was inferred
864
+ ConversationCommand.Summarize in conversation_commands
865
+ # and not triggered via slash command
866
+ and not used_slash_summarize
867
+ # but we can't actually summarize
868
+ and len(file_filters) != 1
869
+ ):
870
+ conversation_commands.remove(ConversationCommand.Summarize)
871
+ elif ConversationCommand.Summarize in conversation_commands:
872
+ response_log = ""
873
+ agent_has_entries = await EntryAdapters.aagent_has_entries(agent)
874
+ if len(file_filters) == 0 and not agent_has_entries:
875
+ response_log = "No files selected for summarization. Please add files using the section on the left."
876
+ async for result in send_llm_response(response_log, tracer.get("usage")):
877
+ yield result
878
+ else:
879
+ async for response in generate_summary_from_files(
880
+ q=q,
881
+ user=user,
882
+ file_filters=file_filters,
883
+ meta_log=meta_log,
884
+ query_images=uploaded_images,
885
+ agent=agent,
886
+ send_status_func=partial(send_event, ChatEvent.STATUS),
887
+ query_files=attached_file_context,
888
+ tracer=tracer,
889
+ ):
890
+ if isinstance(response, dict) and ChatEvent.STATUS in response:
891
+ yield response[ChatEvent.STATUS]
892
+ else:
893
+ if isinstance(response, str):
894
+ response_log = response
895
+ async for result in send_llm_response(response, tracer.get("usage")):
896
+ yield result
897
+
898
+ summarized_document = FileAttachment(
899
+ name="Summarized Document",
900
+ content=response_log,
901
+ type="text/plain",
902
+ size=len(response_log.encode("utf-8")),
903
+ )
904
+
905
+ async for result in send_event(ChatEvent.GENERATED_ASSETS, {"files": [summarized_document.model_dump()]}):
906
+ yield result
907
+
908
+ generated_files.append(summarized_document)
909
+
910
+ custom_filters = []
911
+ if conversation_commands == [ConversationCommand.Help]:
912
+ if not q:
913
+ chat_model = await ConversationAdapters.aget_user_chat_model(user)
914
+ if chat_model == None:
915
+ chat_model = await ConversationAdapters.aget_default_chat_model(user)
916
+ model_type = chat_model.model_type
917
+ formatted_help = help_message.format(model=model_type, version=state.khoj_version, device=get_device())
918
+ async for result in send_llm_response(formatted_help, tracer.get("usage")):
919
+ yield result
920
+ return
921
+ # Adding specification to search online specifically on khoj.dev pages.
922
+ custom_filters.append("site:khoj.dev")
923
+ conversation_commands.append(ConversationCommand.Online)
924
+
925
+ if ConversationCommand.Automation in conversation_commands:
926
+ try:
927
+ automation, crontime, query_to_run, subject = await create_automation(
928
+ q, timezone, user, request.url, meta_log, tracer=tracer
929
+ )
930
+ except Exception as e:
931
+ logger.error(f"Error scheduling task {q} for {user.email}: {e}")
932
+ error_message = f"Unable to create automation. Ensure the automation doesn't already exist."
933
+ async for result in send_llm_response(error_message, tracer.get("usage")):
934
+ yield result
935
+ return
936
+
937
+ llm_response = construct_automation_created_message(automation, crontime, query_to_run, subject)
938
+ await sync_to_async(save_to_conversation_log)(
939
+ q,
940
+ llm_response,
941
+ user,
942
+ meta_log,
943
+ user_message_time,
944
+ intent_type="automation",
945
+ client_application=request.user.client_app,
946
+ conversation_id=conversation_id,
947
+ inferred_queries=[query_to_run],
948
+ automation_id=automation.id,
949
+ query_images=uploaded_images,
950
+ train_of_thought=train_of_thought,
951
+ raw_query_files=raw_query_files,
952
+ tracer=tracer,
953
+ )
954
+ async for result in send_llm_response(llm_response, tracer.get("usage")):
955
+ yield result
956
+ return
957
+
958
+ # Gather Context
959
+ ## Extract Document References
960
+ if not ConversationCommand.Research in conversation_commands:
961
+ try:
962
+ async for result in extract_references_and_questions(
963
+ request,
964
+ meta_log,
965
+ q,
966
+ (n or 7),
967
+ d,
968
+ conversation_id,
969
+ conversation_commands,
970
+ location,
971
+ partial(send_event, ChatEvent.STATUS),
972
+ query_images=uploaded_images,
973
+ agent=agent,
974
+ query_files=attached_file_context,
975
+ tracer=tracer,
976
+ ):
977
+ if isinstance(result, dict) and ChatEvent.STATUS in result:
978
+ yield result[ChatEvent.STATUS]
979
+ else:
980
+ compiled_references.extend(result[0])
981
+ inferred_queries.extend(result[1])
982
+ defiltered_query = result[2]
983
+ except Exception as e:
984
+ error_message = (
985
+ f"Error searching knowledge base: {e}. Attempting to respond without document references."
986
+ )
987
+ logger.error(error_message, exc_info=True)
988
+ async for result in send_event(
989
+ ChatEvent.STATUS, "Document search failed. I'll try respond without document references"
990
+ ):
991
+ yield result
992
+
993
+ if not is_none_or_empty(compiled_references):
994
+ headings = "\n- " + "\n- ".join(set([c.get("compiled", c).split("\n")[0] for c in compiled_references]))
995
+ # Strip only leading # from headings
996
+ headings = headings.replace("#", "")
997
+ async for result in send_event(ChatEvent.STATUS, f"**Found Relevant Notes**: {headings}"):
998
+ yield result
999
+
1000
+ if conversation_commands == [ConversationCommand.Notes] and not await EntryAdapters.auser_has_entries(user):
1001
+ async for result in send_llm_response(f"{no_entries_found.format()}", tracer.get("usage")):
1002
+ yield result
1003
+ return
1004
+
1005
+ if ConversationCommand.Notes in conversation_commands and is_none_or_empty(compiled_references):
1006
+ conversation_commands.remove(ConversationCommand.Notes)
1007
+
1008
+ ## Gather Online References
1009
+ if ConversationCommand.Online in conversation_commands:
1010
+ try:
1011
+ async for result in search_online(
1012
+ defiltered_query,
1013
+ meta_log,
1014
+ location,
1015
+ user,
1016
+ partial(send_event, ChatEvent.STATUS),
1017
+ custom_filters,
1018
+ query_images=uploaded_images,
1019
+ agent=agent,
1020
+ query_files=attached_file_context,
1021
+ tracer=tracer,
1022
+ ):
1023
+ if isinstance(result, dict) and ChatEvent.STATUS in result:
1024
+ yield result[ChatEvent.STATUS]
1025
+ else:
1026
+ online_results = result
1027
+ except Exception as e:
1028
+ error_message = f"Error searching online: {e}. Attempting to respond without online results"
1029
+ logger.warning(error_message)
1030
+ async for result in send_event(
1031
+ ChatEvent.STATUS, "Online search failed. I'll try respond without online references"
1032
+ ):
1033
+ yield result
1034
+
1035
+ ## Gather Webpage References
1036
+ if ConversationCommand.Webpage in conversation_commands:
1037
+ try:
1038
+ async for result in read_webpages(
1039
+ defiltered_query,
1040
+ meta_log,
1041
+ location,
1042
+ user,
1043
+ partial(send_event, ChatEvent.STATUS),
1044
+ query_images=uploaded_images,
1045
+ agent=agent,
1046
+ query_files=attached_file_context,
1047
+ tracer=tracer,
1048
+ ):
1049
+ if isinstance(result, dict) and ChatEvent.STATUS in result:
1050
+ yield result[ChatEvent.STATUS]
1051
+ else:
1052
+ direct_web_pages = result
1053
+ webpages = []
1054
+ for query in direct_web_pages:
1055
+ if online_results.get(query):
1056
+ online_results[query]["webpages"] = direct_web_pages[query]["webpages"]
1057
+ else:
1058
+ online_results[query] = {"webpages": direct_web_pages[query]["webpages"]}
1059
+
1060
+ for webpage in direct_web_pages[query]["webpages"]:
1061
+ webpages.append(webpage["link"])
1062
+ async for result in send_event(ChatEvent.STATUS, f"**Read web pages**: {webpages}"):
1063
+ yield result
1064
+ except Exception as e:
1065
+ logger.warning(
1066
+ f"Error reading webpages: {e}. Attempting to respond without webpage results",
1067
+ exc_info=True,
1068
+ )
1069
+ async for result in send_event(
1070
+ ChatEvent.STATUS, "Webpage read failed. I'll try respond without webpage references"
1071
+ ):
1072
+ yield result
1073
+
1074
+ ## Gather Code Results
1075
+ if ConversationCommand.Code in conversation_commands:
1076
+ try:
1077
+ context = f"# Iteration 1:\n#---\nNotes:\n{compiled_references}\n\nOnline Results:{online_results}"
1078
+ async for result in run_code(
1079
+ defiltered_query,
1080
+ meta_log,
1081
+ context,
1082
+ location,
1083
+ user,
1084
+ partial(send_event, ChatEvent.STATUS),
1085
+ query_images=uploaded_images,
1086
+ agent=agent,
1087
+ query_files=attached_file_context,
1088
+ tracer=tracer,
1089
+ ):
1090
+ if isinstance(result, dict) and ChatEvent.STATUS in result:
1091
+ yield result[ChatEvent.STATUS]
1092
+ else:
1093
+ code_results = result
1094
+ async for result in send_event(ChatEvent.STATUS, f"**Ran code snippets**: {len(code_results)}"):
1095
+ yield result
1096
+ except ValueError as e:
1097
+ program_execution_context.append(f"Failed to run code")
1098
+ logger.warning(
1099
+ f"Failed to use code tool: {e}. Attempting to respond without code results",
1100
+ exc_info=True,
1101
+ )
1102
+
1103
+ ## Send Gathered References
1104
+ unique_online_results = deduplicate_organic_results(online_results)
1105
+ async for result in send_event(
1106
+ ChatEvent.REFERENCES,
1107
+ {
1108
+ "inferredQueries": inferred_queries,
1109
+ "context": compiled_references,
1110
+ "onlineContext": unique_online_results,
1111
+ "codeContext": code_results,
1112
+ },
1113
+ ):
1114
+ yield result
1115
+
1116
+ # Generate Output
1117
+ ## Generate Image Output
1118
+ if ConversationCommand.Image in conversation_commands:
1119
+ async for result in text_to_image(
1120
+ defiltered_query,
1121
+ user,
1122
+ meta_log,
1123
+ location_data=location,
1124
+ references=compiled_references,
1125
+ online_results=online_results,
1126
+ send_status_func=partial(send_event, ChatEvent.STATUS),
1127
+ query_images=uploaded_images,
1128
+ agent=agent,
1129
+ query_files=attached_file_context,
1130
+ tracer=tracer,
1131
+ ):
1132
+ if isinstance(result, dict) and ChatEvent.STATUS in result:
1133
+ yield result[ChatEvent.STATUS]
1134
+ else:
1135
+ generated_image, status_code, improved_image_prompt = result
1136
+
1137
+ inferred_queries.append(improved_image_prompt)
1138
+ if generated_image is None or status_code != 200:
1139
+ program_execution_context.append(f"Failed to generate image with {improved_image_prompt}")
1140
+ async for result in send_event(ChatEvent.STATUS, f"Failed to generate image"):
1141
+ yield result
1142
+ else:
1143
+ generated_images.append(generated_image)
1144
+
1145
+ generated_asset_results["images"] = {
1146
+ "query": improved_image_prompt,
1147
+ }
1148
+
1149
+ async for result in send_event(
1150
+ ChatEvent.GENERATED_ASSETS,
1151
+ {
1152
+ "images": [generated_image],
1153
+ },
1154
+ ):
1155
+ yield result
1156
+
1157
+ if ConversationCommand.Diagram in conversation_commands:
1158
+ async for result in send_event(ChatEvent.STATUS, f"Creating diagram"):
1159
+ yield result
1160
+
1161
+ inferred_queries = []
1162
+ diagram_description = ""
1163
+
1164
+ async for result in generate_excalidraw_diagram(
1165
+ q=defiltered_query,
1166
+ conversation_history=meta_log,
1167
+ location_data=location,
1168
+ note_references=compiled_references,
1169
+ online_results=online_results,
1170
+ query_images=uploaded_images,
1171
+ user=user,
1172
+ agent=agent,
1173
+ send_status_func=partial(send_event, ChatEvent.STATUS),
1174
+ query_files=attached_file_context,
1175
+ tracer=tracer,
1176
+ ):
1177
+ if isinstance(result, dict) and ChatEvent.STATUS in result:
1178
+ yield result[ChatEvent.STATUS]
1179
+ else:
1180
+ better_diagram_description_prompt, excalidraw_diagram_description = result
1181
+ if better_diagram_description_prompt and excalidraw_diagram_description:
1182
+ inferred_queries.append(better_diagram_description_prompt)
1183
+ diagram_description = excalidraw_diagram_description
1184
+
1185
+ generated_excalidraw_diagram = diagram_description
1186
+
1187
+ generated_asset_results["diagrams"] = {
1188
+ "query": better_diagram_description_prompt,
1189
+ }
1190
+
1191
+ async for result in send_event(
1192
+ ChatEvent.GENERATED_ASSETS,
1193
+ {
1194
+ "excalidrawDiagram": excalidraw_diagram_description,
1195
+ },
1196
+ ):
1197
+ yield result
1198
+ else:
1199
+ error_message = "Failed to generate diagram. Please try again later."
1200
+ program_execution_context.append(
1201
+ prompts.failed_diagram_generation.format(
1202
+ attempted_diagram=better_diagram_description_prompt
1203
+ )
1204
+ )
1205
+
1206
+ async for result in send_event(ChatEvent.STATUS, error_message):
1207
+ yield result
1208
+
1209
+ ## Generate Text Output
1210
+ async for result in send_event(ChatEvent.STATUS, f"**Generating a well-informed response**"):
1211
+ yield result
1212
+
1213
+ llm_response, chat_metadata = await agenerate_chat_response(
1214
+ defiltered_query,
1215
+ meta_log,
1216
+ conversation,
1217
+ compiled_references,
1218
+ online_results,
1219
+ code_results,
1220
+ inferred_queries,
1221
+ conversation_commands,
1222
+ user,
1223
+ request.user.client_app,
1224
+ conversation_id,
1225
+ location,
1226
+ user_name,
1227
+ researched_results,
1228
+ uploaded_images,
1229
+ train_of_thought,
1230
+ attached_file_context,
1231
+ raw_query_files,
1232
+ generated_images,
1233
+ generated_files,
1234
+ generated_excalidraw_diagram,
1235
+ program_execution_context,
1236
+ generated_asset_results,
1237
+ tracer,
1238
+ )
1239
+
1240
+ # Send Response
1241
+ async for result in send_event(ChatEvent.START_LLM_RESPONSE, ""):
1242
+ yield result
1243
+
1244
+ continue_stream = True
1245
+ iterator = AsyncIteratorWrapper(llm_response)
1246
+ async for item in iterator:
1247
+ if item is None:
1248
+ async for result in send_event(ChatEvent.END_LLM_RESPONSE, ""):
1249
+ yield result
1250
+ # Send Usage Metadata once llm interactions are complete
1251
+ async for event in send_event(ChatEvent.USAGE, tracer.get("usage")):
1252
+ yield event
1253
+ async for result in send_event(ChatEvent.END_RESPONSE, ""):
1254
+ yield result
1255
+ logger.debug("Finished streaming response")
1256
+ return
1257
+ if not connection_alive or not continue_stream:
1258
+ continue
1259
+ try:
1260
+ async for result in send_event(ChatEvent.MESSAGE, f"{item}"):
1261
+ yield result
1262
+ except Exception as e:
1263
+ continue_stream = False
1264
+ logger.info(f"User {user} disconnected. Emitting rest of responses to clear thread: {e}")
1265
+
1266
+ ## Stream Text Response
1267
+ if stream:
1268
+ return StreamingResponse(event_generator(q, images=raw_images), media_type="text/plain")
1269
+ ## Non-Streaming Text Response
1270
+ else:
1271
+ response_iterator = event_generator(q, images=raw_images)
1272
+ response_data = await read_chat_stream(response_iterator)
1273
+ return Response(content=json.dumps(response_data), media_type="application/json", status_code=200)