khoj 1.16.1.dev15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. khoj/__init__.py +0 -0
  2. khoj/app/README.md +94 -0
  3. khoj/app/__init__.py +0 -0
  4. khoj/app/asgi.py +16 -0
  5. khoj/app/settings.py +192 -0
  6. khoj/app/urls.py +25 -0
  7. khoj/configure.py +424 -0
  8. khoj/database/__init__.py +0 -0
  9. khoj/database/adapters/__init__.py +1234 -0
  10. khoj/database/admin.py +290 -0
  11. khoj/database/apps.py +6 -0
  12. khoj/database/management/__init__.py +0 -0
  13. khoj/database/management/commands/__init__.py +0 -0
  14. khoj/database/management/commands/change_generated_images_url.py +61 -0
  15. khoj/database/management/commands/convert_images_png_to_webp.py +99 -0
  16. khoj/database/migrations/0001_khojuser.py +98 -0
  17. khoj/database/migrations/0002_googleuser.py +32 -0
  18. khoj/database/migrations/0003_vector_extension.py +10 -0
  19. khoj/database/migrations/0004_content_types_and_more.py +181 -0
  20. khoj/database/migrations/0005_embeddings_corpus_id.py +19 -0
  21. khoj/database/migrations/0006_embeddingsdates.py +33 -0
  22. khoj/database/migrations/0007_add_conversation.py +27 -0
  23. khoj/database/migrations/0008_alter_conversation_conversation_log.py +17 -0
  24. khoj/database/migrations/0009_khojapiuser.py +24 -0
  25. khoj/database/migrations/0010_chatmodeloptions_and_more.py +83 -0
  26. khoj/database/migrations/0010_rename_embeddings_entry_and_more.py +30 -0
  27. khoj/database/migrations/0011_merge_20231102_0138.py +14 -0
  28. khoj/database/migrations/0012_entry_file_source.py +21 -0
  29. khoj/database/migrations/0013_subscription.py +37 -0
  30. khoj/database/migrations/0014_alter_googleuser_picture.py +17 -0
  31. khoj/database/migrations/0015_alter_subscription_user.py +21 -0
  32. khoj/database/migrations/0016_alter_subscription_renewal_date.py +17 -0
  33. khoj/database/migrations/0017_searchmodel.py +32 -0
  34. khoj/database/migrations/0018_searchmodelconfig_delete_searchmodel.py +30 -0
  35. khoj/database/migrations/0019_alter_googleuser_family_name_and_more.py +27 -0
  36. khoj/database/migrations/0020_reflectivequestion.py +36 -0
  37. khoj/database/migrations/0021_speechtotextmodeloptions_and_more.py +42 -0
  38. khoj/database/migrations/0022_texttoimagemodelconfig.py +25 -0
  39. khoj/database/migrations/0023_usersearchmodelconfig.py +33 -0
  40. khoj/database/migrations/0024_alter_entry_embeddings.py +18 -0
  41. khoj/database/migrations/0025_clientapplication_khojuser_phone_number_and_more.py +46 -0
  42. khoj/database/migrations/0025_searchmodelconfig_embeddings_inference_endpoint_and_more.py +22 -0
  43. khoj/database/migrations/0026_searchmodelconfig_cross_encoder_inference_endpoint_and_more.py +22 -0
  44. khoj/database/migrations/0027_merge_20240118_1324.py +13 -0
  45. khoj/database/migrations/0028_khojuser_verified_phone_number.py +17 -0
  46. khoj/database/migrations/0029_userrequests.py +27 -0
  47. khoj/database/migrations/0030_conversation_slug_and_title.py +38 -0
  48. khoj/database/migrations/0031_agent_conversation_agent.py +53 -0
  49. khoj/database/migrations/0031_alter_googleuser_locale.py +30 -0
  50. khoj/database/migrations/0032_merge_20240322_0427.py +14 -0
  51. khoj/database/migrations/0033_rename_tuning_agent_personality.py +17 -0
  52. khoj/database/migrations/0034_alter_chatmodeloptions_chat_model.py +32 -0
  53. khoj/database/migrations/0035_processlock.py +26 -0
  54. khoj/database/migrations/0036_alter_processlock_name.py +19 -0
  55. khoj/database/migrations/0036_delete_offlinechatprocessorconversationconfig.py +15 -0
  56. khoj/database/migrations/0036_publicconversation.py +42 -0
  57. khoj/database/migrations/0037_chatmodeloptions_openai_config_and_more.py +51 -0
  58. khoj/database/migrations/0037_searchmodelconfig_bi_encoder_docs_encode_config_and_more.py +32 -0
  59. khoj/database/migrations/0038_merge_20240425_0857.py +14 -0
  60. khoj/database/migrations/0038_merge_20240426_1640.py +12 -0
  61. khoj/database/migrations/0039_merge_20240501_0301.py +12 -0
  62. khoj/database/migrations/0040_alter_processlock_name.py +26 -0
  63. khoj/database/migrations/0040_merge_20240504_1010.py +14 -0
  64. khoj/database/migrations/0041_merge_20240505_1234.py +14 -0
  65. khoj/database/migrations/0042_serverchatsettings.py +46 -0
  66. khoj/database/migrations/0043_alter_chatmodeloptions_model_type.py +21 -0
  67. khoj/database/migrations/0044_conversation_file_filters.py +17 -0
  68. khoj/database/migrations/0045_fileobject.py +37 -0
  69. khoj/database/migrations/0046_khojuser_email_verification_code_and_more.py +22 -0
  70. khoj/database/migrations/0047_alter_entry_file_type.py +31 -0
  71. khoj/database/migrations/0048_voicemodeloption_uservoicemodelconfig.py +52 -0
  72. khoj/database/migrations/0049_datastore.py +38 -0
  73. khoj/database/migrations/0049_texttoimagemodelconfig_api_key_and_more.py +58 -0
  74. khoj/database/migrations/0050_alter_processlock_name.py +25 -0
  75. khoj/database/migrations/0051_merge_20240702_1220.py +14 -0
  76. khoj/database/migrations/0052_alter_searchmodelconfig_bi_encoder_docs_encode_config_and_more.py +27 -0
  77. khoj/database/migrations/__init__.py +0 -0
  78. khoj/database/models/__init__.py +402 -0
  79. khoj/database/tests.py +3 -0
  80. khoj/interface/email/feedback.html +34 -0
  81. khoj/interface/email/magic_link.html +17 -0
  82. khoj/interface/email/task.html +40 -0
  83. khoj/interface/email/welcome.html +61 -0
  84. khoj/interface/web/404.html +56 -0
  85. khoj/interface/web/agent.html +312 -0
  86. khoj/interface/web/agents.html +276 -0
  87. khoj/interface/web/assets/icons/agents.svg +6 -0
  88. khoj/interface/web/assets/icons/automation.svg +37 -0
  89. khoj/interface/web/assets/icons/cancel.svg +3 -0
  90. khoj/interface/web/assets/icons/chat.svg +24 -0
  91. khoj/interface/web/assets/icons/collapse.svg +17 -0
  92. khoj/interface/web/assets/icons/computer.png +0 -0
  93. khoj/interface/web/assets/icons/confirm-icon.svg +1 -0
  94. khoj/interface/web/assets/icons/copy-button-success.svg +6 -0
  95. khoj/interface/web/assets/icons/copy-button.svg +5 -0
  96. khoj/interface/web/assets/icons/credit-card.png +0 -0
  97. khoj/interface/web/assets/icons/delete.svg +26 -0
  98. khoj/interface/web/assets/icons/docx.svg +7 -0
  99. khoj/interface/web/assets/icons/edit.svg +4 -0
  100. khoj/interface/web/assets/icons/favicon-128x128.ico +0 -0
  101. khoj/interface/web/assets/icons/favicon-128x128.png +0 -0
  102. khoj/interface/web/assets/icons/favicon-256x256.png +0 -0
  103. khoj/interface/web/assets/icons/favicon.icns +0 -0
  104. khoj/interface/web/assets/icons/github.svg +1 -0
  105. khoj/interface/web/assets/icons/key.svg +4 -0
  106. khoj/interface/web/assets/icons/khoj-logo-sideways-200.png +0 -0
  107. khoj/interface/web/assets/icons/khoj-logo-sideways-500.png +0 -0
  108. khoj/interface/web/assets/icons/khoj-logo-sideways.svg +5385 -0
  109. khoj/interface/web/assets/icons/logotype.svg +1 -0
  110. khoj/interface/web/assets/icons/markdown.svg +1 -0
  111. khoj/interface/web/assets/icons/new.svg +23 -0
  112. khoj/interface/web/assets/icons/notion.svg +4 -0
  113. khoj/interface/web/assets/icons/openai-logomark.svg +1 -0
  114. khoj/interface/web/assets/icons/org.svg +1 -0
  115. khoj/interface/web/assets/icons/pdf.svg +23 -0
  116. khoj/interface/web/assets/icons/pencil-edit.svg +5 -0
  117. khoj/interface/web/assets/icons/plaintext.svg +1 -0
  118. khoj/interface/web/assets/icons/question-mark-icon.svg +1 -0
  119. khoj/interface/web/assets/icons/search.svg +25 -0
  120. khoj/interface/web/assets/icons/send.svg +1 -0
  121. khoj/interface/web/assets/icons/share.svg +8 -0
  122. khoj/interface/web/assets/icons/speaker.svg +4 -0
  123. khoj/interface/web/assets/icons/stop-solid.svg +37 -0
  124. khoj/interface/web/assets/icons/sync.svg +4 -0
  125. khoj/interface/web/assets/icons/thumbs-down-svgrepo-com.svg +6 -0
  126. khoj/interface/web/assets/icons/thumbs-up-svgrepo-com.svg +6 -0
  127. khoj/interface/web/assets/icons/user-silhouette.svg +4 -0
  128. khoj/interface/web/assets/icons/voice.svg +8 -0
  129. khoj/interface/web/assets/icons/web.svg +2 -0
  130. khoj/interface/web/assets/icons/whatsapp.svg +17 -0
  131. khoj/interface/web/assets/khoj.css +237 -0
  132. khoj/interface/web/assets/markdown-it.min.js +8476 -0
  133. khoj/interface/web/assets/natural-cron.min.js +1 -0
  134. khoj/interface/web/assets/org.min.js +1823 -0
  135. khoj/interface/web/assets/pico.min.css +5 -0
  136. khoj/interface/web/assets/purify.min.js +3 -0
  137. khoj/interface/web/assets/samples/desktop-browse-draw-sample.png +0 -0
  138. khoj/interface/web/assets/samples/desktop-plain-chat-sample.png +0 -0
  139. khoj/interface/web/assets/samples/desktop-remember-plan-sample.png +0 -0
  140. khoj/interface/web/assets/samples/phone-browse-draw-sample.png +0 -0
  141. khoj/interface/web/assets/samples/phone-plain-chat-sample.png +0 -0
  142. khoj/interface/web/assets/samples/phone-remember-plan-sample.png +0 -0
  143. khoj/interface/web/assets/utils.js +33 -0
  144. khoj/interface/web/base_config.html +445 -0
  145. khoj/interface/web/chat.html +3546 -0
  146. khoj/interface/web/config.html +1011 -0
  147. khoj/interface/web/config_automation.html +1103 -0
  148. khoj/interface/web/content_source_computer_input.html +139 -0
  149. khoj/interface/web/content_source_github_input.html +216 -0
  150. khoj/interface/web/content_source_notion_input.html +94 -0
  151. khoj/interface/web/khoj.webmanifest +51 -0
  152. khoj/interface/web/login.html +219 -0
  153. khoj/interface/web/public_conversation.html +2006 -0
  154. khoj/interface/web/search.html +470 -0
  155. khoj/interface/web/utils.html +48 -0
  156. khoj/main.py +241 -0
  157. khoj/manage.py +22 -0
  158. khoj/migrations/__init__.py +0 -0
  159. khoj/migrations/migrate_offline_chat_default_model.py +69 -0
  160. khoj/migrations/migrate_offline_chat_default_model_2.py +71 -0
  161. khoj/migrations/migrate_offline_chat_schema.py +83 -0
  162. khoj/migrations/migrate_offline_model.py +29 -0
  163. khoj/migrations/migrate_processor_config_openai.py +67 -0
  164. khoj/migrations/migrate_server_pg.py +138 -0
  165. khoj/migrations/migrate_version.py +17 -0
  166. khoj/processor/__init__.py +0 -0
  167. khoj/processor/content/__init__.py +0 -0
  168. khoj/processor/content/docx/__init__.py +0 -0
  169. khoj/processor/content/docx/docx_to_entries.py +110 -0
  170. khoj/processor/content/github/__init__.py +0 -0
  171. khoj/processor/content/github/github_to_entries.py +224 -0
  172. khoj/processor/content/images/__init__.py +0 -0
  173. khoj/processor/content/images/image_to_entries.py +118 -0
  174. khoj/processor/content/markdown/__init__.py +0 -0
  175. khoj/processor/content/markdown/markdown_to_entries.py +165 -0
  176. khoj/processor/content/notion/notion_to_entries.py +260 -0
  177. khoj/processor/content/org_mode/__init__.py +0 -0
  178. khoj/processor/content/org_mode/org_to_entries.py +231 -0
  179. khoj/processor/content/org_mode/orgnode.py +532 -0
  180. khoj/processor/content/pdf/__init__.py +0 -0
  181. khoj/processor/content/pdf/pdf_to_entries.py +116 -0
  182. khoj/processor/content/plaintext/__init__.py +0 -0
  183. khoj/processor/content/plaintext/plaintext_to_entries.py +122 -0
  184. khoj/processor/content/text_to_entries.py +297 -0
  185. khoj/processor/conversation/__init__.py +0 -0
  186. khoj/processor/conversation/anthropic/__init__.py +0 -0
  187. khoj/processor/conversation/anthropic/anthropic_chat.py +206 -0
  188. khoj/processor/conversation/anthropic/utils.py +114 -0
  189. khoj/processor/conversation/offline/__init__.py +0 -0
  190. khoj/processor/conversation/offline/chat_model.py +231 -0
  191. khoj/processor/conversation/offline/utils.py +78 -0
  192. khoj/processor/conversation/offline/whisper.py +15 -0
  193. khoj/processor/conversation/openai/__init__.py +0 -0
  194. khoj/processor/conversation/openai/gpt.py +187 -0
  195. khoj/processor/conversation/openai/utils.py +129 -0
  196. khoj/processor/conversation/openai/whisper.py +13 -0
  197. khoj/processor/conversation/prompts.py +758 -0
  198. khoj/processor/conversation/utils.py +262 -0
  199. khoj/processor/embeddings.py +117 -0
  200. khoj/processor/speech/__init__.py +0 -0
  201. khoj/processor/speech/text_to_speech.py +51 -0
  202. khoj/processor/tools/__init__.py +0 -0
  203. khoj/processor/tools/online_search.py +225 -0
  204. khoj/routers/__init__.py +0 -0
  205. khoj/routers/api.py +626 -0
  206. khoj/routers/api_agents.py +43 -0
  207. khoj/routers/api_chat.py +1180 -0
  208. khoj/routers/api_config.py +434 -0
  209. khoj/routers/api_phone.py +86 -0
  210. khoj/routers/auth.py +181 -0
  211. khoj/routers/email.py +133 -0
  212. khoj/routers/helpers.py +1188 -0
  213. khoj/routers/indexer.py +349 -0
  214. khoj/routers/notion.py +91 -0
  215. khoj/routers/storage.py +35 -0
  216. khoj/routers/subscription.py +104 -0
  217. khoj/routers/twilio.py +36 -0
  218. khoj/routers/web_client.py +471 -0
  219. khoj/search_filter/__init__.py +0 -0
  220. khoj/search_filter/base_filter.py +15 -0
  221. khoj/search_filter/date_filter.py +217 -0
  222. khoj/search_filter/file_filter.py +30 -0
  223. khoj/search_filter/word_filter.py +29 -0
  224. khoj/search_type/__init__.py +0 -0
  225. khoj/search_type/text_search.py +241 -0
  226. khoj/utils/__init__.py +0 -0
  227. khoj/utils/cli.py +93 -0
  228. khoj/utils/config.py +81 -0
  229. khoj/utils/constants.py +24 -0
  230. khoj/utils/fs_syncer.py +249 -0
  231. khoj/utils/helpers.py +418 -0
  232. khoj/utils/initialization.py +146 -0
  233. khoj/utils/jsonl.py +43 -0
  234. khoj/utils/models.py +47 -0
  235. khoj/utils/rawconfig.py +160 -0
  236. khoj/utils/state.py +46 -0
  237. khoj/utils/yaml.py +43 -0
  238. khoj-1.16.1.dev15.dist-info/METADATA +178 -0
  239. khoj-1.16.1.dev15.dist-info/RECORD +242 -0
  240. khoj-1.16.1.dev15.dist-info/WHEEL +4 -0
  241. khoj-1.16.1.dev15.dist-info/entry_points.txt +2 -0
  242. khoj-1.16.1.dev15.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,434 @@
