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.
- khoj/__init__.py +0 -0
- khoj/app/README.md +94 -0
- khoj/app/__init__.py +0 -0
- khoj/app/asgi.py +16 -0
- khoj/app/settings.py +218 -0
- khoj/app/urls.py +25 -0
- khoj/configure.py +452 -0
- khoj/database/__init__.py +0 -0
- khoj/database/adapters/__init__.py +1821 -0
- khoj/database/admin.py +417 -0
- khoj/database/apps.py +6 -0
- khoj/database/management/__init__.py +0 -0
- khoj/database/management/commands/__init__.py +0 -0
- khoj/database/management/commands/change_default_model.py +116 -0
- khoj/database/management/commands/change_generated_images_url.py +61 -0
- khoj/database/management/commands/convert_images_png_to_webp.py +99 -0
- khoj/database/migrations/0001_khojuser.py +98 -0
- khoj/database/migrations/0002_googleuser.py +32 -0
- khoj/database/migrations/0003_vector_extension.py +10 -0
- khoj/database/migrations/0004_content_types_and_more.py +181 -0
- khoj/database/migrations/0005_embeddings_corpus_id.py +19 -0
- khoj/database/migrations/0006_embeddingsdates.py +33 -0
- khoj/database/migrations/0007_add_conversation.py +27 -0
- khoj/database/migrations/0008_alter_conversation_conversation_log.py +17 -0
- khoj/database/migrations/0009_khojapiuser.py +24 -0
- khoj/database/migrations/0010_chatmodeloptions_and_more.py +83 -0
- khoj/database/migrations/0010_rename_embeddings_entry_and_more.py +30 -0
- khoj/database/migrations/0011_merge_20231102_0138.py +14 -0
- khoj/database/migrations/0012_entry_file_source.py +21 -0
- khoj/database/migrations/0013_subscription.py +37 -0
- khoj/database/migrations/0014_alter_googleuser_picture.py +17 -0
- khoj/database/migrations/0015_alter_subscription_user.py +21 -0
- khoj/database/migrations/0016_alter_subscription_renewal_date.py +17 -0
- khoj/database/migrations/0017_searchmodel.py +32 -0
- khoj/database/migrations/0018_searchmodelconfig_delete_searchmodel.py +30 -0
- khoj/database/migrations/0019_alter_googleuser_family_name_and_more.py +27 -0
- khoj/database/migrations/0020_reflectivequestion.py +36 -0
- khoj/database/migrations/0021_speechtotextmodeloptions_and_more.py +42 -0
- khoj/database/migrations/0022_texttoimagemodelconfig.py +25 -0
- khoj/database/migrations/0023_usersearchmodelconfig.py +33 -0
- khoj/database/migrations/0024_alter_entry_embeddings.py +18 -0
- khoj/database/migrations/0025_clientapplication_khojuser_phone_number_and_more.py +46 -0
- khoj/database/migrations/0025_searchmodelconfig_embeddings_inference_endpoint_and_more.py +22 -0
- khoj/database/migrations/0026_searchmodelconfig_cross_encoder_inference_endpoint_and_more.py +22 -0
- khoj/database/migrations/0027_merge_20240118_1324.py +13 -0
- khoj/database/migrations/0028_khojuser_verified_phone_number.py +17 -0
- khoj/database/migrations/0029_userrequests.py +27 -0
- khoj/database/migrations/0030_conversation_slug_and_title.py +38 -0
- khoj/database/migrations/0031_agent_conversation_agent.py +53 -0
- khoj/database/migrations/0031_alter_googleuser_locale.py +30 -0
- khoj/database/migrations/0032_merge_20240322_0427.py +14 -0
- khoj/database/migrations/0033_rename_tuning_agent_personality.py +17 -0
- khoj/database/migrations/0034_alter_chatmodeloptions_chat_model.py +32 -0
- khoj/database/migrations/0035_processlock.py +26 -0
- khoj/database/migrations/0036_alter_processlock_name.py +19 -0
- khoj/database/migrations/0036_delete_offlinechatprocessorconversationconfig.py +15 -0
- khoj/database/migrations/0036_publicconversation.py +42 -0
- khoj/database/migrations/0037_chatmodeloptions_openai_config_and_more.py +51 -0
- khoj/database/migrations/0037_searchmodelconfig_bi_encoder_docs_encode_config_and_more.py +32 -0
- khoj/database/migrations/0038_merge_20240425_0857.py +14 -0
- khoj/database/migrations/0038_merge_20240426_1640.py +12 -0
- khoj/database/migrations/0039_merge_20240501_0301.py +12 -0
- khoj/database/migrations/0040_alter_processlock_name.py +26 -0
- khoj/database/migrations/0040_merge_20240504_1010.py +14 -0
- khoj/database/migrations/0041_merge_20240505_1234.py +14 -0
- khoj/database/migrations/0042_serverchatsettings.py +46 -0
- khoj/database/migrations/0043_alter_chatmodeloptions_model_type.py +21 -0
- khoj/database/migrations/0044_conversation_file_filters.py +17 -0
- khoj/database/migrations/0045_fileobject.py +37 -0
- khoj/database/migrations/0046_khojuser_email_verification_code_and_more.py +22 -0
- khoj/database/migrations/0047_alter_entry_file_type.py +31 -0
- khoj/database/migrations/0048_voicemodeloption_uservoicemodelconfig.py +52 -0
- khoj/database/migrations/0049_datastore.py +38 -0
- khoj/database/migrations/0049_texttoimagemodelconfig_api_key_and_more.py +58 -0
- khoj/database/migrations/0050_alter_processlock_name.py +25 -0
- khoj/database/migrations/0051_merge_20240702_1220.py +14 -0
- khoj/database/migrations/0052_alter_searchmodelconfig_bi_encoder_docs_encode_config_and_more.py +27 -0
- khoj/database/migrations/0053_agent_style_color_agent_style_icon.py +61 -0
- khoj/database/migrations/0054_alter_agent_style_color.py +38 -0
- khoj/database/migrations/0055_alter_agent_style_icon.py +37 -0
- khoj/database/migrations/0056_chatmodeloptions_vision_enabled.py +17 -0
- khoj/database/migrations/0056_searchmodelconfig_cross_encoder_model_config.py +17 -0
- khoj/database/migrations/0057_merge_20240816_1409.py +13 -0
- khoj/database/migrations/0057_remove_serverchatsettings_default_model_and_more.py +51 -0
- khoj/database/migrations/0058_alter_chatmodeloptions_chat_model.py +17 -0
- khoj/database/migrations/0059_searchmodelconfig_bi_encoder_confidence_threshold.py +17 -0
- khoj/database/migrations/0060_merge_20240905_1828.py +14 -0
- khoj/database/migrations/0061_alter_chatmodeloptions_model_type.py +26 -0
- khoj/database/migrations/0061_alter_texttoimagemodelconfig_model_type.py +21 -0
- khoj/database/migrations/0062_merge_20240913_0222.py +14 -0
- khoj/database/migrations/0063_conversation_temp_id.py +36 -0
- khoj/database/migrations/0064_remove_conversation_temp_id_alter_conversation_id.py +86 -0
- khoj/database/migrations/0065_remove_agent_avatar_remove_agent_public_and_more.py +49 -0
- khoj/database/migrations/0066_remove_agent_tools_agent_input_tools_and_more.py +69 -0
- khoj/database/migrations/0067_alter_agent_style_icon.py +50 -0
- khoj/database/migrations/0068_alter_agent_output_modes.py +24 -0
- khoj/database/migrations/0069_webscraper_serverchatsettings_web_scraper.py +89 -0
- khoj/database/migrations/0070_alter_agent_input_tools_alter_agent_output_modes.py +46 -0
- khoj/database/migrations/0071_subscription_enabled_trial_at_and_more.py +32 -0
- khoj/database/migrations/0072_entry_search_model.py +24 -0
- khoj/database/migrations/0073_delete_usersearchmodelconfig.py +15 -0
- khoj/database/migrations/0074_alter_conversation_title.py +17 -0
- khoj/database/migrations/0075_migrate_generated_assets_and_validate.py +85 -0
- khoj/database/migrations/0076_rename_openaiprocessorconversationconfig_aimodelapi_and_more.py +26 -0
- khoj/database/migrations/0077_chatmodel_alter_agent_chat_model_and_more.py +62 -0
- khoj/database/migrations/0078_khojuser_email_verification_code_expiry.py +17 -0
- khoj/database/migrations/__init__.py +0 -0
- khoj/database/models/__init__.py +725 -0
- khoj/database/tests.py +3 -0
- khoj/interface/compiled/404/index.html +1 -0
- khoj/interface/compiled/_next/static/Tg-vU1p1B-YKT5Qv8KSHt/_buildManifest.js +1 -0
- khoj/interface/compiled/_next/static/Tg-vU1p1B-YKT5Qv8KSHt/_ssgManifest.js +1 -0
- khoj/interface/compiled/_next/static/chunks/1010-8f39bb4648b5ba10.js +1 -0
- khoj/interface/compiled/_next/static/chunks/182-f1c48a203dc91e0e.js +20 -0
- khoj/interface/compiled/_next/static/chunks/1915-d3c36ad6ce697ce7.js +1 -0
- khoj/interface/compiled/_next/static/chunks/2117-165ef4747a5b836b.js +2 -0
- khoj/interface/compiled/_next/static/chunks/2581-455000f8aeb08fc3.js +1 -0
- khoj/interface/compiled/_next/static/chunks/3727.dcea8f2193111552.js +1 -0
- khoj/interface/compiled/_next/static/chunks/3789-a09e37a819171a9d.js +1 -0
- khoj/interface/compiled/_next/static/chunks/4124-6c28322ce218d2d5.js +1 -0
- khoj/interface/compiled/_next/static/chunks/5427-b52d95253e692bfa.js +1 -0
- khoj/interface/compiled/_next/static/chunks/5473-b1cf56dedac6577a.js +1 -0
- khoj/interface/compiled/_next/static/chunks/5477-0bbddb79c25a54a7.js +1 -0
- khoj/interface/compiled/_next/static/chunks/6065-64db9ad305ba0bcd.js +1 -0
- khoj/interface/compiled/_next/static/chunks/6293-469dd16402ea8a6f.js +3 -0
- khoj/interface/compiled/_next/static/chunks/688-b5b4391bbc0376f1.js +1 -0
- khoj/interface/compiled/_next/static/chunks/8667-b6bf63c72b2d76eb.js +1 -0
- khoj/interface/compiled/_next/static/chunks/9259-1172dbaca0515237.js +1 -0
- khoj/interface/compiled/_next/static/chunks/94ca1967.1d9b42d929a1ee8c.js +1 -0
- khoj/interface/compiled/_next/static/chunks/9597.83583248dfbf6e73.js +1 -0
- khoj/interface/compiled/_next/static/chunks/964ecbae.51d6faf8801d15e6.js +1 -0
- khoj/interface/compiled/_next/static/chunks/9665-391df1e5c51c960a.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/_not-found/page-a834eddae3e235df.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/agents/layout-e00fb81dca656a10.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/agents/page-28ce086a1129bca2.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/automations/layout-1fe1537449f43496.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/automations/page-bf365a60829d347f.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/chat/layout-33934fc2d6ae6838.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/chat/page-0e476e57eb2015e3.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/layout-30e7fda7262713ce.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/page-a5515ea71aec5ef0.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/search/layout-c02531d586972d7d.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/search/page-9140541e67ea307d.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/settings/layout-d09d6510a45cd4bd.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/settings/page-951ba40b5b94b23a.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/share/chat/layout-e8e5db7830bf3f47.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/share/chat/page-1beb80d8d741c932.js +1 -0
- khoj/interface/compiled/_next/static/chunks/d3ac728e-44ebd2a0c99b12a0.js +1 -0
- khoj/interface/compiled/_next/static/chunks/fd9d1056-4482b99a36fd1673.js +1 -0
- khoj/interface/compiled/_next/static/chunks/framework-8e0e0f4a6b83a956.js +1 -0
- khoj/interface/compiled/_next/static/chunks/main-app-de1f09df97a3cfc7.js +1 -0
- khoj/interface/compiled/_next/static/chunks/main-db4bfac6b0a8d00b.js +1 -0
- khoj/interface/compiled/_next/static/chunks/pages/_app-3c9ca398d360b709.js +1 -0
- khoj/interface/compiled/_next/static/chunks/pages/_error-cf5ca766ac8f493f.js +1 -0
- khoj/interface/compiled/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- khoj/interface/compiled/_next/static/chunks/webpack-a03962458328b163.js +1 -0
- khoj/interface/compiled/_next/static/css/089de1d8526b96e9.css +1 -0
- khoj/interface/compiled/_next/static/css/37a73b87f02df402.css +1 -0
- khoj/interface/compiled/_next/static/css/4e4e6a4a1c920d06.css +1 -0
- khoj/interface/compiled/_next/static/css/8d02837c730f8d13.css +25 -0
- khoj/interface/compiled/_next/static/css/8e6a3ca11a60b189.css +1 -0
- khoj/interface/compiled/_next/static/css/9c164d9727dd8092.css +1 -0
- khoj/interface/compiled/_next/static/css/dac88c17aaee5fcf.css +1 -0
- khoj/interface/compiled/_next/static/css/df4b47a2d0d85eae.css +1 -0
- khoj/interface/compiled/_next/static/css/e4eb883b5265d372.css +1 -0
- khoj/interface/compiled/_next/static/media/1d8a05b60287ae6c-s.p.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/6f22fce21a7c433c-s.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/77c207b095007c34-s.p.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/82ef96de0e8f4d8c-s.p.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_AMS-Regular.1608a09b.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_AMS-Regular.4aafdb68.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_AMS-Regular.a79f1c31.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Caligraphic-Bold.b6770918.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Caligraphic-Bold.cce5b8ec.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Caligraphic-Bold.ec17d132.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Caligraphic-Regular.07ef19e7.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Caligraphic-Regular.55fac258.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Caligraphic-Regular.dad44a7f.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Fraktur-Bold.9f256b85.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Fraktur-Bold.b18f59e1.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Fraktur-Bold.d42a5579.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Fraktur-Regular.7c187121.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Fraktur-Regular.d3c882a6.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Fraktur-Regular.ed38e79f.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Main-Bold.b74a1a8b.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Main-Bold.c3fb5ac2.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Main-Bold.d181c465.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Main-BoldItalic.6f2bb1df.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Main-BoldItalic.70d8b0a5.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Main-BoldItalic.e3f82f9d.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Main-Italic.47373d1e.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Main-Italic.8916142b.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Main-Italic.9024d815.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Main-Regular.0462f03b.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Main-Regular.7f51fe03.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Main-Regular.b7f8fe9b.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Math-BoldItalic.572d331f.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Math-BoldItalic.a879cf83.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Math-BoldItalic.f1035d8d.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Math-Italic.5295ba48.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Math-Italic.939bc644.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Math-Italic.f28c23ac.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_SansSerif-Bold.8c5b5494.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_SansSerif-Bold.94e1e8dc.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_SansSerif-Bold.bf59d231.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_SansSerif-Italic.3b1e59b3.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_SansSerif-Italic.7c9bc82b.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_SansSerif-Italic.b4c20c84.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_SansSerif-Regular.74048478.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_SansSerif-Regular.ba21ed5f.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_SansSerif-Regular.d4d7ba48.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Script-Regular.03e9641d.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Script-Regular.07505710.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Script-Regular.fe9cbbe1.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Size1-Regular.e1e279cb.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Size1-Regular.eae34984.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Size1-Regular.fabc004a.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Size2-Regular.57727022.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Size2-Regular.5916a24f.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Size2-Regular.d6b476ec.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Size3-Regular.9acaf01c.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Size3-Regular.a144ef58.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Size3-Regular.b4230e7e.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Size4-Regular.10d95fd3.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Size4-Regular.7a996c9d.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Size4-Regular.fbccdabe.ttf +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Typewriter-Regular.6258592b.woff +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Typewriter-Regular.a8709e36.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/KaTeX_Typewriter-Regular.d97aaf4a.ttf +0 -0
- khoj/interface/compiled/_next/static/media/a6ecd16fa044d500-s.p.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/bd82c78e5b7b3fe9-s.p.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/c32c8052c071fc42-s.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/c4250770ab8708b6-s.p.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/e098aaaecc9cfbb2-s.p.woff2 +0 -0
- khoj/interface/compiled/_next/static/media/flags.3afdda2f.webp +0 -0
- khoj/interface/compiled/_next/static/media/flags@2x.5fbe9fc1.webp +0 -0
- khoj/interface/compiled/_next/static/media/globe.98e105ca.webp +0 -0
- khoj/interface/compiled/_next/static/media/globe@2x.974df6f8.webp +0 -0
- khoj/interface/compiled/agents/index.html +1 -0
- khoj/interface/compiled/agents/index.txt +7 -0
- khoj/interface/compiled/agents.svg +6 -0
- khoj/interface/compiled/assets/icons/khoj_lantern.ico +0 -0
- khoj/interface/compiled/assets/icons/khoj_lantern.svg +100 -0
- khoj/interface/compiled/assets/icons/khoj_lantern_1200x1200.png +0 -0
- khoj/interface/compiled/assets/icons/khoj_lantern_128x128.png +0 -0
- khoj/interface/compiled/assets/icons/khoj_lantern_128x128_dark.png +0 -0
- khoj/interface/compiled/assets/icons/khoj_lantern_256x256.png +0 -0
- khoj/interface/compiled/assets/icons/khoj_lantern_512x512.png +0 -0
- khoj/interface/compiled/assets/icons/khoj_lantern_logomarktype_1200x630.png +0 -0
- khoj/interface/compiled/assets/samples/desktop-browse-draw-sample.png +0 -0
- khoj/interface/compiled/assets/samples/desktop-plain-chat-sample.png +0 -0
- khoj/interface/compiled/assets/samples/desktop-remember-plan-sample.png +0 -0
- khoj/interface/compiled/assets/samples/phone-browse-draw-sample.png +0 -0
- khoj/interface/compiled/assets/samples/phone-plain-chat-sample.png +0 -0
- khoj/interface/compiled/assets/samples/phone-remember-plan-sample.png +0 -0
- khoj/interface/compiled/automation.svg +37 -0
- khoj/interface/compiled/automations/index.html +1 -0
- khoj/interface/compiled/automations/index.txt +8 -0
- khoj/interface/compiled/chat/index.html +1 -0
- khoj/interface/compiled/chat/index.txt +7 -0
- khoj/interface/compiled/chat.svg +24 -0
- khoj/interface/compiled/close.svg +5 -0
- khoj/interface/compiled/copy-button-success.svg +6 -0
- khoj/interface/compiled/copy-button.svg +5 -0
- khoj/interface/compiled/index.html +1 -0
- khoj/interface/compiled/index.txt +7 -0
- khoj/interface/compiled/khoj.webmanifest +76 -0
- khoj/interface/compiled/logo.svg +24 -0
- khoj/interface/compiled/search/index.html +1 -0
- khoj/interface/compiled/search/index.txt +7 -0
- khoj/interface/compiled/send.svg +1 -0
- khoj/interface/compiled/settings/index.html +1 -0
- khoj/interface/compiled/settings/index.txt +9 -0
- khoj/interface/compiled/share/chat/index.html +1 -0
- khoj/interface/compiled/share/chat/index.txt +7 -0
- khoj/interface/compiled/share.svg +8 -0
- khoj/interface/compiled/thumbs-down.svg +6 -0
- khoj/interface/compiled/thumbs-up.svg +6 -0
- khoj/interface/email/feedback.html +34 -0
- khoj/interface/email/magic_link.html +40 -0
- khoj/interface/email/task.html +37 -0
- khoj/interface/email/welcome.html +90 -0
- khoj/interface/web/.well-known/assetlinks.json +11 -0
- khoj/interface/web/assets/icons/agents.svg +19 -0
- khoj/interface/web/assets/icons/automation.svg +43 -0
- khoj/interface/web/assets/icons/chat.svg +24 -0
- khoj/interface/web/assets/icons/github.svg +1 -0
- khoj/interface/web/assets/icons/khoj-logo-sideways-200.png +0 -0
- khoj/interface/web/assets/icons/khoj-logo-sideways-500.png +0 -0
- khoj/interface/web/assets/icons/khoj-logo-sideways.svg +32 -0
- khoj/interface/web/assets/icons/khoj.svg +26 -0
- khoj/interface/web/assets/icons/logotype.svg +1 -0
- khoj/interface/web/assets/icons/search.svg +57 -0
- khoj/interface/web/assets/icons/sync.svg +4 -0
- khoj/interface/web/assets/khoj.css +237 -0
- khoj/interface/web/assets/utils.js +33 -0
- khoj/interface/web/base_config.html +445 -0
- khoj/interface/web/content_source_github_input.html +208 -0
- khoj/interface/web/login.html +310 -0
- khoj/interface/web/utils.html +48 -0
- khoj/main.py +249 -0
- khoj/manage.py +22 -0
- khoj/migrations/__init__.py +0 -0
- khoj/migrations/migrate_offline_chat_default_model.py +69 -0
- khoj/migrations/migrate_offline_chat_default_model_2.py +71 -0
- khoj/migrations/migrate_offline_chat_schema.py +83 -0
- khoj/migrations/migrate_offline_model.py +29 -0
- khoj/migrations/migrate_processor_config_openai.py +67 -0
- khoj/migrations/migrate_server_pg.py +132 -0
- khoj/migrations/migrate_version.py +17 -0
- khoj/processor/__init__.py +0 -0
- khoj/processor/content/__init__.py +0 -0
- khoj/processor/content/docx/__init__.py +0 -0
- khoj/processor/content/docx/docx_to_entries.py +111 -0
- khoj/processor/content/github/__init__.py +0 -0
- khoj/processor/content/github/github_to_entries.py +226 -0
- khoj/processor/content/images/__init__.py +0 -0
- khoj/processor/content/images/image_to_entries.py +117 -0
- khoj/processor/content/markdown/__init__.py +0 -0
- khoj/processor/content/markdown/markdown_to_entries.py +160 -0
- khoj/processor/content/notion/notion_to_entries.py +259 -0
- khoj/processor/content/org_mode/__init__.py +0 -0
- khoj/processor/content/org_mode/org_to_entries.py +226 -0
- khoj/processor/content/org_mode/orgnode.py +532 -0
- khoj/processor/content/pdf/__init__.py +0 -0
- khoj/processor/content/pdf/pdf_to_entries.py +119 -0
- khoj/processor/content/plaintext/__init__.py +0 -0
- khoj/processor/content/plaintext/plaintext_to_entries.py +117 -0
- khoj/processor/content/text_to_entries.py +296 -0
- khoj/processor/conversation/__init__.py +0 -0
- khoj/processor/conversation/anthropic/__init__.py +0 -0
- khoj/processor/conversation/anthropic/anthropic_chat.py +243 -0
- khoj/processor/conversation/anthropic/utils.py +217 -0
- khoj/processor/conversation/google/__init__.py +0 -0
- khoj/processor/conversation/google/gemini_chat.py +253 -0
- khoj/processor/conversation/google/utils.py +260 -0
- khoj/processor/conversation/offline/__init__.py +0 -0
- khoj/processor/conversation/offline/chat_model.py +308 -0
- khoj/processor/conversation/offline/utils.py +80 -0
- khoj/processor/conversation/offline/whisper.py +15 -0
- khoj/processor/conversation/openai/__init__.py +0 -0
- khoj/processor/conversation/openai/gpt.py +243 -0
- khoj/processor/conversation/openai/utils.py +232 -0
- khoj/processor/conversation/openai/whisper.py +13 -0
- khoj/processor/conversation/prompts.py +1188 -0
- khoj/processor/conversation/utils.py +867 -0
- khoj/processor/embeddings.py +122 -0
- khoj/processor/image/generate.py +215 -0
- khoj/processor/speech/__init__.py +0 -0
- khoj/processor/speech/text_to_speech.py +51 -0
- khoj/processor/tools/__init__.py +0 -0
- khoj/processor/tools/online_search.py +472 -0
- khoj/processor/tools/run_code.py +179 -0
- khoj/routers/__init__.py +0 -0
- khoj/routers/api.py +760 -0
- khoj/routers/api_agents.py +295 -0
- khoj/routers/api_chat.py +1273 -0
- khoj/routers/api_content.py +634 -0
- khoj/routers/api_model.py +123 -0
- khoj/routers/api_phone.py +86 -0
- khoj/routers/api_subscription.py +144 -0
- khoj/routers/auth.py +307 -0
- khoj/routers/email.py +135 -0
- khoj/routers/helpers.py +2333 -0
- khoj/routers/notion.py +85 -0
- khoj/routers/research.py +364 -0
- khoj/routers/storage.py +63 -0
- khoj/routers/twilio.py +36 -0
- khoj/routers/web_client.py +141 -0
- khoj/search_filter/__init__.py +0 -0
- khoj/search_filter/base_filter.py +15 -0
- khoj/search_filter/date_filter.py +215 -0
- khoj/search_filter/file_filter.py +32 -0
- khoj/search_filter/word_filter.py +29 -0
- khoj/search_type/__init__.py +0 -0
- khoj/search_type/text_search.py +255 -0
- khoj/utils/__init__.py +0 -0
- khoj/utils/cli.py +101 -0
- khoj/utils/config.py +81 -0
- khoj/utils/constants.py +51 -0
- khoj/utils/fs_syncer.py +252 -0
- khoj/utils/helpers.py +627 -0
- khoj/utils/initialization.py +301 -0
- khoj/utils/jsonl.py +43 -0
- khoj/utils/models.py +47 -0
- khoj/utils/rawconfig.py +208 -0
- khoj/utils/state.py +48 -0
- khoj/utils/yaml.py +47 -0
- khoj-1.33.3.dev32.dist-info/METADATA +190 -0
- khoj-1.33.3.dev32.dist-info/RECORD +393 -0
- khoj-1.33.3.dev32.dist-info/WHEEL +4 -0
- khoj-1.33.3.dev32.dist-info/entry_points.txt +2 -0
- khoj-1.33.3.dev32.dist-info/licenses/LICENSE +661 -0
khoj/routers/api_chat.py
ADDED
@@ -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)
|