khoj 1.20.4.dev13__py3-none-any.whl → 1.20.5.dev17__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 +6 -32
- khoj/database/adapters/__init__.py +76 -28
- khoj/database/admin.py +15 -3
- khoj/database/migrations/0056_searchmodelconfig_cross_encoder_model_config.py +17 -0
- khoj/database/migrations/0057_remove_serverchatsettings_default_model_and_more.py +51 -0
- khoj/database/models/__init__.py +7 -4
- khoj/interface/compiled/404/index.html +1 -1
- khoj/interface/compiled/_next/static/chunks/{9178-5a1fa2b9023249af.js → 9178-b9ab3fa2e9be8287.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/page-8c9b92236d4daf4b.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/settings/page-ddcd51147d18c694.js +1 -0
- khoj/interface/compiled/_next/static/chunks/{webpack-c6008e79c8ef349d.js → webpack-9953d80989df2d20.js} +1 -1
- 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 +1 -1
- 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 +2 -2
- khoj/interface/compiled/share/chat/index.html +1 -1
- khoj/interface/compiled/share/chat/index.txt +2 -2
- khoj/processor/conversation/anthropic/anthropic_chat.py +6 -2
- khoj/processor/conversation/offline/chat_model.py +15 -3
- khoj/processor/conversation/openai/gpt.py +6 -3
- khoj/processor/conversation/openai/utils.py +1 -1
- khoj/processor/conversation/prompts.py +20 -0
- khoj/processor/embeddings.py +3 -1
- khoj/processor/tools/online_search.py +19 -8
- khoj/routers/api.py +3 -1
- khoj/routers/api_chat.py +21 -9
- khoj/routers/helpers.py +38 -14
- khoj/search_filter/file_filter.py +5 -2
- {khoj-1.20.4.dev13.dist-info → khoj-1.20.5.dev17.dist-info}/METADATA +1 -1
- {khoj-1.20.4.dev13.dist-info → khoj-1.20.5.dev17.dist-info}/RECORD +49 -47
- khoj/interface/compiled/_next/static/chunks/app/chat/page-51ab7c4b766ff344.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/settings/page-e83f6fa32691ca64.js +0 -1
- /khoj/interface/compiled/_next/static/{EuaXDNeXisAc9H0EHd10x → QSaUkpIwMzbLQnDFgPg3j}/_buildManifest.js +0 -0
- /khoj/interface/compiled/_next/static/{EuaXDNeXisAc9H0EHd10x → QSaUkpIwMzbLQnDFgPg3j}/_ssgManifest.js +0 -0
- /khoj/interface/compiled/_next/static/chunks/{8423-132ea64eac83fd43.js → 8423-898d821eaab634af.js} +0 -0
- /khoj/interface/compiled/_next/static/chunks/{9417-2e54c6fd056982d8.js → 9417-5d14ac74aaab2c66.js} +0 -0
- /khoj/interface/compiled/_next/static/chunks/app/agents/{page-922694b75f1fb67b.js → page-989a824c640bc532.js} +0 -0
- /khoj/interface/compiled/_next/static/chunks/app/{page-ef4e7248d37fae41.js → page-851860583273ab5d.js} +0 -0
- {khoj-1.20.4.dev13.dist-info → khoj-1.20.5.dev17.dist-info}/WHEEL +0 -0
- {khoj-1.20.4.dev13.dist-info → khoj-1.20.5.dev17.dist-info}/entry_points.txt +0 -0
- {khoj-1.20.4.dev13.dist-info → khoj-1.20.5.dev17.dist-info}/licenses/LICENSE +0 -0
|
@@ -7,7 +7,7 @@ from typing import Any, Iterator, List, Union
|
|
|
7
7
|
from langchain.schema import ChatMessage
|
|
8
8
|
from llama_cpp import Llama
|
|
9
9
|
|
|
10
|
-
from khoj.database.models import Agent
|
|
10
|
+
from khoj.database.models import Agent, KhojUser
|
|
11
11
|
from khoj.processor.conversation import prompts
|
|
12
12
|
from khoj.processor.conversation.offline.utils import download_model
|
|
13
13
|
from khoj.processor.conversation.utils import (
|
|
@@ -30,7 +30,9 @@ def extract_questions_offline(
|
|
|
30
30
|
use_history: bool = True,
|
|
31
31
|
should_extract_questions: bool = True,
|
|
32
32
|
location_data: LocationData = None,
|
|
33
|
+
user: KhojUser = None,
|
|
33
34
|
max_prompt_size: int = None,
|
|
35
|
+
temperature: float = 0.7,
|
|
34
36
|
) -> List[str]:
|
|
35
37
|
"""
|
|
36
38
|
Infer search queries to retrieve relevant notes to answer user query
|
|
@@ -45,6 +47,7 @@ def extract_questions_offline(
|
|
|
45
47
|
offline_chat_model = loaded_model or download_model(model, max_tokens=max_prompt_size)
|
|
46
48
|
|
|
47
49
|
location = f"{location_data.city}, {location_data.region}, {location_data.country}" if location_data else "Unknown"
|
|
50
|
+
username = prompts.user_name.format(name=user.get_full_name()) if user and user.get_full_name() else ""
|
|
48
51
|
|
|
49
52
|
# Extract Past User Message and Inferred Questions from Conversation Log
|
|
50
53
|
chat_history = ""
|
|
@@ -64,10 +67,12 @@ def extract_questions_offline(
|
|
|
64
67
|
chat_history=chat_history,
|
|
65
68
|
current_date=today.strftime("%Y-%m-%d"),
|
|
66
69
|
day_of_week=today.strftime("%A"),
|
|
70
|
+
current_month=today.strftime("%Y-%m"),
|
|
67
71
|
yesterday_date=yesterday,
|
|
68
72
|
last_year=last_year,
|
|
69
73
|
this_year=today.year,
|
|
70
74
|
location=location,
|
|
75
|
+
username=username,
|
|
71
76
|
)
|
|
72
77
|
|
|
73
78
|
messages = generate_chatml_messages_with_context(
|
|
@@ -77,7 +82,11 @@ def extract_questions_offline(
|
|
|
77
82
|
state.chat_lock.acquire()
|
|
78
83
|
try:
|
|
79
84
|
response = send_message_to_model_offline(
|
|
80
|
-
messages,
|
|
85
|
+
messages,
|
|
86
|
+
loaded_model=offline_chat_model,
|
|
87
|
+
model=model,
|
|
88
|
+
max_prompt_size=max_prompt_size,
|
|
89
|
+
temperature=temperature,
|
|
81
90
|
)
|
|
82
91
|
finally:
|
|
83
92
|
state.chat_lock.release()
|
|
@@ -229,6 +238,7 @@ def send_message_to_model_offline(
|
|
|
229
238
|
messages: List[ChatMessage],
|
|
230
239
|
loaded_model=None,
|
|
231
240
|
model="NousResearch/Hermes-2-Pro-Mistral-7B-GGUF",
|
|
241
|
+
temperature: float = 0.2,
|
|
232
242
|
streaming=False,
|
|
233
243
|
stop=[],
|
|
234
244
|
max_prompt_size: int = None,
|
|
@@ -236,7 +246,9 @@ def send_message_to_model_offline(
|
|
|
236
246
|
assert loaded_model is None or isinstance(loaded_model, Llama), "loaded_model must be of type Llama, if configured"
|
|
237
247
|
offline_chat_model = loaded_model or download_model(model, max_tokens=max_prompt_size)
|
|
238
248
|
messages_dict = [{"role": message.role, "content": message.content} for message in messages]
|
|
239
|
-
response = offline_chat_model.create_chat_completion(
|
|
249
|
+
response = offline_chat_model.create_chat_completion(
|
|
250
|
+
messages_dict, stop=stop, stream=streaming, temperature=temperature
|
|
251
|
+
)
|
|
240
252
|
if streaming:
|
|
241
253
|
return response
|
|
242
254
|
else:
|
|
@@ -5,7 +5,7 @@ from typing import Dict, Optional
|
|
|
5
5
|
|
|
6
6
|
from langchain.schema import ChatMessage
|
|
7
7
|
|
|
8
|
-
from khoj.database.models import Agent
|
|
8
|
+
from khoj.database.models import Agent, KhojUser
|
|
9
9
|
from khoj.processor.conversation import prompts
|
|
10
10
|
from khoj.processor.conversation.openai.utils import (
|
|
11
11
|
chat_completion_with_backoff,
|
|
@@ -24,14 +24,15 @@ def extract_questions(
|
|
|
24
24
|
conversation_log={},
|
|
25
25
|
api_key=None,
|
|
26
26
|
api_base_url=None,
|
|
27
|
-
temperature=0,
|
|
28
|
-
max_tokens=100,
|
|
27
|
+
temperature=0.7,
|
|
29
28
|
location_data: LocationData = None,
|
|
29
|
+
user: KhojUser = None,
|
|
30
30
|
):
|
|
31
31
|
"""
|
|
32
32
|
Infer search queries to retrieve relevant notes to answer user query
|
|
33
33
|
"""
|
|
34
34
|
location = f"{location_data.city}, {location_data.region}, {location_data.country}" if location_data else "Unknown"
|
|
35
|
+
username = prompts.user_name.format(name=user.get_full_name()) if user and user.get_full_name() else ""
|
|
35
36
|
|
|
36
37
|
# Extract Past User Message and Inferred Questions from Conversation Log
|
|
37
38
|
chat_history = "".join(
|
|
@@ -50,6 +51,7 @@ def extract_questions(
|
|
|
50
51
|
prompt = prompts.extract_questions.format(
|
|
51
52
|
current_date=today.strftime("%Y-%m-%d"),
|
|
52
53
|
day_of_week=today.strftime("%A"),
|
|
54
|
+
current_month=today.strftime("%Y-%m"),
|
|
53
55
|
last_new_year=last_new_year.strftime("%Y"),
|
|
54
56
|
last_new_year_date=last_new_year.strftime("%Y-%m-%d"),
|
|
55
57
|
current_new_year_date=current_new_year.strftime("%Y-%m-%d"),
|
|
@@ -59,6 +61,7 @@ def extract_questions(
|
|
|
59
61
|
text=text,
|
|
60
62
|
yesterday_date=(today - timedelta(days=1)).strftime("%Y-%m-%d"),
|
|
61
63
|
location=location,
|
|
64
|
+
username=username,
|
|
62
65
|
)
|
|
63
66
|
messages = [ChatMessage(content=prompt, role="user")]
|
|
64
67
|
|
|
@@ -36,7 +36,7 @@ def completion_with_backoff(
|
|
|
36
36
|
messages, model, temperature=0, openai_api_key=None, api_base_url=None, model_kwargs=None
|
|
37
37
|
) -> str:
|
|
38
38
|
client_key = f"{openai_api_key}--{api_base_url}"
|
|
39
|
-
client: openai.OpenAI = openai_clients.get(client_key)
|
|
39
|
+
client: openai.OpenAI | None = openai_clients.get(client_key)
|
|
40
40
|
if not client:
|
|
41
41
|
client = openai.OpenAI(
|
|
42
42
|
api_key=openai_api_key,
|
|
@@ -208,10 +208,12 @@ Construct search queries to retrieve relevant information to answer the user's q
|
|
|
208
208
|
- Add as much context from the previous questions and answers as required into your search queries.
|
|
209
209
|
- Break messages into multiple search queries when required to retrieve the relevant information.
|
|
210
210
|
- Add date filters to your search queries from questions and answers when required to retrieve the relevant information.
|
|
211
|
+
- When asked a meta, vague or random questions, search for a variety of broad topics to answer the user's question.
|
|
211
212
|
- Share relevant search queries as a JSON list of strings. Do not say anything else.
|
|
212
213
|
|
|
213
214
|
Current Date: {day_of_week}, {current_date}
|
|
214
215
|
User's Location: {location}
|
|
216
|
+
{username}
|
|
215
217
|
|
|
216
218
|
Examples:
|
|
217
219
|
Q: How was my trip to Cambodia?
|
|
@@ -238,6 +240,9 @@ Khoj: ["What kind of plants do I have?", "What issues do my plants have?"]
|
|
|
238
240
|
Q: Who all did I meet here yesterday?
|
|
239
241
|
Khoj: ["Met in {location} on {yesterday_date} dt>='{yesterday_date}' dt<'{current_date}'"]
|
|
240
242
|
|
|
243
|
+
Q: Share some random, interesting experiences from this month
|
|
244
|
+
Khoj: ["Exciting travel adventures from {current_month}", "Fun social events dt>='{current_month}-01' dt<'{current_date}'", "Intense emotional experiences in {current_month}"]
|
|
245
|
+
|
|
241
246
|
Chat History:
|
|
242
247
|
{chat_history}
|
|
243
248
|
What searches will you perform to answer the following question, using the chat history as reference? Respond only with relevant search queries as a valid JSON list of strings.
|
|
@@ -254,10 +259,12 @@ Construct search queries to retrieve relevant information to answer the user's q
|
|
|
254
259
|
- Add as much context from the previous questions and answers as required into your search queries.
|
|
255
260
|
- Break messages into multiple search queries when required to retrieve the relevant information.
|
|
256
261
|
- Add date filters to your search queries from questions and answers when required to retrieve the relevant information.
|
|
262
|
+
- When asked a meta, vague or random questions, search for a variety of broad topics to answer the user's question.
|
|
257
263
|
|
|
258
264
|
What searches will you perform to answer the users question? Respond with search queries as list of strings in a JSON object.
|
|
259
265
|
Current Date: {day_of_week}, {current_date}
|
|
260
266
|
User's Location: {location}
|
|
267
|
+
{username}
|
|
261
268
|
|
|
262
269
|
Q: How was my trip to Cambodia?
|
|
263
270
|
Khoj: {{"queries": ["How was my trip to Cambodia?"]}}
|
|
@@ -279,6 +286,10 @@ Q: How many tennis balls fit in the back of a 2002 Honda Civic?
|
|
|
279
286
|
Khoj: {{"queries": ["What is the size of a tennis ball?", "What is the trunk size of a 2002 Honda Civic?"]}}
|
|
280
287
|
A: 1085 tennis balls will fit in the trunk of a Honda Civic
|
|
281
288
|
|
|
289
|
+
Q: Share some random, interesting experiences from this month
|
|
290
|
+
Khoj: {{"queries": ["Exciting travel adventures from {current_month}", "Fun social events dt>='{current_month}-01' dt<'{current_date}'", "Intense emotional experiences in {current_month}"]}}
|
|
291
|
+
A: You had a great time at the local beach with your friends, attended a music concert and had a deep conversation with your friend, Khalid.
|
|
292
|
+
|
|
282
293
|
Q: Is Bob older than Tom?
|
|
283
294
|
Khoj: {{"queries": ["When was Bob born?", "What is Tom's age?"]}}
|
|
284
295
|
A: Yes, Bob is older than Tom. As Bob was born on 1984-01-01 and Tom is 30 years old.
|
|
@@ -305,11 +316,13 @@ Construct search queries to retrieve relevant information to answer the user's q
|
|
|
305
316
|
- Add as much context from the previous questions and answers as required into your search queries.
|
|
306
317
|
- Break messages into multiple search queries when required to retrieve the relevant information.
|
|
307
318
|
- Add date filters to your search queries from questions and answers when required to retrieve the relevant information.
|
|
319
|
+
- When asked a meta, vague or random questions, search for a variety of broad topics to answer the user's question.
|
|
308
320
|
|
|
309
321
|
What searches will you perform to answer the users question? Respond with a JSON object with the key "queries" mapping to a list of searches you would perform on the user's knowledge base. Just return the queries and nothing else.
|
|
310
322
|
|
|
311
323
|
Current Date: {day_of_week}, {current_date}
|
|
312
324
|
User's Location: {location}
|
|
325
|
+
{username}
|
|
313
326
|
|
|
314
327
|
Here are some examples of how you can construct search queries to answer the user's question:
|
|
315
328
|
|
|
@@ -328,6 +341,11 @@ A: I can help you live healthier and happier across work and personal life
|
|
|
328
341
|
User: Who all did I meet here yesterday?
|
|
329
342
|
Assistant: {{"queries": ["Met in {location} on {yesterday_date} dt>='{yesterday_date}' dt<'{current_date}'"]}}
|
|
330
343
|
A: Yesterday's note mentions your visit to your local beach with Ram and Shyam.
|
|
344
|
+
|
|
345
|
+
User: Share some random, interesting experiences from this month
|
|
346
|
+
Assistant: {{"queries": ["Exciting travel adventures from {current_month}", "Fun social events dt>='{current_month}-01' dt<'{current_date}'", "Intense emotional experiences in {current_month}"]}}
|
|
347
|
+
A: You had a great time at the local beach with your friends, attended a music concert and had a deep conversation with your friend, Khalid.
|
|
348
|
+
|
|
331
349
|
""".strip()
|
|
332
350
|
)
|
|
333
351
|
|
|
@@ -525,6 +543,7 @@ Which webpages will you need to read to answer the user's question?
|
|
|
525
543
|
Provide web page links as a list of strings in a JSON object.
|
|
526
544
|
Current Date: {current_date}
|
|
527
545
|
User's Location: {location}
|
|
546
|
+
{username}
|
|
528
547
|
|
|
529
548
|
Here are some examples:
|
|
530
549
|
History:
|
|
@@ -571,6 +590,7 @@ What Google searches, if any, will you need to perform to answer the user's ques
|
|
|
571
590
|
Provide search queries as a list of strings in a JSON object. Do not wrap the json in a codeblock.
|
|
572
591
|
Current Date: {current_date}
|
|
573
592
|
User's Location: {location}
|
|
593
|
+
{username}
|
|
574
594
|
|
|
575
595
|
Here are some examples:
|
|
576
596
|
History:
|
khoj/processor/embeddings.py
CHANGED
|
@@ -95,11 +95,13 @@ class CrossEncoderModel:
|
|
|
95
95
|
model_name: str = "mixedbread-ai/mxbai-rerank-xsmall-v1",
|
|
96
96
|
cross_encoder_inference_endpoint: str = None,
|
|
97
97
|
cross_encoder_inference_endpoint_api_key: str = None,
|
|
98
|
+
model_kwargs: dict = {},
|
|
98
99
|
):
|
|
99
100
|
self.model_name = model_name
|
|
100
|
-
self.cross_encoder_model = CrossEncoder(model_name=self.model_name, device=get_device())
|
|
101
101
|
self.inference_endpoint = cross_encoder_inference_endpoint
|
|
102
102
|
self.api_key = cross_encoder_inference_endpoint_api_key
|
|
103
|
+
self.model_kwargs = merge_dicts(model_kwargs, {"device": get_device()})
|
|
104
|
+
self.cross_encoder_model = CrossEncoder(model_name=self.model_name, **self.model_kwargs)
|
|
103
105
|
|
|
104
106
|
def inference_server_enabled(self) -> bool:
|
|
105
107
|
return self.api_key is not None and self.inference_endpoint is not None
|
|
@@ -10,6 +10,7 @@ import aiohttp
|
|
|
10
10
|
from bs4 import BeautifulSoup
|
|
11
11
|
from markdownify import markdownify
|
|
12
12
|
|
|
13
|
+
from khoj.database.models import KhojUser
|
|
13
14
|
from khoj.routers.helpers import (
|
|
14
15
|
ChatEvent,
|
|
15
16
|
extract_relevant_info,
|
|
@@ -51,6 +52,8 @@ async def search_online(
|
|
|
51
52
|
query: str,
|
|
52
53
|
conversation_history: dict,
|
|
53
54
|
location: LocationData,
|
|
55
|
+
user: KhojUser,
|
|
56
|
+
subscribed: bool = False,
|
|
54
57
|
send_status_func: Optional[Callable] = None,
|
|
55
58
|
custom_filters: List[str] = [],
|
|
56
59
|
):
|
|
@@ -61,7 +64,7 @@ async def search_online(
|
|
|
61
64
|
return
|
|
62
65
|
|
|
63
66
|
# Breakdown the query into subqueries to get the correct answer
|
|
64
|
-
subqueries = await generate_online_subqueries(query, conversation_history, location)
|
|
67
|
+
subqueries = await generate_online_subqueries(query, conversation_history, location, user)
|
|
65
68
|
response_dict = {}
|
|
66
69
|
|
|
67
70
|
if subqueries:
|
|
@@ -89,12 +92,15 @@ async def search_online(
|
|
|
89
92
|
# Read, extract relevant info from the retrieved web pages
|
|
90
93
|
if webpages:
|
|
91
94
|
webpage_links = [link for link, _, _ in webpages]
|
|
92
|
-
logger.info(f"
|
|
95
|
+
logger.info(f"Reading web pages at: {list(webpage_links)}")
|
|
93
96
|
if send_status_func:
|
|
94
97
|
webpage_links_str = "\n- " + "\n- ".join(list(webpage_links))
|
|
95
98
|
async for event in send_status_func(f"**Reading web pages**: {webpage_links_str}"):
|
|
96
99
|
yield {ChatEvent.STATUS: event}
|
|
97
|
-
tasks = [
|
|
100
|
+
tasks = [
|
|
101
|
+
read_webpage_and_extract_content(subquery, link, content, subscribed=subscribed)
|
|
102
|
+
for link, subquery, content in webpages
|
|
103
|
+
]
|
|
98
104
|
results = await asyncio.gather(*tasks)
|
|
99
105
|
|
|
100
106
|
# Collect extracted info from the retrieved web pages
|
|
@@ -126,21 +132,26 @@ async def search_with_google(query: str) -> Tuple[str, Dict[str, List[Dict]]]:
|
|
|
126
132
|
|
|
127
133
|
|
|
128
134
|
async def read_webpages(
|
|
129
|
-
query: str,
|
|
135
|
+
query: str,
|
|
136
|
+
conversation_history: dict,
|
|
137
|
+
location: LocationData,
|
|
138
|
+
user: KhojUser,
|
|
139
|
+
subscribed: bool = False,
|
|
140
|
+
send_status_func: Optional[Callable] = None,
|
|
130
141
|
):
|
|
131
142
|
"Infer web pages to read from the query and extract relevant information from them"
|
|
132
143
|
logger.info(f"Inferring web pages to read")
|
|
133
144
|
if send_status_func:
|
|
134
145
|
async for event in send_status_func(f"**Inferring web pages to read**"):
|
|
135
146
|
yield {ChatEvent.STATUS: event}
|
|
136
|
-
urls = await infer_webpage_urls(query, conversation_history, location)
|
|
147
|
+
urls = await infer_webpage_urls(query, conversation_history, location, user)
|
|
137
148
|
|
|
138
149
|
logger.info(f"Reading web pages at: {urls}")
|
|
139
150
|
if send_status_func:
|
|
140
151
|
webpage_links_str = "\n- " + "\n- ".join(list(urls))
|
|
141
152
|
async for event in send_status_func(f"**Reading web pages**: {webpage_links_str}"):
|
|
142
153
|
yield {ChatEvent.STATUS: event}
|
|
143
|
-
tasks = [read_webpage_and_extract_content(query, url) for url in urls]
|
|
154
|
+
tasks = [read_webpage_and_extract_content(query, url, subscribed=subscribed) for url in urls]
|
|
144
155
|
results = await asyncio.gather(*tasks)
|
|
145
156
|
|
|
146
157
|
response: Dict[str, Dict] = defaultdict(dict)
|
|
@@ -151,14 +162,14 @@ async def read_webpages(
|
|
|
151
162
|
|
|
152
163
|
|
|
153
164
|
async def read_webpage_and_extract_content(
|
|
154
|
-
subquery: str, url: str, content: str = None
|
|
165
|
+
subquery: str, url: str, content: str = None, subscribed: bool = False
|
|
155
166
|
) -> Tuple[str, Union[None, str], str]:
|
|
156
167
|
try:
|
|
157
168
|
if is_none_or_empty(content):
|
|
158
169
|
with timer(f"Reading web page at '{url}' took", logger):
|
|
159
170
|
content = await read_webpage_with_olostep(url) if OLOSTEP_API_KEY else await read_webpage_with_jina(url)
|
|
160
171
|
with timer(f"Extracting relevant information from web page at '{url}' took", logger):
|
|
161
|
-
extracted_info = await extract_relevant_info(subquery, content)
|
|
172
|
+
extracted_info = await extract_relevant_info(subquery, content, subscribed=subscribed)
|
|
162
173
|
return subquery, extracted_info, url
|
|
163
174
|
except Exception as e:
|
|
164
175
|
logger.error(f"Failed to read web page at '{url}' with {e}")
|
khoj/routers/api.py
CHANGED
|
@@ -388,6 +388,7 @@ async def extract_references_and_questions(
|
|
|
388
388
|
conversation_log=meta_log,
|
|
389
389
|
should_extract_questions=True,
|
|
390
390
|
location_data=location_data,
|
|
391
|
+
user=user,
|
|
391
392
|
max_prompt_size=conversation_config.max_prompt_size,
|
|
392
393
|
)
|
|
393
394
|
elif conversation_config.model_type == ChatModelOptions.ModelType.OPENAI:
|
|
@@ -402,7 +403,7 @@ async def extract_references_and_questions(
|
|
|
402
403
|
api_base_url=base_url,
|
|
403
404
|
conversation_log=meta_log,
|
|
404
405
|
location_data=location_data,
|
|
405
|
-
|
|
406
|
+
user=user,
|
|
406
407
|
)
|
|
407
408
|
elif conversation_config.model_type == ChatModelOptions.ModelType.ANTHROPIC:
|
|
408
409
|
api_key = conversation_config.openai_config.api_key
|
|
@@ -413,6 +414,7 @@ async def extract_references_and_questions(
|
|
|
413
414
|
api_key=api_key,
|
|
414
415
|
conversation_log=meta_log,
|
|
415
416
|
location_data=location_data,
|
|
417
|
+
user=user,
|
|
416
418
|
)
|
|
417
419
|
|
|
418
420
|
# Collate search results as context for GPT
|
khoj/routers/api_chat.py
CHANGED
|
@@ -4,14 +4,14 @@ import logging
|
|
|
4
4
|
import time
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from functools import partial
|
|
7
|
-
from typing import
|
|
7
|
+
from typing import Dict, Optional
|
|
8
8
|
from urllib.parse import unquote
|
|
9
9
|
|
|
10
10
|
from asgiref.sync import sync_to_async
|
|
11
11
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
12
12
|
from fastapi.requests import Request
|
|
13
13
|
from fastapi.responses import Response, StreamingResponse
|
|
14
|
-
from starlette.authentication import requires
|
|
14
|
+
from starlette.authentication import has_required_scope, requires
|
|
15
15
|
|
|
16
16
|
from khoj.app.settings import ALLOWED_HOSTS
|
|
17
17
|
from khoj.database.adapters import (
|
|
@@ -59,7 +59,7 @@ from khoj.utils.rawconfig import FileFilterRequest, FilesFilterRequest, Location
|
|
|
59
59
|
# Initialize Router
|
|
60
60
|
logger = logging.getLogger(__name__)
|
|
61
61
|
conversation_command_rate_limiter = ConversationCommandRateLimiter(
|
|
62
|
-
trial_rate_limit=
|
|
62
|
+
trial_rate_limit=100, subscribed_rate_limit=100, slug="command"
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
|
|
@@ -532,10 +532,10 @@ async def chat(
|
|
|
532
532
|
country: Optional[str] = None,
|
|
533
533
|
timezone: Optional[str] = None,
|
|
534
534
|
rate_limiter_per_minute=Depends(
|
|
535
|
-
ApiUserRateLimiter(requests=
|
|
535
|
+
ApiUserRateLimiter(requests=60, subscribed_requests=60, window=60, slug="chat_minute")
|
|
536
536
|
),
|
|
537
537
|
rate_limiter_per_day=Depends(
|
|
538
|
-
ApiUserRateLimiter(requests=
|
|
538
|
+
ApiUserRateLimiter(requests=600, subscribed_requests=600, window=60 * 60 * 24, slug="chat_day")
|
|
539
539
|
),
|
|
540
540
|
):
|
|
541
541
|
async def event_generator(q: str):
|
|
@@ -544,6 +544,7 @@ async def chat(
|
|
|
544
544
|
chat_metadata: dict = {}
|
|
545
545
|
connection_alive = True
|
|
546
546
|
user: KhojUser = request.user.object
|
|
547
|
+
subscribed: bool = has_required_scope(request, ["premium"])
|
|
547
548
|
event_delimiter = "␃🔚␗"
|
|
548
549
|
q = unquote(q)
|
|
549
550
|
|
|
@@ -632,7 +633,9 @@ async def chat(
|
|
|
632
633
|
is_automated_task = conversation_commands == [ConversationCommand.AutomatedTask]
|
|
633
634
|
|
|
634
635
|
if conversation_commands == [ConversationCommand.Default] or is_automated_task:
|
|
635
|
-
conversation_commands = await aget_relevant_information_sources(
|
|
636
|
+
conversation_commands = await aget_relevant_information_sources(
|
|
637
|
+
q, meta_log, is_automated_task, subscribed=subscribed
|
|
638
|
+
)
|
|
636
639
|
conversation_commands_str = ", ".join([cmd.value for cmd in conversation_commands])
|
|
637
640
|
async for result in send_event(
|
|
638
641
|
ChatEvent.STATUS, f"**Chose Data Sources to Search:** {conversation_commands_str}"
|
|
@@ -687,7 +690,7 @@ async def chat(
|
|
|
687
690
|
):
|
|
688
691
|
yield result
|
|
689
692
|
|
|
690
|
-
response = await extract_relevant_summary(q, contextual_data)
|
|
693
|
+
response = await extract_relevant_summary(q, contextual_data, subscribed=subscribed)
|
|
691
694
|
response_log = str(response)
|
|
692
695
|
async for result in send_llm_response(response_log):
|
|
693
696
|
yield result
|
|
@@ -775,6 +778,8 @@ async def chat(
|
|
|
775
778
|
|
|
776
779
|
if not is_none_or_empty(compiled_references):
|
|
777
780
|
headings = "\n- " + "\n- ".join(set([c.get("compiled", c).split("\n")[0] for c in compiled_references]))
|
|
781
|
+
# Strip only leading # from headings
|
|
782
|
+
headings = headings.replace("#", "")
|
|
778
783
|
async for result in send_event(ChatEvent.STATUS, f"**Found Relevant Notes**: {headings}"):
|
|
779
784
|
yield result
|
|
780
785
|
|
|
@@ -792,7 +797,13 @@ async def chat(
|
|
|
792
797
|
if ConversationCommand.Online in conversation_commands:
|
|
793
798
|
try:
|
|
794
799
|
async for result in search_online(
|
|
795
|
-
defiltered_query,
|
|
800
|
+
defiltered_query,
|
|
801
|
+
meta_log,
|
|
802
|
+
location,
|
|
803
|
+
user,
|
|
804
|
+
subscribed,
|
|
805
|
+
partial(send_event, ChatEvent.STATUS),
|
|
806
|
+
custom_filters,
|
|
796
807
|
):
|
|
797
808
|
if isinstance(result, dict) and ChatEvent.STATUS in result:
|
|
798
809
|
yield result[ChatEvent.STATUS]
|
|
@@ -809,7 +820,7 @@ async def chat(
|
|
|
809
820
|
if ConversationCommand.Webpage in conversation_commands:
|
|
810
821
|
try:
|
|
811
822
|
async for result in read_webpages(
|
|
812
|
-
defiltered_query, meta_log, location, partial(send_event, ChatEvent.STATUS)
|
|
823
|
+
defiltered_query, meta_log, location, user, subscribed, partial(send_event, ChatEvent.STATUS)
|
|
813
824
|
):
|
|
814
825
|
if isinstance(result, dict) and ChatEvent.STATUS in result:
|
|
815
826
|
yield result[ChatEvent.STATUS]
|
|
@@ -853,6 +864,7 @@ async def chat(
|
|
|
853
864
|
location_data=location,
|
|
854
865
|
references=compiled_references,
|
|
855
866
|
online_results=online_results,
|
|
867
|
+
subscribed=subscribed,
|
|
856
868
|
send_status_func=partial(send_event, ChatEvent.STATUS),
|
|
857
869
|
):
|
|
858
870
|
if isinstance(result, dict) and ChatEvent.STATUS in result:
|
khoj/routers/helpers.py
CHANGED
|
@@ -252,7 +252,7 @@ async def acreate_title_from_query(query: str) -> str:
|
|
|
252
252
|
return response.strip()
|
|
253
253
|
|
|
254
254
|
|
|
255
|
-
async def aget_relevant_information_sources(query: str, conversation_history: dict, is_task: bool):
|
|
255
|
+
async def aget_relevant_information_sources(query: str, conversation_history: dict, is_task: bool, subscribed: bool):
|
|
256
256
|
"""
|
|
257
257
|
Given a query, determine which of the available tools the agent should use in order to answer appropriately.
|
|
258
258
|
"""
|
|
@@ -273,7 +273,9 @@ async def aget_relevant_information_sources(query: str, conversation_history: di
|
|
|
273
273
|
)
|
|
274
274
|
|
|
275
275
|
with timer("Chat actor: Infer information sources to refer", logger):
|
|
276
|
-
response = await send_message_to_model_wrapper(
|
|
276
|
+
response = await send_message_to_model_wrapper(
|
|
277
|
+
relevant_tools_prompt, response_type="json_object", subscribed=subscribed
|
|
278
|
+
)
|
|
277
279
|
|
|
278
280
|
try:
|
|
279
281
|
response = response.strip()
|
|
@@ -340,11 +342,14 @@ async def aget_relevant_output_modes(query: str, conversation_history: dict, is_
|
|
|
340
342
|
return ConversationCommand.Text
|
|
341
343
|
|
|
342
344
|
|
|
343
|
-
async def infer_webpage_urls(
|
|
345
|
+
async def infer_webpage_urls(
|
|
346
|
+
q: str, conversation_history: dict, location_data: LocationData, user: KhojUser
|
|
347
|
+
) -> List[str]:
|
|
344
348
|
"""
|
|
345
349
|
Infer webpage links from the given query
|
|
346
350
|
"""
|
|
347
351
|
location = f"{location_data.city}, {location_data.region}, {location_data.country}" if location_data else "Unknown"
|
|
352
|
+
username = prompts.user_name.format(name=user.get_full_name()) if user.get_full_name() else ""
|
|
348
353
|
chat_history = construct_chat_history(conversation_history)
|
|
349
354
|
|
|
350
355
|
utc_date = datetime.utcnow().strftime("%Y-%m-%d")
|
|
@@ -353,6 +358,7 @@ async def infer_webpage_urls(q: str, conversation_history: dict, location_data:
|
|
|
353
358
|
query=q,
|
|
354
359
|
chat_history=chat_history,
|
|
355
360
|
location=location,
|
|
361
|
+
username=username,
|
|
356
362
|
)
|
|
357
363
|
|
|
358
364
|
with timer("Chat actor: Infer webpage urls to read", logger):
|
|
@@ -370,11 +376,14 @@ async def infer_webpage_urls(q: str, conversation_history: dict, location_data:
|
|
|
370
376
|
raise ValueError(f"Invalid list of urls: {response}")
|
|
371
377
|
|
|
372
378
|
|
|
373
|
-
async def generate_online_subqueries(
|
|
379
|
+
async def generate_online_subqueries(
|
|
380
|
+
q: str, conversation_history: dict, location_data: LocationData, user: KhojUser
|
|
381
|
+
) -> List[str]:
|
|
374
382
|
"""
|
|
375
383
|
Generate subqueries from the given query
|
|
376
384
|
"""
|
|
377
385
|
location = f"{location_data.city}, {location_data.region}, {location_data.country}" if location_data else "Unknown"
|
|
386
|
+
username = prompts.user_name.format(name=user.get_full_name()) if user.get_full_name() else ""
|
|
378
387
|
chat_history = construct_chat_history(conversation_history)
|
|
379
388
|
|
|
380
389
|
utc_date = datetime.utcnow().strftime("%Y-%m-%d")
|
|
@@ -383,6 +392,7 @@ async def generate_online_subqueries(q: str, conversation_history: dict, locatio
|
|
|
383
392
|
query=q,
|
|
384
393
|
chat_history=chat_history,
|
|
385
394
|
location=location,
|
|
395
|
+
username=username,
|
|
386
396
|
)
|
|
387
397
|
|
|
388
398
|
with timer("Chat actor: Generate online search subqueries", logger):
|
|
@@ -426,7 +436,7 @@ async def schedule_query(q: str, conversation_history: dict) -> Tuple[str, ...]:
|
|
|
426
436
|
raise AssertionError(f"Invalid response for scheduling query: {raw_response}")
|
|
427
437
|
|
|
428
438
|
|
|
429
|
-
async def extract_relevant_info(q: str, corpus: str) -> Union[str, None]:
|
|
439
|
+
async def extract_relevant_info(q: str, corpus: str, subscribed: bool) -> Union[str, None]:
|
|
430
440
|
"""
|
|
431
441
|
Extract relevant information for a given query from the target corpus
|
|
432
442
|
"""
|
|
@@ -439,18 +449,19 @@ async def extract_relevant_info(q: str, corpus: str) -> Union[str, None]:
|
|
|
439
449
|
corpus=corpus.strip(),
|
|
440
450
|
)
|
|
441
451
|
|
|
442
|
-
|
|
452
|
+
chat_model: ChatModelOptions = await ConversationAdapters.aget_default_conversation_config()
|
|
443
453
|
|
|
444
454
|
with timer("Chat actor: Extract relevant information from data", logger):
|
|
445
455
|
response = await send_message_to_model_wrapper(
|
|
446
456
|
extract_relevant_information,
|
|
447
457
|
prompts.system_prompt_extract_relevant_information,
|
|
448
|
-
chat_model_option=
|
|
458
|
+
chat_model_option=chat_model,
|
|
459
|
+
subscribed=subscribed,
|
|
449
460
|
)
|
|
450
461
|
return response.strip()
|
|
451
462
|
|
|
452
463
|
|
|
453
|
-
async def extract_relevant_summary(q: str, corpus: str) -> Union[str, None]:
|
|
464
|
+
async def extract_relevant_summary(q: str, corpus: str, subscribed: bool = False) -> Union[str, None]:
|
|
454
465
|
"""
|
|
455
466
|
Extract relevant information for a given query from the target corpus
|
|
456
467
|
"""
|
|
@@ -463,13 +474,14 @@ async def extract_relevant_summary(q: str, corpus: str) -> Union[str, None]:
|
|
|
463
474
|
corpus=corpus.strip(),
|
|
464
475
|
)
|
|
465
476
|
|
|
466
|
-
|
|
477
|
+
chat_model: ChatModelOptions = await ConversationAdapters.aget_default_conversation_config()
|
|
467
478
|
|
|
468
479
|
with timer("Chat actor: Extract relevant information from data", logger):
|
|
469
480
|
response = await send_message_to_model_wrapper(
|
|
470
481
|
extract_relevant_information,
|
|
471
482
|
prompts.system_prompt_extract_relevant_summary,
|
|
472
|
-
chat_model_option=
|
|
483
|
+
chat_model_option=chat_model,
|
|
484
|
+
subscribed=subscribed,
|
|
473
485
|
)
|
|
474
486
|
return response.strip()
|
|
475
487
|
|
|
@@ -481,6 +493,7 @@ async def generate_better_image_prompt(
|
|
|
481
493
|
note_references: List[Dict[str, Any]],
|
|
482
494
|
online_results: Optional[dict] = None,
|
|
483
495
|
model_type: Optional[str] = None,
|
|
496
|
+
subscribed: bool = False,
|
|
484
497
|
) -> str:
|
|
485
498
|
"""
|
|
486
499
|
Generate a better image prompt from the given query
|
|
@@ -525,10 +538,12 @@ async def generate_better_image_prompt(
|
|
|
525
538
|
online_results=simplified_online_results,
|
|
526
539
|
)
|
|
527
540
|
|
|
528
|
-
|
|
541
|
+
chat_model: ChatModelOptions = await ConversationAdapters.aget_default_conversation_config()
|
|
529
542
|
|
|
530
543
|
with timer("Chat actor: Generate contextual image prompt", logger):
|
|
531
|
-
response = await send_message_to_model_wrapper(
|
|
544
|
+
response = await send_message_to_model_wrapper(
|
|
545
|
+
image_prompt, chat_model_option=chat_model, subscribed=subscribed
|
|
546
|
+
)
|
|
532
547
|
response = response.strip()
|
|
533
548
|
if response.startswith(('"', "'")) and response.endswith(('"', "'")):
|
|
534
549
|
response = response[1:-1]
|
|
@@ -541,13 +556,18 @@ async def send_message_to_model_wrapper(
|
|
|
541
556
|
system_message: str = "",
|
|
542
557
|
response_type: str = "text",
|
|
543
558
|
chat_model_option: ChatModelOptions = None,
|
|
559
|
+
subscribed: bool = False,
|
|
544
560
|
):
|
|
545
561
|
conversation_config: ChatModelOptions = (
|
|
546
562
|
chat_model_option or await ConversationAdapters.aget_default_conversation_config()
|
|
547
563
|
)
|
|
548
564
|
|
|
549
565
|
chat_model = conversation_config.chat_model
|
|
550
|
-
max_tokens =
|
|
566
|
+
max_tokens = (
|
|
567
|
+
conversation_config.subscribed_max_prompt_size
|
|
568
|
+
if subscribed and conversation_config.subscribed_max_prompt_size
|
|
569
|
+
else conversation_config.max_prompt_size
|
|
570
|
+
)
|
|
551
571
|
tokenizer = conversation_config.tokenizer
|
|
552
572
|
|
|
553
573
|
if conversation_config.model_type == "offline":
|
|
@@ -778,6 +798,7 @@ async def text_to_image(
|
|
|
778
798
|
location_data: LocationData,
|
|
779
799
|
references: List[Dict[str, Any]],
|
|
780
800
|
online_results: Dict[str, Any],
|
|
801
|
+
subscribed: bool = False,
|
|
781
802
|
send_status_func: Optional[Callable] = None,
|
|
782
803
|
):
|
|
783
804
|
status_code = 200
|
|
@@ -814,6 +835,7 @@ async def text_to_image(
|
|
|
814
835
|
note_references=references,
|
|
815
836
|
online_results=online_results,
|
|
816
837
|
model_type=text_to_image_config.model_type,
|
|
838
|
+
subscribed=subscribed,
|
|
817
839
|
)
|
|
818
840
|
|
|
819
841
|
if send_status_func:
|
|
@@ -1351,7 +1373,9 @@ def get_user_config(user: KhojUser, request: Request, is_detailed: bool = False)
|
|
|
1351
1373
|
current_notion_config = get_user_notion_config(user)
|
|
1352
1374
|
notion_token = current_notion_config.token if current_notion_config else ""
|
|
1353
1375
|
|
|
1354
|
-
selected_chat_model_config =
|
|
1376
|
+
selected_chat_model_config = (
|
|
1377
|
+
ConversationAdapters.get_conversation_config(user) or ConversationAdapters.get_default_conversation_config()
|
|
1378
|
+
)
|
|
1355
1379
|
chat_models = ConversationAdapters.get_conversation_processor_options().all()
|
|
1356
1380
|
chat_model_options = list()
|
|
1357
1381
|
for chat_model in chat_models:
|
|
@@ -11,7 +11,8 @@ logger = logging.getLogger(__name__)
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class FileFilter(BaseFilter):
|
|
14
|
-
file_filter_regex = r'file:"(.+?)" ?'
|
|
14
|
+
file_filter_regex = r'(?<!-)file:"(.+?)" ?'
|
|
15
|
+
excluded_file_filter_regex = r'-file:"(.+?)" ?'
|
|
15
16
|
|
|
16
17
|
def __init__(self, entry_key="file"):
|
|
17
18
|
self.entry_key = entry_key
|
|
@@ -20,7 +21,9 @@ class FileFilter(BaseFilter):
|
|
|
20
21
|
|
|
21
22
|
def get_filter_terms(self, query: str) -> List[str]:
|
|
22
23
|
"Get all filter terms in query"
|
|
23
|
-
|
|
24
|
+
required_files = [f"{required_file}" for required_file in re.findall(self.file_filter_regex, query)]
|
|
25
|
+
excluded_files = [f"-{excluded_file}" for excluded_file in re.findall(self.excluded_file_filter_regex, query)]
|
|
26
|
+
return required_files + excluded_files
|
|
24
27
|
|
|
25
28
|
def convert_to_regex(self, file_filter: str) -> str:
|
|
26
29
|
"Convert file filter to regex"
|