1
+ import json
2
+ import logging
3
+ import math
4
+ from typing import Dict, List, Optional, Union
5
+
6
+ from asgiref.sync import sync_to_async
7
+ from fastapi import APIRouter, HTTPException, Request
8
+ from fastapi.requests import Request
9
+ from fastapi.responses import Response
10
+ from starlette.authentication import has_required_scope, requires
11
+
12
+ from khoj.database import adapters
13
+ from khoj.database.adapters import ConversationAdapters, EntryAdapters
14
+ from khoj.database.models import Entry as DbEntry
15
+ from khoj.database.models import (
16
+ GithubConfig,
17
+ KhojUser,
18
+ LocalMarkdownConfig,
19
+ LocalOrgConfig,
20
+ LocalPdfConfig,
21
+ LocalPlaintextConfig,
22
+ NotionConfig,
23
+ Subscription,
24
+ )
25
+ from khoj.routers.helpers import CommonQueryParams, update_telemetry_state
26
+ from khoj.utils import constants, state
27
+ from khoj.utils.rawconfig import (
28
+ FullConfig,
29
+ GithubContentConfig,
30
+ NotionContentConfig,
31
+ SearchConfig,
32
+ )
33
+ from khoj.utils.state import SearchType
34
+
35
+ api_config = APIRouter()
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ def map_config_to_object(content_source: str):
40
+ if content_source == DbEntry.EntrySource.GITHUB:
41
+ return GithubConfig
42
+ if content_source == DbEntry.EntrySource.NOTION:
43
+ return NotionConfig
44
+ if content_source == DbEntry.EntrySource.COMPUTER:
45
+ return "Computer"
46
+
47
+
48
+ async def map_config_to_db(config: FullConfig, user: KhojUser):
49
+ if config.content_type:
50
+ if config.content_type.org:
51
+ await LocalOrgConfig.objects.filter(user=user).adelete()
52
+ await LocalOrgConfig.objects.acreate(
53
+ input_files=config.content_type.org.input_files,
54
+ input_filter=config.content_type.org.input_filter,
55
+ index_heading_entries=config.content_type.org.index_heading_entries,
56
+ user=user,
57
+ )
58
+ if config.content_type.markdown:
59
+ await LocalMarkdownConfig.objects.filter(user=user).adelete()
60
+ await LocalMarkdownConfig.objects.acreate(
61
+ input_files=config.content_type.markdown.input_files,
62
+ input_filter=config.content_type.markdown.input_filter,
63
+ index_heading_entries=config.content_type.markdown.index_heading_entries,
64
+ user=user,
65
+ )
66
+ if config.content_type.pdf:
67
+ await LocalPdfConfig.objects.filter(user=user).adelete()
68
+ await LocalPdfConfig.objects.acreate(
69
+ input_files=config.content_type.pdf.input_files,
70
+ input_filter=config.content_type.pdf.input_filter,
71
+ index_heading_entries=config.content_type.pdf.index_heading_entries,
72
+ user=user,
73
+ )
74
+ if config.content_type.plaintext:
75
+ await LocalPlaintextConfig.objects.filter(user=user).adelete()
76
+ await LocalPlaintextConfig.objects.acreate(
77
+ input_files=config.content_type.plaintext.input_files,
78
+ input_filter=config.content_type.plaintext.input_filter,
79
+ index_heading_entries=config.content_type.plaintext.index_heading_entries,
80
+ user=user,
81
+ )
82
+ if config.content_type.github:
83
+ await adapters.set_user_github_config(
84
+ user=user,
85
+ pat_token=config.content_type.github.pat_token,
86
+ repos=config.content_type.github.repos,
87
+ )
88
+ if config.content_type.notion:
89
+ await adapters.set_notion_config(
90
+ user=user,
91
+ token=config.content_type.notion.token,
92
+ )
93
+
94
+
95
+ def _initialize_config():
96
+ if state.config is None:
97
+ state.config = FullConfig()
98
+ state.config.search_type = SearchConfig.model_validate(constants.default_config["search-type"])
99
+
100
+
101
+ @api_config.post("/data/content-source/github", status_code=200)
102
+ @requires(["authenticated"])
103
+ async def set_content_config_github_data(
104
+ request: Request,
105
+ updated_config: Union[GithubContentConfig, None],
106
+ client: Optional[str] = None,
107
+ ):
108
+ _initialize_config()
109
+
110
+ user = request.user.object
111
+
112
+ try:
113
+ await adapters.set_user_github_config(
114
+ user=user,
115
+ pat_token=updated_config.pat_token,
116
+ repos=updated_config.repos,
117
+ )
118
+ except Exception as e:
119
+ logger.error(e, exc_info=True)
120
+ raise HTTPException(status_code=500, detail="Failed to set Github config")
121
+
122
+ update_telemetry_state(
123
+ request=request,
124
+ telemetry_type="api",
125
+ api="set_content_config",
126
+ client=client,
127
+ metadata={"content_type": "github"},
128
+ )
129
+
130
+ return {"status": "ok"}
131
+
132
+
133
+ @api_config.post("/data/content-source/notion", status_code=200)
134
+ @requires(["authenticated"])
135
+ async def set_content_config_notion_data(
136
+ request: Request,
137
+ updated_config: Union[NotionContentConfig, None],
138
+ client: Optional[str] = None,
139
+ ):
140
+ _initialize_config()
141
+
142
+ user = request.user.object
143
+
144
+ try:
145
+ await adapters.set_notion_config(
146
+ user=user,
147
+ token=updated_config.token,
148
+ )
149
+ except Exception as e:
150
+ logger.error(e, exc_info=True)
151
+ raise HTTPException(status_code=500, detail="Failed to set Github config")
152
+
153
+ update_telemetry_state(
154
+ request=request,
155
+ telemetry_type="api",
156
+ api="set_content_config",
157
+ client=client,
158
+ metadata={"content_type": "notion"},
159
+ )
160
+
161
+ return {"status": "ok"}
162
+
163
+
164
+ @api_config.delete("/data/content-source/{content_source}", status_code=200)
165
+ @requires(["authenticated"])
166
+ async def remove_content_source_data(
167
+ request: Request,
168
+ content_source: str,
169
+ client: Optional[str] = None,
170
+ ):
171
+ user = request.user.object
172
+
173
+ update_telemetry_state(
174
+ request=request,
175
+ telemetry_type="api",
176
+ api="delete_content_config",
177
+ client=client,
178
+ metadata={"content_source": content_source},
179
+ )
180
+
181
+ content_object = map_config_to_object(content_source)
182
+ if content_object is None:
183
+ raise ValueError(f"Invalid content source: {content_source}")
184
+ elif content_object != "Computer":
185
+ await content_object.objects.filter(user=user).adelete()
186
+ await sync_to_async(EntryAdapters.delete_all_entries)(user, file_source=content_source)
187
+
188
+ enabled_content = await sync_to_async(EntryAdapters.get_unique_file_types)(user)
189
+ return {"status": "ok"}
190
+
191
+
192
+ @api_config.delete("/data/file", status_code=200)
193
+ @requires(["authenticated"])
194
+ async def remove_file_data(
195
+ request: Request,
196
+ filename: str,
197
+ client: Optional[str] = None,
198
+ ):
199
+ user = request.user.object
200
+
201
+ update_telemetry_state(
202
+ request=request,
203
+ telemetry_type="api",
204
+ api="delete_file",
205
+ client=client,
206
+ )
207
+
208
+ await EntryAdapters.adelete_entry_by_file(user, filename)
209
+
210
+ return {"status": "ok"}
211
+
212
+
213
+ @api_config.get("/data/{content_source}", response_model=List[str])
214
+ @requires(["authenticated"])
215
+ async def get_all_filenames(
216
+ request: Request,
217
+ content_source: str,
218
+ client: Optional[str] = None,
219
+ ):
220
+ user = request.user.object
221
+
222
+ update_telemetry_state(
223
+ request=request,
224
+ telemetry_type="api",
225
+ api="get_all_filenames",
226
+ client=client,
227
+ )
228
+
229
+ return await sync_to_async(list)(EntryAdapters.get_all_filenames_by_source(user, content_source)) # type: ignore[call-arg]
230
+
231
+
232
+ @api_config.get("/data/conversation/model/options", response_model=Dict[str, Union[str, int]])
233
+ def get_chat_model_options(
234
+ request: Request,
235
+ client: Optional[str] = None,
236
+ ):
237
+ conversation_options = ConversationAdapters.get_conversation_processor_options().all()
238
+
239
+ all_conversation_options = list()
240
+ for conversation_option in conversation_options:
241
+ all_conversation_options.append({"chat_model": conversation_option.chat_model, "id": conversation_option.id})
242
+
243
+ return Response(content=json.dumps(all_conversation_options), media_type="application/json", status_code=200)
244
+
245
+
246
+ @api_config.get("/data/conversation/model")
247
+ @requires(["authenticated"])
248
+ def get_user_chat_model(
249
+ request: Request,
250
+ client: Optional[str] = None,
251
+ ):
252
+ user = request.user.object
253
+
254
+ chat_model = ConversationAdapters.get_conversation_config(user)
255
+
256
+ if chat_model is None:
257
+ chat_model = ConversationAdapters.get_default_conversation_config()
258
+
259
+ return Response(status_code=200, content=json.dumps({"id": chat_model.id, "chat_model": chat_model.chat_model}))
260
+
261
+
262
+ @api_config.post("/data/conversation/model", status_code=200)
263
+ @requires(["authenticated", "premium"])
264
+ async def update_chat_model(
265
+ request: Request,
266
+ id: str,
267
+ client: Optional[str] = None,
268
+ ):
269
+ user = request.user.object
270
+
271
+ new_config = await ConversationAdapters.aset_user_conversation_processor(user, int(id))
272
+
273
+ update_telemetry_state(
274
+ request=request,
275
+ telemetry_type="api",
276
+ api="set_conversation_chat_model",
277
+ client=client,
278
+ metadata={"processor_conversation_type": "conversation"},
279
+ )
280
+
281
+ if new_config is None:
282
+ return {"status": "error", "message": "Model not found"}
283
+
284
+ return {"status": "ok"}
285
+
286
+
287
+ @api_config.post("/data/voice/model", status_code=200)
288
+ @requires(["authenticated", "premium"])
289
+ async def update_voice_model(
290
+ request: Request,
291
+ id: str,
292
+ client: Optional[str] = None,
293
+ ):
294
+ user = request.user.object
295
+
296
+ new_config = await ConversationAdapters.aset_user_voice_model(user, id)
297
+
298
+ update_telemetry_state(
299
+ request=request,
300
+ telemetry_type="api",
301
+ api="set_voice_model",
302
+ client=client,
303
+ )
304
+
305
+ if new_config is None:
306
+ return Response(status_code=404, content=json.dumps({"status": "error", "message": "Model not found"}))
307
+
308
+ return Response(status_code=202, content=json.dumps({"status": "ok"}))
309
+
310
+
311
+ @api_config.post("/data/search/model", status_code=200)
312
+ @requires(["authenticated"])
313
+ async def update_search_model(
314
+ request: Request,
315
+ id: str,
316
+ client: Optional[str] = None,
317
+ ):
318
+ user = request.user.object
319
+
320
+ prev_config = await adapters.aget_user_search_model(user)
321
+ new_config = await adapters.aset_user_search_model(user, int(id))
322
+
323
+ if prev_config and int(id) != prev_config.id and new_config:
324
+ await EntryAdapters.adelete_all_entries(user)
325
+
326
+ if not prev_config:
327
+ # If the use was just using the default config, delete all the entries and set the new config.
328
+ await EntryAdapters.adelete_all_entries(user)
329
+
330
+ if new_config is None:
331
+ return {"status": "error", "message": "Model not found"}
332
+ else:
333
+ update_telemetry_state(
334
+ request=request,
335
+ telemetry_type="api",
336
+ api="set_search_model",
337
+ client=client,
338
+ metadata={"search_model": new_config.setting.name},
339
+ )
340
+
341
+ return {"status": "ok"}
342
+
343
+
344
+ @api_config.post("/data/paint/model", status_code=200)
345
+ @requires(["authenticated"])
346
+ async def update_paint_model(
347
+ request: Request,
348
+ id: str,
349
+ client: Optional[str] = None,
350
+ ):
351
+ user = request.user.object
352
+ subscribed = has_required_scope(request, ["premium"])
353
+
354
+ if not subscribed:
355
+ raise HTTPException(status_code=403, detail="User is not subscribed to premium")
356
+
357
+ new_config = await ConversationAdapters.aset_user_text_to_image_model(user, int(id))
358
+
359
+ update_telemetry_state(
360
+ request=request,
361
+ telemetry_type="api",
362
+ api="set_paint_model",
363
+ client=client,
364
+ metadata={"paint_model": new_config.setting.model_name},
365
+ )
366
+
367
+ if new_config is None:
368
+ return {"status": "error", "message": "Model not found"}
369
+
370
+ return {"status": "ok"}
371
+
372
+
373
+ @api_config.get("/index/size", response_model=Dict[str, int])
374
+ @requires(["authenticated"])
375
+ async def get_indexed_data_size(request: Request, common: CommonQueryParams):
376
+ user = request.user.object
377
+ indexed_data_size_in_mb = await sync_to_async(EntryAdapters.get_size_of_indexed_data_in_mb)(user)
378
+ return Response(
379
+ content=json.dumps({"indexed_data_size_in_mb": math.ceil(indexed_data_size_in_mb)}),
380
+ media_type="application/json",
381
+ status_code=200,
382
+ )
383
+
384
+
385
+ @api_config.post("/user/name", status_code=200)
386
+ @requires(["authenticated"])
387
+ def set_user_name(
388
+ request: Request,
389
+ name: str,
390
+ client: Optional[str] = None,
391
+ ):
392
+ user = request.user.object
393
+
394
+ split_name = name.split(" ")
395
+
396
+ if len(split_name) > 2:
397
+ raise HTTPException(status_code=400, detail="Name must be in the format: Firstname Lastname")
398
+
399
+ if len(split_name) == 1:
400
+ first_name = split_name[0]
401
+ last_name = ""
402
+ else:
403
+ first_name, last_name = split_name[0], split_name[-1]
404
+
405
+ adapters.set_user_name(user, first_name, last_name)
406
+
407
+ update_telemetry_state(
408
+ request=request,
409
+ telemetry_type="api",
410
+ api="set_user_name",
411
+ client=client,
412
+ )
413
+
414
+ return {"status": "ok"}
415
+
416
+
417
+ @api_config.get("/types", response_model=List[str])
418
+ @requires(["authenticated"])
419
+ def get_config_types(
420
+ request: Request,
421
+ ):
422
+ user = request.user.object
423
+ enabled_file_types = EntryAdapters.get_unique_file_types(user)
424
+ configured_content_types = list(enabled_file_types)
425
+
426
+ if state.config and state.config.content_type:
427
+ for ctype in state.config.content_type.model_dump(exclude_none=True):
428
+ configured_content_types.append(ctype)
429
+
430
+ return [
431
+ search_type.value
432
+ for search_type in SearchType
433
+ if (search_type.value in configured_content_types) or search_type == SearchType.All
434
+ ]
@@ -0,0 +1,86 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ from fastapi import APIRouter, Depends, HTTPException, Request
5
+ from starlette.authentication import requires
6
+
7
+ from khoj.database import adapters
8
+ from khoj.database.models import KhojUser
9
+ from khoj.routers.helpers import ApiUserRateLimiter, update_telemetry_state
10
+ from khoj.routers.twilio import create_otp, verify_otp
11
+
12
+ api_phone = APIRouter()
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @api_phone.post("", status_code=200)
17
+ @requires(["authenticated"])
18
+ async def update_phone_number(
19
+ request: Request,
20
+ phone_number: str,
21
+ client: Optional[str] = None,
22
+ rate_limiter_per_day=Depends(
23
+ ApiUserRateLimiter(requests=5, subscribed_requests=5, window=60 * 60 * 24, slug="update_phone")
24
+ ),
25
+ ):
26
+ user = request.user.object
27
+
28
+ await adapters.aset_user_phone_number(user, phone_number)
29
+ create_otp(user)
30
+
31
+ update_telemetry_state(
32
+ request=request,
33
+ telemetry_type="api",
34
+ api="set_phone_number",
35
+ client=client,
36
+ metadata={"phone_number": phone_number},
37
+ )
38
+
39
+ return {"status": "ok"}
40
+
41
+
42
+ @api_phone.delete("", status_code=200)
43
+ @requires(["authenticated"])
44
+ async def delete_phone_number(
45
+ request: Request,
46
+ client: Optional[str] = None,
47
+ ):
48
+ user = request.user.object
49
+
50
+ await adapters.aremove_phone_number(user)
51
+
52
+ update_telemetry_state(
53
+ request=request,
54
+ telemetry_type="api",
55
+ api="delete_phone_number",
56
+ client=client,
57
+ )
58
+
59
+ return {"status": "ok"}
60
+
61
+
62
+ @api_phone.post("/verify", status_code=200)
63
+ @requires(["authenticated"])
64
+ async def verify_mobile_otp(
65
+ request: Request,
66
+ code: str,
67
+ client: Optional[str] = None,
68
+ rate_limiter_per_day=Depends(
69
+ ApiUserRateLimiter(requests=5, subscribed_requests=5, window=60 * 60 * 24, slug="verify_phone")
70
+ ),
71
+ ):
72
+ user: KhojUser = request.user.object
73
+
74
+ update_telemetry_state(
75
+ request=request,
76
+ telemetry_type="api",
77
+ api="verify_phone_number",
78
+ client=client,
79
+ )
80
+
81
+ if not verify_otp(user, code):
82
+ raise HTTPException(status_code=400, detail="Invalid OTP")
83
+
84
+ user.verified_phone_number = True
85
+ await user.asave()
86
+ return {"status": "ok"}
khoj/routers/auth.py ADDED
@@ -0,0 +1,181 @@
1
+ import asyncio
2
+ import datetime
3
+ import logging
4
+ import os
5
+ from typing import Optional
6
+
7
+ from fastapi import APIRouter
8
+ from pydantic import BaseModel, EmailStr
9
+ from starlette.authentication import requires
10
+ from starlette.config import Config
11
+ from starlette.requests import Request
12
+ from starlette.responses import HTMLResponse, RedirectResponse, Response
13
+ from starlette.status import HTTP_302_FOUND
14
+
15
+ from khoj.database.adapters import (
16
+ acreate_khoj_token,
17
+ aget_or_create_user_by_email,
18
+ aget_user_validated_by_email_verification_code,
19
+ delete_khoj_token,
20
+ get_khoj_tokens,
21
+ get_or_create_user,
22
+ )
23
+ from khoj.routers.email import send_magic_link_email, send_welcome_email
24
+ from khoj.routers.helpers import get_next_url, update_telemetry_state
25
+ from khoj.utils import state
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ auth_router = APIRouter()
30
+
31
+
32
+ class MagicLinkForm(BaseModel):
33
+ email: EmailStr
34
+
35
+
36
+ if not state.anonymous_mode:
37
+ missing_requirements = []
38
+ from authlib.integrations.starlette_client import OAuth, OAuthError
39
+
40
+ try:
41
+ from google.auth.transport import requests as google_requests
42
+ from google.oauth2 import id_token
43
+ except ImportError:
44
+ missing_requirements += ["Install the Khoj production package with `pip install khoj[prod]`"]
45
+ if not os.environ.get("RESEND_API_KEY") and (
46
+ not os.environ.get("GOOGLE_CLIENT_ID") or not os.environ.get("GOOGLE_CLIENT_SECRET")
47
+ ):
48
+ missing_requirements += [
49
+ "Set your RESEND_API_KEY or GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET as environment variables"
50
+ ]
51
+ if missing_requirements:
52
+ requirements_string = "\n - " + "\n - ".join(missing_requirements)
53
+ error_msg = f"🚨 Start Khoj with --anonymous-mode flag or to enable authentication:{requirements_string}"
54
+ logger.error(error_msg)
55
+
56
+ config = Config(environ=os.environ)
57
+
58
+ oauth = OAuth(config)
59
+
60
+ CONF_URL = "https://accounts.google.com/.well-known/openid-configuration"
61
+ oauth.register(name="google", server_metadata_url=CONF_URL, client_kwargs={"scope": "openid email profile"})
62
+
63
+
64
+ @auth_router.get("/login")
65
+ async def login_get(request: Request):
66
+ redirect_uri = str(request.app.url_path_for("auth"))
67
+ return await oauth.google.authorize_redirect(request, redirect_uri)
68
+
69
+
70
+ @auth_router.post("/login")
71
+ async def login(request: Request):
72
+ redirect_uri = str(request.app.url_path_for("auth"))
73
+ return await oauth.google.authorize_redirect(request, redirect_uri)
74
+
75
+
76
+ @auth_router.post("/magic")
77
+ async def login_magic_link(request: Request, form: MagicLinkForm):
78
+ if request.user.is_authenticated:
79
+ # Clear the session if user is already authenticated
80
+ request.session.pop("user", None)
81
+
82
+ email = form.email
83
+ user = await aget_or_create_user_by_email(email)
84
+ unique_id = user.email_verification_code
85
+
86
+ if user:
87
+ await send_magic_link_email(email, unique_id, request.base_url)
88
+
89
+ return Response(status_code=200)
90
+
91
+
92
+ @auth_router.get("/magic")
93
+ async def sign_in_with_magic_link(request: Request, code: str):
94
+ user = await aget_user_validated_by_email_verification_code(code)
95
+ if user:
96
+ id_info = {
97
+ "email": user.email,
98
+ }
99
+
100
+ request.session["user"] = dict(id_info)
101
+ return RedirectResponse(url="/")
102
+ return RedirectResponse(request.app.url_path_for("login_page"))
103
+
104
+
105
+ @auth_router.post("/token")
106
+ @requires(["authenticated"], redirect="login_page")
107
+ async def generate_token(request: Request, token_name: Optional[str] = None):
108
+ "Generate API token for given user"
109
+ if token_name:
110
+ token = await acreate_khoj_token(user=request.user.object, name=token_name)
111
+ else:
112
+ token = await acreate_khoj_token(user=request.user.object)
113
+ return {
114
+ "token": token.token,
115
+ "name": token.name,
116
+ }
117
+
118
+
119
+ @auth_router.get("/token")
120
+ @requires(["authenticated"], redirect="login_page")
121
+ def get_tokens(request: Request):
122
+ "Get API tokens enabled for given user"
123
+ tokens = get_khoj_tokens(user=request.user.object)
124
+ return tokens
125
+
126
+
127
+ @auth_router.delete("/token")
128
+ @requires(["authenticated"], redirect="login_page")
129
+ async def delete_token(request: Request, token: str):
130
+ "Delete API token for given user"
131
+ return await delete_khoj_token(user=request.user.object, token=token)
132
+
133
+
134
+ @auth_router.post("/redirect")
135
+ async def auth(request: Request):
136
+ form = await request.form()
137
+ next_url = get_next_url(request)
138
+ for q in request.query_params:
139
+ if not q == "next":
140
+ next_url += f"&{q}={request.query_params[q]}"
141
+
142
+ credential = form.get("credential")
143
+
144
+ csrf_token_cookie = request.cookies.get("g_csrf_token")
145
+ if not csrf_token_cookie:
146
+ logger.info("Missing CSRF token. Redirecting user to login page")
147
+ return RedirectResponse(url=next_url)
148
+ csrf_token_body = form.get("g_csrf_token")
149
+ if not csrf_token_body:
150
+ logger.info("Missing CSRF token body. Redirecting user to login page")
151
+ return RedirectResponse(url=next_url)
152
+ if csrf_token_cookie != csrf_token_body:
153
+ return Response("Invalid CSRF token", status_code=400)
154
+
155
+ try:
156
+ idinfo = id_token.verify_oauth2_token(credential, google_requests.Request(), os.environ["GOOGLE_CLIENT_ID"])
157
+ except OAuthError as error:
158
+ return HTMLResponse(f"<h1>{error.error}</h1>")
159
+ khoj_user = await get_or_create_user(idinfo)
160
+
161
+ if khoj_user:
162
+ request.session["user"] = dict(idinfo)
163
+
164
+ if datetime.timedelta(minutes=3) > (datetime.datetime.now(datetime.timezone.utc) - khoj_user.date_joined):
165
+ asyncio.create_task(send_welcome_email(idinfo["name"], idinfo["email"]))
166
+ update_telemetry_state(
167
+ request=request,
168
+ telemetry_type="api",
169
+ api="create_user",
170
+ metadata={"user_id": str(khoj_user.uuid)},
171
+ )
172
+ logger.log(logging.INFO, f"🥳 New User Created: {khoj_user.uuid}")
173
+ return RedirectResponse(url=next_url, status_code=HTTP_302_FOUND)
174
+
175
+ return RedirectResponse(url=next_url, status_code=HTTP_302_FOUND)
176
+
177
+
178
+ @auth_router.get("/logout")
179
+ async def logout(request: Request):
180
+ request.session.pop("user", None)
181
+ return RedirectResponse(url="/")