khoj 1.24.2.dev3__py3-none-any.whl → 1.25.1.dev34__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/configure.py +13 -4
- khoj/database/adapters/__init__.py +289 -52
- khoj/database/admin.py +20 -1
- 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/models/__init__.py +136 -18
- khoj/interface/compiled/404/index.html +1 -1
- khoj/interface/compiled/_next/static/chunks/1603-fa3ee48860b9dc5c.js +1 -0
- khoj/interface/compiled/_next/static/chunks/2697-a38d01981ad3bdf8.js +1 -0
- khoj/interface/compiled/_next/static/chunks/3110-ef2cacd1b8d79ad8.js +1 -0
- khoj/interface/compiled/_next/static/chunks/4086-2c74808ba38a5a0f.js +1 -0
- khoj/interface/compiled/_next/static/chunks/477-ec86e93db10571c1.js +1 -0
- khoj/interface/compiled/_next/static/chunks/51-e8f5bdb69b5ea421.js +1 -0
- khoj/interface/compiled/_next/static/chunks/7762-79f2205740622b5c.js +1 -0
- khoj/interface/compiled/_next/static/chunks/9178-899fe9a6b754ecfe.js +1 -0
- khoj/interface/compiled/_next/static/chunks/9417-29502e39c3e7d60c.js +1 -0
- khoj/interface/compiled/_next/static/chunks/9479-7eed36fc954ef804.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/agents/{layout-e71c8e913cccf792.js → layout-75636ab3a413fa8e.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/agents/page-fa282831808ee536.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/automations/page-5480731341f34450.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/chat/{layout-8102549127db3067.js → layout-96fcf62857bf8f30.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/page-702057ccbcf27881.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/factchecker/page-e7b34316ec6f44de.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/{layout-f3e40d346da53112.js → layout-d0f0a9067427fb20.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/page-10a5aad6e04f3cf8.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/search/page-d56541c746fded7d.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/settings/{layout-6f9314b0d7a26046.js → layout-a8f33dfe92f997fb.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/settings/page-e044a999468a7c5d.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/share/chat/{layout-39f03f9e32399f0f.js → layout-2df56074e42adaa0.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/share/chat/page-fbbd66a4d4633438.js +1 -0
- khoj/interface/compiled/_next/static/chunks/{webpack-d4781cada9b58e75.js → webpack-c0cd5a6afb1f0798.js} +1 -1
- khoj/interface/compiled/_next/static/css/2de69f0be774c768.css +1 -0
- khoj/interface/compiled/_next/static/css/467a524c75e7d7c0.css +1 -0
- khoj/interface/compiled/_next/static/css/592ca99f5122e75a.css +1 -0
- khoj/interface/compiled/_next/static/css/b9a6bf04305d98d7.css +25 -0
- khoj/interface/compiled/agents/index.html +1 -1
- khoj/interface/compiled/agents/index.txt +2 -2
- khoj/interface/compiled/automations/index.html +1 -1
- khoj/interface/compiled/automations/index.txt +2 -2
- khoj/interface/compiled/chat/index.html +1 -1
- khoj/interface/compiled/chat/index.txt +2 -2
- khoj/interface/compiled/factchecker/index.html +1 -1
- khoj/interface/compiled/factchecker/index.txt +2 -2
- khoj/interface/compiled/index.html +1 -1
- khoj/interface/compiled/index.txt +2 -2
- khoj/interface/compiled/search/index.html +1 -1
- khoj/interface/compiled/search/index.txt +2 -2
- khoj/interface/compiled/settings/index.html +1 -1
- khoj/interface/compiled/settings/index.txt +3 -3
- khoj/interface/compiled/share/chat/index.html +1 -1
- khoj/interface/compiled/share/chat/index.txt +2 -2
- khoj/interface/web/assets/icons/agents.svg +1 -0
- khoj/interface/web/assets/icons/automation.svg +1 -0
- khoj/interface/web/assets/icons/chat.svg +24 -0
- khoj/interface/web/login.html +11 -22
- khoj/processor/content/notion/notion_to_entries.py +2 -1
- khoj/processor/conversation/anthropic/anthropic_chat.py +2 -0
- khoj/processor/conversation/google/gemini_chat.py +6 -19
- khoj/processor/conversation/google/utils.py +33 -15
- khoj/processor/conversation/offline/chat_model.py +3 -1
- khoj/processor/conversation/openai/gpt.py +2 -0
- khoj/processor/conversation/prompts.py +67 -5
- khoj/processor/conversation/utils.py +3 -7
- khoj/processor/embeddings.py +6 -3
- khoj/processor/image/generate.py +4 -3
- khoj/processor/tools/online_search.py +139 -44
- khoj/routers/api.py +35 -6
- khoj/routers/api_agents.py +235 -4
- khoj/routers/api_chat.py +102 -530
- khoj/routers/api_content.py +14 -0
- khoj/routers/api_model.py +1 -1
- khoj/routers/auth.py +9 -1
- khoj/routers/helpers.py +181 -68
- khoj/routers/subscription.py +18 -4
- khoj/search_type/text_search.py +11 -3
- khoj/utils/helpers.py +64 -8
- khoj/utils/initialization.py +0 -3
- {khoj-1.24.2.dev3.dist-info → khoj-1.25.1.dev34.dist-info}/METADATA +19 -21
- {khoj-1.24.2.dev3.dist-info → khoj-1.25.1.dev34.dist-info}/RECORD +87 -81
- khoj/interface/compiled/_next/static/chunks/1603-3e2e1528e3b6ea1d.js +0 -1
- khoj/interface/compiled/_next/static/chunks/2697-a29cb9191a9e339c.js +0 -1
- khoj/interface/compiled/_next/static/chunks/6648-ee109f4ea33a74e2.js +0 -1
- khoj/interface/compiled/_next/static/chunks/7071-b4711cecca6619a8.js +0 -1
- khoj/interface/compiled/_next/static/chunks/743-1a64254447cda71f.js +0 -1
- khoj/interface/compiled/_next/static/chunks/8423-62ac6c832be2461b.js +0 -1
- khoj/interface/compiled/_next/static/chunks/9162-0be016519a18568b.js +0 -1
- khoj/interface/compiled/_next/static/chunks/9178-7e815211edcb3657.js +0 -1
- khoj/interface/compiled/_next/static/chunks/9417-5d14ac74aaab2c66.js +0 -1
- khoj/interface/compiled/_next/static/chunks/9984-e410179c6fac7cf1.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/agents/page-d302911777a3e027.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/automations/page-0a5de8c254c29a1c.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/page-d96bf6a84bb05290.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/factchecker/page-32e61af29e6b431d.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/page-96cab08c985716f4.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/search/page-b3193d46c65571c5.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/settings/page-0db9b708366606ec.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/share/chat/page-f06ac16cfe5b5a16.js +0 -1
- khoj/interface/compiled/_next/static/css/1538cedb321e3a97.css +0 -1
- khoj/interface/compiled/_next/static/css/24f141a6e37cd204.css +0 -25
- khoj/interface/compiled/_next/static/css/4cae6c0e5c72fb2d.css +0 -1
- khoj/interface/compiled/_next/static/css/f768dddada62459d.css +0 -1
- /khoj/interface/compiled/_next/static/{_29ceahp81LhuIHo5QgOD → Jid9q6Qg851ioDaaO_fth}/_buildManifest.js +0 -0
- /khoj/interface/compiled/_next/static/{_29ceahp81LhuIHo5QgOD → Jid9q6Qg851ioDaaO_fth}/_ssgManifest.js +0 -0
- {khoj-1.24.2.dev3.dist-info → khoj-1.25.1.dev34.dist-info}/WHEEL +0 -0
- {khoj-1.24.2.dev3.dist-info → khoj-1.25.1.dev34.dist-info}/entry_points.txt +0 -0
- {khoj-1.24.2.dev3.dist-info → khoj-1.25.1.dev34.dist-info}/licenses/LICENSE +0 -0
khoj/routers/api_content.py
CHANGED
@@ -2,11 +2,13 @@ import asyncio
|
|
2
2
|
import json
|
3
3
|
import logging
|
4
4
|
import math
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
5
6
|
from typing import Dict, List, Optional, Union
|
6
7
|
|
7
8
|
from asgiref.sync import sync_to_async
|
8
9
|
from fastapi import (
|
9
10
|
APIRouter,
|
11
|
+
BackgroundTasks,
|
10
12
|
Depends,
|
11
13
|
Header,
|
12
14
|
HTTPException,
|
@@ -58,6 +60,8 @@ logger = logging.getLogger(__name__)
|
|
58
60
|
|
59
61
|
api_content = APIRouter()
|
60
62
|
|
63
|
+
executor = ThreadPoolExecutor()
|
64
|
+
|
61
65
|
|
62
66
|
class File(BaseModel):
|
63
67
|
path: str
|
@@ -77,6 +81,11 @@ class IndexerInput(BaseModel):
|
|
77
81
|
docx: Optional[dict[str, bytes]] = None
|
78
82
|
|
79
83
|
|
84
|
+
async def run_in_executor(func, *args):
|
85
|
+
loop = asyncio.get_event_loop()
|
86
|
+
return await loop.run_in_executor(executor, func, *args)
|
87
|
+
|
88
|
+
|
80
89
|
@api_content.put("")
|
81
90
|
@requires(["authenticated"])
|
82
91
|
async def put_content(
|
@@ -209,6 +218,7 @@ async def set_content_github(
|
|
209
218
|
@requires(["authenticated"])
|
210
219
|
async def set_content_notion(
|
211
220
|
request: Request,
|
221
|
+
background_tasks: BackgroundTasks,
|
212
222
|
updated_config: Union[NotionContentConfig, None],
|
213
223
|
client: Optional[str] = None,
|
214
224
|
):
|
@@ -225,6 +235,10 @@ async def set_content_notion(
|
|
225
235
|
logger.error(e, exc_info=True)
|
226
236
|
raise HTTPException(status_code=500, detail="Failed to set Notion config")
|
227
237
|
|
238
|
+
if updated_config.token:
|
239
|
+
# Trigger an async job to configure_content. Let it run without blocking the response.
|
240
|
+
background_tasks.add_task(run_in_executor, configure_content, {}, False, SearchType.Notion, user)
|
241
|
+
|
228
242
|
update_telemetry_state(
|
229
243
|
request=request,
|
230
244
|
telemetry_type="api",
|
khoj/routers/api_model.py
CHANGED
@@ -40,7 +40,7 @@ def get_user_chat_model(
|
|
40
40
|
chat_model = ConversationAdapters.get_conversation_config(user)
|
41
41
|
|
42
42
|
if chat_model is None:
|
43
|
-
chat_model = ConversationAdapters.get_default_conversation_config()
|
43
|
+
chat_model = ConversationAdapters.get_default_conversation_config(user)
|
44
44
|
|
45
45
|
return Response(status_code=200, content=json.dumps({"id": chat_model.id, "chat_model": chat_model.chat_model}))
|
46
46
|
|
khoj/routers/auth.py
CHANGED
@@ -80,11 +80,19 @@ async def login_magic_link(request: Request, form: MagicLinkForm):
|
|
80
80
|
request.session.pop("user", None)
|
81
81
|
|
82
82
|
email = form.email
|
83
|
-
user = await aget_or_create_user_by_email(email)
|
83
|
+
user, is_new = await aget_or_create_user_by_email(email)
|
84
84
|
unique_id = user.email_verification_code
|
85
85
|
|
86
86
|
if user:
|
87
87
|
await send_magic_link_email(email, unique_id, request.base_url)
|
88
|
+
if is_new:
|
89
|
+
update_telemetry_state(
|
90
|
+
request=request,
|
91
|
+
telemetry_type="api",
|
92
|
+
api="create_user",
|
93
|
+
metadata={"user_id": str(user.uuid)},
|
94
|
+
)
|
95
|
+
logger.log(logging.INFO, f"🥳 New User Created: {user.uuid}")
|
88
96
|
|
89
97
|
return Response(status_code=200)
|
90
98
|
|
khoj/routers/helpers.py
CHANGED
@@ -39,6 +39,7 @@ from khoj.database.adapters import (
|
|
39
39
|
AutomationAdapters,
|
40
40
|
ConversationAdapters,
|
41
41
|
EntryAdapters,
|
42
|
+
ais_user_subscribed,
|
42
43
|
create_khoj_token,
|
43
44
|
get_khoj_tokens,
|
44
45
|
get_user_name,
|
@@ -47,6 +48,7 @@ from khoj.database.adapters import (
|
|
47
48
|
run_with_process_lock,
|
48
49
|
)
|
49
50
|
from khoj.database.models import (
|
51
|
+
Agent,
|
50
52
|
ChatModelOptions,
|
51
53
|
ClientApplication,
|
52
54
|
Conversation,
|
@@ -118,20 +120,20 @@ def is_query_empty(query: str) -> bool:
|
|
118
120
|
return is_none_or_empty(query.strip())
|
119
121
|
|
120
122
|
|
121
|
-
def validate_conversation_config():
|
122
|
-
default_config = ConversationAdapters.get_default_conversation_config()
|
123
|
+
def validate_conversation_config(user: KhojUser):
|
124
|
+
default_config = ConversationAdapters.get_default_conversation_config(user)
|
123
125
|
|
124
126
|
if default_config is None:
|
125
|
-
raise HTTPException(status_code=500, detail="Contact the server administrator to
|
127
|
+
raise HTTPException(status_code=500, detail="Contact the server administrator to add a chat model.")
|
126
128
|
|
127
129
|
if default_config.model_type == "openai" and not default_config.openai_config:
|
128
|
-
raise HTTPException(status_code=500, detail="Contact the server administrator to
|
130
|
+
raise HTTPException(status_code=500, detail="Contact the server administrator to add a chat model.")
|
129
131
|
|
130
132
|
|
131
133
|
async def is_ready_to_chat(user: KhojUser):
|
132
|
-
user_conversation_config =
|
133
|
-
|
134
|
-
|
134
|
+
user_conversation_config = await ConversationAdapters.aget_user_conversation_config(user)
|
135
|
+
if user_conversation_config == None:
|
136
|
+
user_conversation_config = await ConversationAdapters.aget_default_conversation_config()
|
135
137
|
|
136
138
|
if user_conversation_config and user_conversation_config.model_type == ChatModelOptions.ModelType.OFFLINE:
|
137
139
|
chat_model = user_conversation_config.chat_model
|
@@ -207,7 +209,7 @@ def get_next_url(request: Request) -> str:
|
|
207
209
|
def construct_chat_history(conversation_history: dict, n: int = 4, agent_name="AI") -> str:
|
208
210
|
chat_history = ""
|
209
211
|
for chat in conversation_history.get("chat", [])[-n:]:
|
210
|
-
if chat["by"] == "khoj" and chat["intent"].get("type") in ["remember", "reminder"]:
|
212
|
+
if chat["by"] == "khoj" and chat["intent"].get("type") in ["remember", "reminder", "summarize"]:
|
211
213
|
chat_history += f"User: {chat['intent']['query']}\n"
|
212
214
|
chat_history += f"{agent_name}: {chat['message']}\n"
|
213
215
|
elif chat["by"] == "khoj" and ("text-to-image" in chat["intent"].get("type")):
|
@@ -245,20 +247,51 @@ async def agenerate_chat_response(*args):
|
|
245
247
|
return await loop.run_in_executor(executor, generate_chat_response, *args)
|
246
248
|
|
247
249
|
|
248
|
-
async def acreate_title_from_query(query: str) -> str:
|
250
|
+
async def acreate_title_from_query(query: str, user: KhojUser = None) -> str:
|
249
251
|
"""
|
250
252
|
Create a title from the given query
|
251
253
|
"""
|
252
254
|
title_generation_prompt = prompts.subject_generation.format(query=query)
|
253
255
|
|
254
256
|
with timer("Chat actor: Generate title from query", logger):
|
255
|
-
response = await send_message_to_model_wrapper(title_generation_prompt)
|
257
|
+
response = await send_message_to_model_wrapper(title_generation_prompt, user=user)
|
256
258
|
|
257
259
|
return response.strip()
|
258
260
|
|
259
261
|
|
262
|
+
async def acheck_if_safe_prompt(system_prompt: str, user: KhojUser = None) -> Tuple[bool, str]:
|
263
|
+
"""
|
264
|
+
Check if the system prompt is safe to use
|
265
|
+
"""
|
266
|
+
safe_prompt_check = prompts.personality_prompt_safety_expert.format(prompt=system_prompt)
|
267
|
+
is_safe = True
|
268
|
+
reason = ""
|
269
|
+
|
270
|
+
with timer("Chat actor: Check if safe prompt", logger):
|
271
|
+
response = await send_message_to_model_wrapper(safe_prompt_check, user=user)
|
272
|
+
|
273
|
+
response = response.strip()
|
274
|
+
try:
|
275
|
+
response = json.loads(response)
|
276
|
+
is_safe = response.get("safe", "True") == "True"
|
277
|
+
if not is_safe:
|
278
|
+
reason = response.get("reason", "")
|
279
|
+
except Exception:
|
280
|
+
logger.error(f"Invalid response for checking safe prompt: {response}")
|
281
|
+
|
282
|
+
if not is_safe:
|
283
|
+
logger.error(f"Unsafe prompt: {system_prompt}. Reason: {reason}")
|
284
|
+
|
285
|
+
return is_safe, reason
|
286
|
+
|
287
|
+
|
260
288
|
async def aget_relevant_information_sources(
|
261
|
-
query: str,
|
289
|
+
query: str,
|
290
|
+
conversation_history: dict,
|
291
|
+
is_task: bool,
|
292
|
+
user: KhojUser,
|
293
|
+
uploaded_image_url: str = None,
|
294
|
+
agent: Agent = None,
|
262
295
|
):
|
263
296
|
"""
|
264
297
|
Given a query, determine which of the available tools the agent should use in order to answer appropriately.
|
@@ -267,26 +300,34 @@ async def aget_relevant_information_sources(
|
|
267
300
|
tool_options = dict()
|
268
301
|
tool_options_str = ""
|
269
302
|
|
303
|
+
agent_tools = agent.input_tools if agent else []
|
304
|
+
|
270
305
|
for tool, description in tool_descriptions_for_llm.items():
|
271
306
|
tool_options[tool.value] = description
|
272
|
-
|
307
|
+
if len(agent_tools) == 0 or tool.value in agent_tools:
|
308
|
+
tool_options_str += f'- "{tool.value}": "{description}"\n'
|
273
309
|
|
274
310
|
chat_history = construct_chat_history(conversation_history)
|
275
311
|
|
276
312
|
if uploaded_image_url:
|
277
313
|
query = f"[placeholder for user attached image]\n{query}"
|
278
314
|
|
315
|
+
personality_context = (
|
316
|
+
prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
|
317
|
+
)
|
318
|
+
|
279
319
|
relevant_tools_prompt = prompts.pick_relevant_information_collection_tools.format(
|
280
320
|
query=query,
|
281
321
|
tools=tool_options_str,
|
282
322
|
chat_history=chat_history,
|
323
|
+
personality_context=personality_context,
|
283
324
|
)
|
284
325
|
|
285
326
|
with timer("Chat actor: Infer information sources to refer", logger):
|
286
327
|
response = await send_message_to_model_wrapper(
|
287
328
|
relevant_tools_prompt,
|
288
329
|
response_type="json_object",
|
289
|
-
|
330
|
+
user=user,
|
290
331
|
)
|
291
332
|
|
292
333
|
try:
|
@@ -300,20 +341,34 @@ async def aget_relevant_information_sources(
|
|
300
341
|
|
301
342
|
final_response = [] if not is_task else [ConversationCommand.AutomatedTask]
|
302
343
|
for llm_suggested_tool in response:
|
303
|
-
|
344
|
+
# Add a double check to verify it's in the agent list, because the LLM sometimes gets confused by the tool options.
|
345
|
+
if llm_suggested_tool in tool_options.keys() and (
|
346
|
+
len(agent_tools) == 0 or llm_suggested_tool in agent_tools
|
347
|
+
):
|
304
348
|
# Check whether the tool exists as a valid ConversationCommand
|
305
349
|
final_response.append(ConversationCommand(llm_suggested_tool))
|
306
350
|
|
307
351
|
if is_none_or_empty(final_response):
|
308
|
-
|
309
|
-
|
310
|
-
|
352
|
+
if len(agent_tools) == 0:
|
353
|
+
final_response = [ConversationCommand.Default]
|
354
|
+
else:
|
355
|
+
final_response = [ConversationCommand.General]
|
356
|
+
except Exception:
|
311
357
|
logger.error(f"Invalid response for determining relevant tools: {response}")
|
312
|
-
|
358
|
+
if len(agent_tools) == 0:
|
359
|
+
final_response = [ConversationCommand.Default]
|
360
|
+
else:
|
361
|
+
final_response = agent_tools
|
362
|
+
return final_response
|
313
363
|
|
314
364
|
|
315
365
|
async def aget_relevant_output_modes(
|
316
|
-
query: str,
|
366
|
+
query: str,
|
367
|
+
conversation_history: dict,
|
368
|
+
is_task: bool = False,
|
369
|
+
user: KhojUser = None,
|
370
|
+
uploaded_image_url: str = None,
|
371
|
+
agent: Agent = None,
|
317
372
|
):
|
318
373
|
"""
|
319
374
|
Given a query, determine which of the available tools the agent should use in order to answer appropriately.
|
@@ -322,26 +377,34 @@ async def aget_relevant_output_modes(
|
|
322
377
|
mode_options = dict()
|
323
378
|
mode_options_str = ""
|
324
379
|
|
380
|
+
output_modes = agent.output_modes if agent else []
|
381
|
+
|
325
382
|
for mode, description in mode_descriptions_for_llm.items():
|
326
383
|
# Do not allow tasks to schedule another task
|
327
384
|
if is_task and mode == ConversationCommand.Automation:
|
328
385
|
continue
|
329
386
|
mode_options[mode.value] = description
|
330
|
-
|
387
|
+
if len(output_modes) == 0 or mode.value in output_modes:
|
388
|
+
mode_options_str += f'- "{mode.value}": "{description}"\n'
|
331
389
|
|
332
390
|
chat_history = construct_chat_history(conversation_history)
|
333
391
|
|
334
392
|
if uploaded_image_url:
|
335
393
|
query = f"[placeholder for user attached image]\n{query}"
|
336
394
|
|
395
|
+
personality_context = (
|
396
|
+
prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
|
397
|
+
)
|
398
|
+
|
337
399
|
relevant_mode_prompt = prompts.pick_relevant_output_mode.format(
|
338
400
|
query=query,
|
339
401
|
modes=mode_options_str,
|
340
402
|
chat_history=chat_history,
|
403
|
+
personality_context=personality_context,
|
341
404
|
)
|
342
405
|
|
343
406
|
with timer("Chat actor: Infer output mode for chat response", logger):
|
344
|
-
response = await send_message_to_model_wrapper(relevant_mode_prompt, response_type="json_object")
|
407
|
+
response = await send_message_to_model_wrapper(relevant_mode_prompt, response_type="json_object", user=user)
|
345
408
|
|
346
409
|
try:
|
347
410
|
response = response.strip()
|
@@ -352,7 +415,9 @@ async def aget_relevant_output_modes(
|
|
352
415
|
return ConversationCommand.Text
|
353
416
|
|
354
417
|
output_mode = response["output"]
|
355
|
-
|
418
|
+
|
419
|
+
# Add a double check to verify it's in the agent list, because the LLM sometimes gets confused by the tool options.
|
420
|
+
if output_mode in mode_options.keys() and (len(output_modes) == 0 or output_mode in output_modes):
|
356
421
|
# Check whether the tool exists as a valid ConversationCommand
|
357
422
|
return ConversationCommand(output_mode)
|
358
423
|
|
@@ -364,7 +429,12 @@ async def aget_relevant_output_modes(
|
|
364
429
|
|
365
430
|
|
366
431
|
async def infer_webpage_urls(
|
367
|
-
q: str,
|
432
|
+
q: str,
|
433
|
+
conversation_history: dict,
|
434
|
+
location_data: LocationData,
|
435
|
+
user: KhojUser,
|
436
|
+
uploaded_image_url: str = None,
|
437
|
+
agent: Agent = None,
|
368
438
|
) -> List[str]:
|
369
439
|
"""
|
370
440
|
Infer webpage links from the given query
|
@@ -374,17 +444,22 @@ async def infer_webpage_urls(
|
|
374
444
|
chat_history = construct_chat_history(conversation_history)
|
375
445
|
|
376
446
|
utc_date = datetime.utcnow().strftime("%Y-%m-%d")
|
447
|
+
personality_context = (
|
448
|
+
prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
|
449
|
+
)
|
450
|
+
|
377
451
|
online_queries_prompt = prompts.infer_webpages_to_read.format(
|
378
452
|
current_date=utc_date,
|
379
453
|
query=q,
|
380
454
|
chat_history=chat_history,
|
381
455
|
location=location,
|
382
456
|
username=username,
|
457
|
+
personality_context=personality_context,
|
383
458
|
)
|
384
459
|
|
385
460
|
with timer("Chat actor: Infer webpage urls to read", logger):
|
386
461
|
response = await send_message_to_model_wrapper(
|
387
|
-
online_queries_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object"
|
462
|
+
online_queries_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object", user=user
|
388
463
|
)
|
389
464
|
|
390
465
|
# Validate that the response is a non-empty, JSON-serializable list of URLs
|
@@ -400,7 +475,12 @@ async def infer_webpage_urls(
|
|
400
475
|
|
401
476
|
|
402
477
|
async def generate_online_subqueries(
|
403
|
-
q: str,
|
478
|
+
q: str,
|
479
|
+
conversation_history: dict,
|
480
|
+
location_data: LocationData,
|
481
|
+
user: KhojUser,
|
482
|
+
uploaded_image_url: str = None,
|
483
|
+
agent: Agent = None,
|
404
484
|
) -> List[str]:
|
405
485
|
"""
|
406
486
|
Generate subqueries from the given query
|
@@ -410,17 +490,22 @@ async def generate_online_subqueries(
|
|
410
490
|
chat_history = construct_chat_history(conversation_history)
|
411
491
|
|
412
492
|
utc_date = datetime.utcnow().strftime("%Y-%m-%d")
|
493
|
+
personality_context = (
|
494
|
+
prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
|
495
|
+
)
|
496
|
+
|
413
497
|
online_queries_prompt = prompts.online_search_conversation_subqueries.format(
|
414
498
|
current_date=utc_date,
|
415
499
|
query=q,
|
416
500
|
chat_history=chat_history,
|
417
501
|
location=location,
|
418
502
|
username=username,
|
503
|
+
personality_context=personality_context,
|
419
504
|
)
|
420
505
|
|
421
506
|
with timer("Chat actor: Generate online search subqueries", logger):
|
422
507
|
response = await send_message_to_model_wrapper(
|
423
|
-
online_queries_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object"
|
508
|
+
online_queries_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object", user=user
|
424
509
|
)
|
425
510
|
|
426
511
|
# Validate that the response is a non-empty, JSON-serializable list
|
@@ -438,7 +523,9 @@ async def generate_online_subqueries(
|
|
438
523
|
return [q]
|
439
524
|
|
440
525
|
|
441
|
-
async def schedule_query(
|
526
|
+
async def schedule_query(
|
527
|
+
q: str, conversation_history: dict, user: KhojUser, uploaded_image_url: str = None
|
528
|
+
) -> Tuple[str, ...]:
|
442
529
|
"""
|
443
530
|
Schedule the date, time to run the query. Assume the server timezone is UTC.
|
444
531
|
"""
|
@@ -450,7 +537,7 @@ async def schedule_query(q: str, conversation_history: dict, uploaded_image_url:
|
|
450
537
|
)
|
451
538
|
|
452
539
|
raw_response = await send_message_to_model_wrapper(
|
453
|
-
crontime_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object"
|
540
|
+
crontime_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object", user=user
|
454
541
|
)
|
455
542
|
|
456
543
|
# Validate that the response is a non-empty, JSON-serializable list
|
@@ -464,33 +551,41 @@ async def schedule_query(q: str, conversation_history: dict, uploaded_image_url:
|
|
464
551
|
raise AssertionError(f"Invalid response for scheduling query: {raw_response}")
|
465
552
|
|
466
553
|
|
467
|
-
async def extract_relevant_info(
|
554
|
+
async def extract_relevant_info(
|
555
|
+
qs: set[str], corpus: str, user: KhojUser = None, agent: Agent = None
|
556
|
+
) -> Union[str, None]:
|
468
557
|
"""
|
469
558
|
Extract relevant information for a given query from the target corpus
|
470
559
|
"""
|
471
560
|
|
472
|
-
if is_none_or_empty(corpus) or is_none_or_empty(
|
561
|
+
if is_none_or_empty(corpus) or is_none_or_empty(qs):
|
473
562
|
return None
|
474
563
|
|
564
|
+
personality_context = (
|
565
|
+
prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
|
566
|
+
)
|
567
|
+
|
475
568
|
extract_relevant_information = prompts.extract_relevant_information.format(
|
476
|
-
query=
|
569
|
+
query=", ".join(qs),
|
477
570
|
corpus=corpus.strip(),
|
571
|
+
personality_context=personality_context,
|
478
572
|
)
|
479
573
|
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
prompts.system_prompt_extract_relevant_information,
|
486
|
-
chat_model_option=chat_model,
|
487
|
-
subscribed=subscribed,
|
488
|
-
)
|
574
|
+
response = await send_message_to_model_wrapper(
|
575
|
+
extract_relevant_information,
|
576
|
+
prompts.system_prompt_extract_relevant_information,
|
577
|
+
user=user,
|
578
|
+
)
|
489
579
|
return response.strip()
|
490
580
|
|
491
581
|
|
492
582
|
async def extract_relevant_summary(
|
493
|
-
q: str,
|
583
|
+
q: str,
|
584
|
+
corpus: str,
|
585
|
+
conversation_history: dict,
|
586
|
+
uploaded_image_url: str = None,
|
587
|
+
user: KhojUser = None,
|
588
|
+
agent: Agent = None,
|
494
589
|
) -> Union[str, None]:
|
495
590
|
"""
|
496
591
|
Extract relevant information for a given query from the target corpus
|
@@ -499,19 +594,24 @@ async def extract_relevant_summary(
|
|
499
594
|
if is_none_or_empty(corpus) or is_none_or_empty(q):
|
500
595
|
return None
|
501
596
|
|
597
|
+
personality_context = (
|
598
|
+
prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
|
599
|
+
)
|
600
|
+
|
601
|
+
chat_history = construct_chat_history(conversation_history)
|
602
|
+
|
502
603
|
extract_relevant_information = prompts.extract_relevant_summary.format(
|
503
604
|
query=q,
|
605
|
+
chat_history=chat_history,
|
504
606
|
corpus=corpus.strip(),
|
607
|
+
personality_context=personality_context,
|
505
608
|
)
|
506
609
|
|
507
|
-
chat_model: ChatModelOptions = await ConversationAdapters.aget_default_conversation_config()
|
508
|
-
|
509
610
|
with timer("Chat actor: Extract relevant information from data", logger):
|
510
611
|
response = await send_message_to_model_wrapper(
|
511
612
|
extract_relevant_information,
|
512
613
|
prompts.system_prompt_extract_relevant_summary,
|
513
|
-
|
514
|
-
subscribed=subscribed,
|
614
|
+
user=user,
|
515
615
|
uploaded_image_url=uploaded_image_url,
|
516
616
|
)
|
517
617
|
return response.strip()
|
@@ -524,14 +624,18 @@ async def generate_better_image_prompt(
|
|
524
624
|
note_references: List[Dict[str, Any]],
|
525
625
|
online_results: Optional[dict] = None,
|
526
626
|
model_type: Optional[str] = None,
|
527
|
-
subscribed: bool = False,
|
528
627
|
uploaded_image_url: Optional[str] = None,
|
628
|
+
user: KhojUser = None,
|
629
|
+
agent: Agent = None,
|
529
630
|
) -> str:
|
530
631
|
"""
|
531
632
|
Generate a better image prompt from the given query
|
532
633
|
"""
|
533
634
|
|
534
635
|
today_date = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d, %A")
|
636
|
+
personality_context = (
|
637
|
+
prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
|
638
|
+
)
|
535
639
|
model_type = model_type or TextToImageModelConfig.ModelType.OPENAI
|
536
640
|
|
537
641
|
if location_data:
|
@@ -558,6 +662,7 @@ async def generate_better_image_prompt(
|
|
558
662
|
current_date=today_date,
|
559
663
|
references=user_references,
|
560
664
|
online_results=simplified_online_results,
|
665
|
+
personality_context=personality_context,
|
561
666
|
)
|
562
667
|
elif model_type in [TextToImageModelConfig.ModelType.STABILITYAI, TextToImageModelConfig.ModelType.REPLICATE]:
|
563
668
|
image_prompt = prompts.image_generation_improve_prompt_sd.format(
|
@@ -567,14 +672,11 @@ async def generate_better_image_prompt(
|
|
567
672
|
current_date=today_date,
|
568
673
|
references=user_references,
|
569
674
|
online_results=simplified_online_results,
|
675
|
+
personality_context=personality_context,
|
570
676
|
)
|
571
677
|
|
572
|
-
chat_model: ChatModelOptions = await ConversationAdapters.aget_default_conversation_config()
|
573
|
-
|
574
678
|
with timer("Chat actor: Generate contextual image prompt", logger):
|
575
|
-
response = await send_message_to_model_wrapper(
|
576
|
-
image_prompt, chat_model_option=chat_model, subscribed=subscribed, uploaded_image_url=uploaded_image_url
|
577
|
-
)
|
679
|
+
response = await send_message_to_model_wrapper(image_prompt, uploaded_image_url=uploaded_image_url, user=user)
|
578
680
|
response = response.strip()
|
579
681
|
if response.startswith(('"', "'")) and response.endswith(('"', "'")):
|
580
682
|
response = response[1:-1]
|
@@ -586,14 +688,10 @@ async def send_message_to_model_wrapper(
|
|
586
688
|
message: str,
|
587
689
|
system_message: str = "",
|
588
690
|
response_type: str = "text",
|
589
|
-
|
590
|
-
subscribed: bool = False,
|
691
|
+
user: KhojUser = None,
|
591
692
|
uploaded_image_url: str = None,
|
592
693
|
):
|
593
|
-
conversation_config: ChatModelOptions = (
|
594
|
-
chat_model_option or await ConversationAdapters.aget_default_conversation_config()
|
595
|
-
)
|
596
|
-
|
694
|
+
conversation_config: ChatModelOptions = await ConversationAdapters.aget_default_conversation_config(user)
|
597
695
|
vision_available = conversation_config.vision_enabled
|
598
696
|
if not vision_available and uploaded_image_url:
|
599
697
|
vision_enabled_config = await ConversationAdapters.aget_vision_enabled_config()
|
@@ -601,6 +699,7 @@ async def send_message_to_model_wrapper(
|
|
601
699
|
conversation_config = vision_enabled_config
|
602
700
|
vision_available = True
|
603
701
|
|
702
|
+
subscribed = await ais_user_subscribed(user)
|
604
703
|
chat_model = conversation_config.chat_model
|
605
704
|
max_tokens = (
|
606
705
|
conversation_config.subscribed_max_prompt_size
|
@@ -651,15 +750,13 @@ async def send_message_to_model_wrapper(
|
|
651
750
|
model_type=conversation_config.model_type,
|
652
751
|
)
|
653
752
|
|
654
|
-
|
753
|
+
return send_message_to_model(
|
655
754
|
messages=truncated_messages,
|
656
755
|
api_key=api_key,
|
657
756
|
model=chat_model,
|
658
757
|
response_type=response_type,
|
659
758
|
api_base_url=api_base_url,
|
660
759
|
)
|
661
|
-
|
662
|
-
return openai_response
|
663
760
|
elif model_type == ChatModelOptions.ModelType.ANTHROPIC:
|
664
761
|
api_key = conversation_config.openai_config.api_key
|
665
762
|
truncated_messages = generate_chatml_messages_with_context(
|
@@ -701,8 +798,9 @@ def send_message_to_model_wrapper_sync(
|
|
701
798
|
message: str,
|
702
799
|
system_message: str = "",
|
703
800
|
response_type: str = "text",
|
801
|
+
user: KhojUser = None,
|
704
802
|
):
|
705
|
-
conversation_config: ChatModelOptions = ConversationAdapters.get_default_conversation_config()
|
803
|
+
conversation_config: ChatModelOptions = ConversationAdapters.get_default_conversation_config(user)
|
706
804
|
|
707
805
|
if conversation_config is None:
|
708
806
|
raise HTTPException(status_code=500, detail="Contact the server administrator to set a default chat model.")
|
@@ -942,13 +1040,23 @@ class ApiUserRateLimiter:
|
|
942
1040
|
|
943
1041
|
# Check if the user has exceeded the rate limit
|
944
1042
|
if subscribed and count_requests >= self.subscribed_requests:
|
1043
|
+
logger.info(
|
1044
|
+
f"Rate limit: {count_requests} requests in {self.window} seconds for user: {user}. Limit is {self.subscribed_requests} requests."
|
1045
|
+
)
|
945
1046
|
raise HTTPException(status_code=429, detail="Slow down! Too Many Requests")
|
946
1047
|
if not subscribed and count_requests >= self.requests:
|
947
1048
|
if self.requests >= self.subscribed_requests:
|
1049
|
+
logger.info(
|
1050
|
+
f"Rate limit: {count_requests} requests in {self.window} seconds for user: {user}. Limit is {self.subscribed_requests} requests."
|
1051
|
+
)
|
948
1052
|
raise HTTPException(
|
949
1053
|
status_code=429,
|
950
1054
|
detail="Slow down! Too Many Requests",
|
951
1055
|
)
|
1056
|
+
|
1057
|
+
logger.info(
|
1058
|
+
f"Rate limit: {count_requests} requests in {self.window} seconds for user: {user}. Limit is {self.subscribed_requests} requests."
|
1059
|
+
)
|
952
1060
|
raise HTTPException(
|
953
1061
|
status_code=429,
|
954
1062
|
detail="We're glad you're enjoying Khoj! You've exceeded your usage limit for today. Come back tomorrow or subscribe to increase your usage limit via [your settings](https://app.khoj.dev/settings).",
|
@@ -986,6 +1094,9 @@ class ConversationCommandRateLimiter:
|
|
986
1094
|
).acount()
|
987
1095
|
|
988
1096
|
if subscribed and count_requests >= self.subscribed_rate_limit:
|
1097
|
+
logger.info(
|
1098
|
+
f"Rate limit: {count_requests} requests in 24 hours for user: {user}. Limit is {self.subscribed_rate_limit} requests."
|
1099
|
+
)
|
989
1100
|
raise HTTPException(status_code=429, detail="Slow down! Too Many Requests")
|
990
1101
|
if not subscribed and count_requests >= self.trial_rate_limit:
|
991
1102
|
raise HTTPException(
|
@@ -1068,7 +1179,7 @@ class CommonQueryParamsClass:
|
|
1068
1179
|
CommonQueryParams = Annotated[CommonQueryParamsClass, Depends()]
|
1069
1180
|
|
1070
1181
|
|
1071
|
-
def should_notify(original_query: str, executed_query: str, ai_response: str) -> bool:
|
1182
|
+
def should_notify(original_query: str, executed_query: str, ai_response: str, user: KhojUser) -> bool:
|
1072
1183
|
"""
|
1073
1184
|
Decide whether to notify the user of the AI response.
|
1074
1185
|
Default to notifying the user for now.
|
@@ -1085,7 +1196,7 @@ def should_notify(original_query: str, executed_query: str, ai_response: str) ->
|
|
1085
1196
|
with timer("Chat actor: Decide to notify user of automation response", logger):
|
1086
1197
|
try:
|
1087
1198
|
# TODO Replace with async call so we don't have to maintain a sync version
|
1088
|
-
response = send_message_to_model_wrapper_sync(to_notify_or_not)
|
1199
|
+
response = send_message_to_model_wrapper_sync(to_notify_or_not, user)
|
1089
1200
|
should_notify_result = "no" not in response.lower()
|
1090
1201
|
logger.info(f'Decided to {"not " if not should_notify_result else ""}notify user of automation response.')
|
1091
1202
|
return should_notify_result
|
@@ -1177,7 +1288,9 @@ def scheduled_chat(
|
|
1177
1288
|
ai_response = raw_response.text
|
1178
1289
|
|
1179
1290
|
# Notify user if the AI response is satisfactory
|
1180
|
-
if should_notify(
|
1291
|
+
if should_notify(
|
1292
|
+
original_query=scheduling_request, executed_query=cleaned_query, ai_response=ai_response, user=user
|
1293
|
+
):
|
1181
1294
|
if is_resend_enabled():
|
1182
1295
|
send_task_email(user.get_short_name(), user.email, cleaned_query, ai_response, subject, is_image)
|
1183
1296
|
else:
|
@@ -1187,7 +1300,7 @@ def scheduled_chat(
|
|
1187
1300
|
async def create_automation(
|
1188
1301
|
q: str, timezone: str, user: KhojUser, calling_url: URL, meta_log: dict = {}, conversation_id: str = None
|
1189
1302
|
):
|
1190
|
-
crontime, query_to_run, subject = await schedule_query(q, meta_log)
|
1303
|
+
crontime, query_to_run, subject = await schedule_query(q, meta_log, user)
|
1191
1304
|
job = await schedule_automation(query_to_run, subject, crontime, timezone, q, user, calling_url, conversation_id)
|
1192
1305
|
return job, crontime, query_to_run, subject
|
1193
1306
|
|
@@ -1381,9 +1494,9 @@ def get_user_config(user: KhojUser, request: Request, is_detailed: bool = False)
|
|
1381
1494
|
current_notion_config = get_user_notion_config(user)
|
1382
1495
|
notion_token = current_notion_config.token if current_notion_config else ""
|
1383
1496
|
|
1384
|
-
selected_chat_model_config = (
|
1385
|
-
|
1386
|
-
)
|
1497
|
+
selected_chat_model_config = ConversationAdapters.get_conversation_config(
|
1498
|
+
user
|
1499
|
+
) or ConversationAdapters.get_default_conversation_config(user)
|
1387
1500
|
chat_models = ConversationAdapters.get_conversation_processor_options().all()
|
1388
1501
|
chat_model_options = list()
|
1389
1502
|
for chat_model in chat_models:
|