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