khoj 1.36.7.dev1__py3-none-any.whl → 1.36.7.dev18__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/database/adapters/__init__.py +29 -0
- khoj/interface/compiled/404/index.html +2 -2
- khoj/interface/compiled/_next/static/chunks/{2327-36d17f2483e80f60.js → 2327-02e86a50c65e575a.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/{8155-87b4d2ea2cf725cc.js → 8155-ad130153ddcc930f.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/agents/{layout-447b58869479276c.js → layout-4a0e32561d6b1e27.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/agents/{page-2f55f9d0da49bf31.js → page-fbe2c1c661cd14ac.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/automations/{page-d0a630a2b4ecc41d.js → page-ad620b194fd508fe.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/{layout-4d0b1ba93124fccb.js → layout-9e151fb837f53026.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/{page-9c0a2df3e33c029b.js → page-24bf7a5c917dbaff.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/{page-642bd02fc4f16606.js → page-528e96d17e520304.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/search/{layout-ff081947c70ea9b7.js → layout-66f736b858b38c2c.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/search/{page-7c80e369ee1cdfad.js → page-30e231665f1f3796.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/settings/page-c580520d59d92267.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/share/chat/{layout-94a33aa0eae034fc.js → layout-2ce0cb95b1219d97.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/share/chat/{page-6253896a84300e9b.js → page-975934b12916f3d4.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/{webpack-63715907cbdf16c6.js → webpack-32b7cc428f0d015b.js} +1 -1
- khoj/interface/compiled/_next/static/css/017016e6eff88fdc.css +1 -0
- khoj/interface/compiled/_next/static/css/440ae0f0f650dc35.css +1 -0
- khoj/interface/compiled/_next/static/css/f29752d6e1be7624.css +1 -0
- khoj/interface/compiled/agents/index.html +2 -2
- khoj/interface/compiled/agents/index.txt +3 -3
- khoj/interface/compiled/automations/index.html +2 -2
- khoj/interface/compiled/automations/index.txt +3 -3
- khoj/interface/compiled/chat/index.html +2 -2
- khoj/interface/compiled/chat/index.txt +3 -3
- khoj/interface/compiled/index.html +2 -2
- khoj/interface/compiled/index.txt +2 -2
- khoj/interface/compiled/search/index.html +2 -2
- khoj/interface/compiled/search/index.txt +3 -3
- khoj/interface/compiled/settings/index.html +2 -2
- khoj/interface/compiled/settings/index.txt +5 -5
- khoj/interface/compiled/share/chat/index.html +2 -2
- khoj/interface/compiled/share/chat/index.txt +3 -3
- khoj/processor/conversation/google/gemini_chat.py +7 -7
- khoj/processor/conversation/prompts.py +100 -19
- khoj/processor/tools/run_code.py +163 -21
- khoj/routers/api_content.py +37 -3
- khoj/utils/constants.py +2 -1
- khoj/utils/helpers.py +11 -2
- khoj/utils/initialization.py +24 -7
- {khoj-1.36.7.dev1.dist-info → khoj-1.36.7.dev18.dist-info}/METADATA +3 -2
- {khoj-1.36.7.dev1.dist-info → khoj-1.36.7.dev18.dist-info}/RECORD +53 -53
- khoj/interface/compiled/_next/static/chunks/app/settings/page-9939a20613b25901.js +0 -1
- khoj/interface/compiled/_next/static/css/55d4a822f8d94b67.css +0 -1
- khoj/interface/compiled/_next/static/css/804ceddd6c935d4a.css +0 -1
- khoj/interface/compiled/_next/static/css/b15666ef52060cd0.css +0 -1
- /khoj/interface/compiled/_next/static/{ft5KXHI1GLT3L6pQPfg8R → SaAC14w_LNITF1YZanO67}/_buildManifest.js +0 -0
- /khoj/interface/compiled/_next/static/{ft5KXHI1GLT3L6pQPfg8R → SaAC14w_LNITF1YZanO67}/_ssgManifest.js +0 -0
- /khoj/interface/compiled/_next/static/chunks/{1915-233ac8a122732d6b.js → 1915-4b7980a58fb630d6.js} +0 -0
- /khoj/interface/compiled/_next/static/chunks/{2117-ce1f0a4598f5e4fe.js → 2117-f99825f0a867a42d.js} +0 -0
- /khoj/interface/compiled/_next/static/chunks/{4363-9870bda67c2cf031.js → 4363-ac51bce40b6fc313.js} +0 -0
- /khoj/interface/compiled/_next/static/chunks/{4447-6e47461d1100c3cc.js → 4447-30959771ff58d99d.js} +0 -0
- /khoj/interface/compiled/_next/static/chunks/{8667-8136f74e9a086fca.js → 8667-adbe6017a66cef10.js} +0 -0
- /khoj/interface/compiled/_next/static/chunks/{9259-fa40e7cf2ca28e04.js → 9259-5be50737cfe989bc.js} +0 -0
- {khoj-1.36.7.dev1.dist-info → khoj-1.36.7.dev18.dist-info}/WHEEL +0 -0
- {khoj-1.36.7.dev1.dist-info → khoj-1.36.7.dev18.dist-info}/entry_points.txt +0 -0
- {khoj-1.36.7.dev1.dist-info → khoj-1.36.7.dev18.dist-info}/licenses/LICENSE +0 -0
khoj/processor/tools/run_code.py
CHANGED
@@ -1,12 +1,23 @@
|
|
1
|
+
import asyncio
|
1
2
|
import base64
|
2
3
|
import datetime
|
3
4
|
import logging
|
4
5
|
import mimetypes
|
5
6
|
import os
|
7
|
+
import re
|
6
8
|
from pathlib import Path
|
7
9
|
from typing import Any, Callable, List, NamedTuple, Optional
|
8
10
|
|
9
11
|
import aiohttp
|
12
|
+
from asgiref.sync import sync_to_async
|
13
|
+
from httpx import RemoteProtocolError
|
14
|
+
from tenacity import (
|
15
|
+
before_sleep_log,
|
16
|
+
retry,
|
17
|
+
retry_if_exception_type,
|
18
|
+
stop_after_attempt,
|
19
|
+
wait_random_exponential,
|
20
|
+
)
|
10
21
|
|
11
22
|
from khoj.database.adapters import FileObjectAdapters
|
12
23
|
from khoj.database.models import Agent, FileObject, KhojUser
|
@@ -15,22 +26,26 @@ from khoj.processor.conversation.utils import (
|
|
15
26
|
ChatEvent,
|
16
27
|
clean_code_python,
|
17
28
|
construct_chat_history,
|
18
|
-
load_complex_json,
|
19
29
|
)
|
20
30
|
from khoj.routers.helpers import send_message_to_model_wrapper
|
21
|
-
from khoj.utils.helpers import
|
31
|
+
from khoj.utils.helpers import (
|
32
|
+
is_e2b_code_sandbox_enabled,
|
33
|
+
is_none_or_empty,
|
34
|
+
timer,
|
35
|
+
truncate_code_context,
|
36
|
+
)
|
22
37
|
from khoj.utils.rawconfig import LocationData
|
23
38
|
|
24
39
|
logger = logging.getLogger(__name__)
|
25
40
|
|
26
41
|
|
27
42
|
SANDBOX_URL = os.getenv("KHOJ_TERRARIUM_URL", "http://localhost:8080")
|
43
|
+
DEFAULT_E2B_TEMPLATE = "pmt2o0ghpang8gbiys57"
|
28
44
|
|
29
45
|
|
30
46
|
class GeneratedCode(NamedTuple):
|
31
47
|
code: str
|
32
|
-
input_files: List[
|
33
|
-
input_links: List[str]
|
48
|
+
input_files: List[FileObject]
|
34
49
|
|
35
50
|
|
36
51
|
async def run_code(
|
@@ -68,13 +83,10 @@ async def run_code(
|
|
68
83
|
|
69
84
|
# Prepare Input Data
|
70
85
|
input_data = []
|
71
|
-
|
72
|
-
for input_file in generated_code.input_files:
|
73
|
-
user_input_files += await FileObjectAdapters.aget_file_objects_by_name(user, input_file)
|
74
|
-
for f in user_input_files:
|
86
|
+
for f in generated_code.input_files:
|
75
87
|
input_data.append(
|
76
88
|
{
|
77
|
-
"filename":
|
89
|
+
"filename": f.file_name,
|
78
90
|
"b64_data": base64.b64encode(f.raw_text.encode("utf-8")).decode("utf-8"),
|
79
91
|
}
|
80
92
|
)
|
@@ -90,6 +102,14 @@ async def run_code(
|
|
90
102
|
cleaned_result = truncate_code_context({"cleaned": {"results": result}})["cleaned"]["results"]
|
91
103
|
logger.info(f"Executed Code\n----\n{code}\n----\nResult\n----\n{cleaned_result}\n----")
|
92
104
|
yield {query: {"code": code, "results": result}}
|
105
|
+
except asyncio.TimeoutError as e:
|
106
|
+
# Call the sandbox_url/stop GET API endpoint to stop the code sandbox
|
107
|
+
error = f"Failed to run code for {query} with Timeout error: {e}"
|
108
|
+
try:
|
109
|
+
await aiohttp.ClientSession().get(f"{sandbox_url}/stop", timeout=5)
|
110
|
+
except Exception as e:
|
111
|
+
error += f"\n\nFailed to stop code sandbox with error: {e}"
|
112
|
+
raise ValueError(error)
|
93
113
|
except Exception as e:
|
94
114
|
raise ValueError(f"Failed to run code for {query} with error: {e}")
|
95
115
|
|
@@ -114,6 +134,12 @@ async def generate_python_code(
|
|
114
134
|
prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
|
115
135
|
)
|
116
136
|
|
137
|
+
# add sandbox specific context like available packages
|
138
|
+
sandbox_context = (
|
139
|
+
prompts.e2b_sandbox_context if is_e2b_code_sandbox_enabled() else prompts.terrarium_sandbox_context
|
140
|
+
)
|
141
|
+
personality_context = f"{sandbox_context}\n{personality_context}"
|
142
|
+
|
117
143
|
code_generation_prompt = prompts.python_code_generation_prompt.format(
|
118
144
|
current_date=utc_date,
|
119
145
|
query=q,
|
@@ -127,23 +153,50 @@ async def generate_python_code(
|
|
127
153
|
response = await send_message_to_model_wrapper(
|
128
154
|
code_generation_prompt,
|
129
155
|
query_images=query_images,
|
130
|
-
response_type="json_object",
|
131
156
|
user=user,
|
132
157
|
tracer=tracer,
|
133
158
|
query_files=query_files,
|
134
159
|
)
|
135
160
|
|
136
|
-
#
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
161
|
+
# Extract python code wrapped in markdown code blocks from the response
|
162
|
+
code_blocks = re.findall(r"```(?:python)?\n(.*?)\n```", response, re.DOTALL)
|
163
|
+
|
164
|
+
if not code_blocks:
|
165
|
+
raise ValueError("No Python code blocks found in response")
|
166
|
+
|
167
|
+
# Join multiple code blocks with newlines and strip any leading/trailing whitespace
|
168
|
+
code = "\n".join(code_blocks).strip()
|
141
169
|
|
142
170
|
if not isinstance(code, str) or is_none_or_empty(code):
|
143
171
|
raise ValueError
|
144
|
-
return GeneratedCode(code, input_files, input_links)
|
145
172
|
|
173
|
+
# Infer user files required in sandbox based on user file paths mentioned in code
|
174
|
+
input_files: List[FileObject] = []
|
175
|
+
user_files = await sync_to_async(set)(FileObjectAdapters.get_all_file_objects(user))
|
176
|
+
for user_file in user_files:
|
177
|
+
if user_file.file_name in code:
|
178
|
+
# Replace references to full file path used in code with just the file basename to ease reference in sandbox
|
179
|
+
file_basename = os.path.basename(user_file.file_name)
|
180
|
+
code = code.replace(user_file.file_name, file_basename)
|
181
|
+
user_file.file_name = file_basename
|
182
|
+
input_files.append(user_file)
|
183
|
+
|
184
|
+
return GeneratedCode(code, input_files)
|
146
185
|
|
186
|
+
|
187
|
+
@retry(
|
188
|
+
retry=(
|
189
|
+
retry_if_exception_type(aiohttp.ClientError)
|
190
|
+
| retry_if_exception_type(aiohttp.ClientTimeout)
|
191
|
+
| retry_if_exception_type(asyncio.TimeoutError)
|
192
|
+
| retry_if_exception_type(ConnectionError)
|
193
|
+
| retry_if_exception_type(RemoteProtocolError)
|
194
|
+
),
|
195
|
+
wait=wait_random_exponential(min=1, max=5),
|
196
|
+
stop=stop_after_attempt(3),
|
197
|
+
before_sleep=before_sleep_log(logger, logging.DEBUG),
|
198
|
+
reraise=True,
|
199
|
+
)
|
147
200
|
async def execute_sandboxed_python(code: str, input_data: list[dict], sandbox_url: str = SANDBOX_URL) -> dict[str, Any]:
|
148
201
|
"""
|
149
202
|
Takes code to run as a string and calls the terrarium API to execute it.
|
@@ -152,15 +205,104 @@ async def execute_sandboxed_python(code: str, input_data: list[dict], sandbox_ur
|
|
152
205
|
Reference data i/o format based on Terrarium example client code at:
|
153
206
|
https://github.com/cohere-ai/cohere-terrarium/blob/main/example-clients/python/terrarium_client.py
|
154
207
|
"""
|
155
|
-
headers = {"Content-Type": "application/json"}
|
156
208
|
cleaned_code = clean_code_python(code)
|
157
|
-
|
209
|
+
if is_e2b_code_sandbox_enabled():
|
210
|
+
try:
|
211
|
+
return await execute_e2b(cleaned_code, input_data)
|
212
|
+
except ImportError:
|
213
|
+
pass
|
214
|
+
return await execute_terrarium(cleaned_code, input_data, sandbox_url)
|
215
|
+
|
216
|
+
|
217
|
+
async def execute_e2b(code: str, input_files: list[dict]) -> dict[str, Any]:
|
218
|
+
"""Execute code and handle file I/O in e2b sandbox"""
|
219
|
+
from e2b_code_interpreter import AsyncSandbox
|
220
|
+
|
221
|
+
sandbox = await AsyncSandbox.create(
|
222
|
+
api_key=os.getenv("E2B_API_KEY"),
|
223
|
+
template=os.getenv("E2B_TEMPLATE", DEFAULT_E2B_TEMPLATE),
|
224
|
+
timeout=120,
|
225
|
+
request_timeout=30,
|
226
|
+
)
|
158
227
|
|
228
|
+
try:
|
229
|
+
# Upload input files in parallel
|
230
|
+
upload_tasks = [
|
231
|
+
sandbox.files.write(path=file["filename"], data=base64.b64decode(file["b64_data"]), request_timeout=30)
|
232
|
+
for file in input_files
|
233
|
+
]
|
234
|
+
await asyncio.gather(*upload_tasks)
|
235
|
+
|
236
|
+
# Note stored files before execution to identify new files created during execution
|
237
|
+
E2bFile = NamedTuple("E2bFile", [("name", str), ("path", str)])
|
238
|
+
original_files = {E2bFile(f.name, f.path) for f in await sandbox.files.list("~")}
|
239
|
+
|
240
|
+
# Execute code from main.py file
|
241
|
+
execution = await sandbox.run_code(code=code, timeout=60)
|
242
|
+
|
243
|
+
# Collect output files
|
244
|
+
output_files = []
|
245
|
+
|
246
|
+
# Identify new files created during execution
|
247
|
+
new_files = set(E2bFile(f.name, f.path) for f in await sandbox.files.list("~")) - original_files
|
248
|
+
# Read newly created files in parallel
|
249
|
+
download_tasks = [sandbox.files.read(f.path, request_timeout=30) for f in new_files]
|
250
|
+
downloaded_files = await asyncio.gather(*download_tasks)
|
251
|
+
for f, content in zip(new_files, downloaded_files):
|
252
|
+
if isinstance(content, bytes):
|
253
|
+
# Binary files like PNG - encode as base64
|
254
|
+
b64_data = base64.b64encode(content).decode("utf-8")
|
255
|
+
elif Path(f.name).suffix in [".png", ".jpeg", ".jpg", ".svg"]:
|
256
|
+
# Ignore image files as they are extracted from execution results below for inline display
|
257
|
+
continue
|
258
|
+
else:
|
259
|
+
# Text files - encode utf-8 string as base64
|
260
|
+
b64_data = base64.b64encode(content.encode("utf-8")).decode("utf-8")
|
261
|
+
output_files.append({"filename": f.name, "b64_data": b64_data})
|
262
|
+
|
263
|
+
# Collect output files from execution results
|
264
|
+
for idx, result in enumerate(execution.results):
|
265
|
+
for result_type in {"png", "jpeg", "svg", "text", "markdown", "json"}:
|
266
|
+
if b64_data := getattr(result, result_type, None):
|
267
|
+
output_files.append({"filename": f"{idx}.{result_type}", "b64_data": b64_data})
|
268
|
+
break
|
269
|
+
|
270
|
+
# collect logs
|
271
|
+
success = not execution.error and not execution.logs.stderr
|
272
|
+
stdout = "\n".join(execution.logs.stdout)
|
273
|
+
errors = "\n".join(execution.logs.stderr)
|
274
|
+
if execution.error:
|
275
|
+
errors = f"{execution.error}\n{errors}"
|
276
|
+
|
277
|
+
return {
|
278
|
+
"code": code,
|
279
|
+
"success": success,
|
280
|
+
"std_out": stdout,
|
281
|
+
"std_err": errors,
|
282
|
+
"output_files": output_files,
|
283
|
+
}
|
284
|
+
except Exception as e:
|
285
|
+
return {
|
286
|
+
"code": code,
|
287
|
+
"success": False,
|
288
|
+
"std_err": f"Sandbox failed to execute code: {str(e)}",
|
289
|
+
"output_files": [],
|
290
|
+
}
|
291
|
+
|
292
|
+
|
293
|
+
async def execute_terrarium(
|
294
|
+
code: str,
|
295
|
+
input_data: list[dict],
|
296
|
+
sandbox_url: str,
|
297
|
+
) -> dict[str, Any]:
|
298
|
+
"""Execute code using Terrarium sandbox"""
|
299
|
+
headers = {"Content-Type": "application/json"}
|
300
|
+
data = {"code": code, "files": input_data}
|
159
301
|
async with aiohttp.ClientSession() as session:
|
160
|
-
async with session.post(sandbox_url, json=data, headers=headers) as response:
|
302
|
+
async with session.post(sandbox_url, json=data, headers=headers, timeout=30) as response:
|
161
303
|
if response.status == 200:
|
162
304
|
result: dict[str, Any] = await response.json()
|
163
|
-
result["code"] =
|
305
|
+
result["code"] = code
|
164
306
|
# Store decoded output files
|
165
307
|
result["output_files"] = result.get("output_files", [])
|
166
308
|
for output_file in result["output_files"]:
|
@@ -172,7 +314,7 @@ async def execute_sandboxed_python(code: str, input_data: list[dict], sandbox_ur
|
|
172
314
|
return result
|
173
315
|
else:
|
174
316
|
return {
|
175
|
-
"code":
|
317
|
+
"code": code,
|
176
318
|
"success": False,
|
177
319
|
"std_err": f"Failed to execute code with {response.status}",
|
178
320
|
"output_files": [],
|
khoj/routers/api_content.py
CHANGED
@@ -401,6 +401,36 @@ async def get_file_object(
|
|
401
401
|
)
|
402
402
|
|
403
403
|
|
404
|
+
@api_content.delete("/type/{content_type}", status_code=200)
|
405
|
+
@requires(["authenticated"])
|
406
|
+
async def delete_content_type(
|
407
|
+
request: Request,
|
408
|
+
content_type: str,
|
409
|
+
client: Optional[str] = None,
|
410
|
+
):
|
411
|
+
user = request.user.object
|
412
|
+
if content_type not in {s.value for s in SearchType}:
|
413
|
+
raise ValueError(f"Unsupported content type: {content_type}")
|
414
|
+
if content_type == "all":
|
415
|
+
await EntryAdapters.adelete_all_entries(user)
|
416
|
+
else:
|
417
|
+
# Delete file objects of the given type
|
418
|
+
file_list = await sync_to_async(list)(EntryAdapters.get_all_filenames_by_type(user, content_type)) # type: ignore[call-arg]
|
419
|
+
await FileObjectAdapters.adelete_file_objects_by_names(user, file_list)
|
420
|
+
# Delete entries of the given type
|
421
|
+
await EntryAdapters.adelete_all_entries(user, file_type=content_type)
|
422
|
+
|
423
|
+
update_telemetry_state(
|
424
|
+
request=request,
|
425
|
+
telemetry_type="api",
|
426
|
+
api="delete_content_config",
|
427
|
+
client=client,
|
428
|
+
metadata={"content_type": content_type},
|
429
|
+
)
|
430
|
+
|
431
|
+
return {"status": "ok"}
|
432
|
+
|
433
|
+
|
404
434
|
@api_content.get("/{content_source}", response_model=List[str])
|
405
435
|
@requires(["authenticated"])
|
406
436
|
async def get_content_source(
|
@@ -420,7 +450,7 @@ async def get_content_source(
|
|
420
450
|
return await sync_to_async(list)(EntryAdapters.get_all_filenames_by_source(user, content_source)) # type: ignore[call-arg]
|
421
451
|
|
422
452
|
|
423
|
-
@api_content.delete("/{content_source}", status_code=200)
|
453
|
+
@api_content.delete("/source/{content_source}", status_code=200)
|
424
454
|
@requires(["authenticated"])
|
425
455
|
async def delete_content_source(
|
426
456
|
request: Request,
|
@@ -434,7 +464,12 @@ async def delete_content_source(
|
|
434
464
|
raise ValueError(f"Invalid content source: {content_source}")
|
435
465
|
elif content_object != "Computer":
|
436
466
|
await content_object.objects.filter(user=user).adelete()
|
437
|
-
|
467
|
+
else:
|
468
|
+
# Delete file objects from the given source
|
469
|
+
file_list = await sync_to_async(list)(EntryAdapters.get_all_filenames_by_source(user, content_source)) # type: ignore[call-arg]
|
470
|
+
await FileObjectAdapters.adelete_file_objects_by_names(user, file_list)
|
471
|
+
# Delete entries from the given source
|
472
|
+
await EntryAdapters.adelete_all_entries(user, file_source=content_source)
|
438
473
|
|
439
474
|
if content_source == DbEntry.EntrySource.NOTION:
|
440
475
|
await NotionConfig.objects.filter(user=user).adelete()
|
@@ -449,7 +484,6 @@ async def delete_content_source(
|
|
449
484
|
metadata={"content_source": content_source},
|
450
485
|
)
|
451
486
|
|
452
|
-
enabled_content = await sync_to_async(EntryAdapters.get_unique_file_types)(user)
|
453
487
|
return {"status": "ok"}
|
454
488
|
|
455
489
|
|
khoj/utils/constants.py
CHANGED
@@ -18,7 +18,7 @@ default_offline_chat_models = [
|
|
18
18
|
"bartowski/Qwen2.5-14B-Instruct-GGUF",
|
19
19
|
]
|
20
20
|
default_openai_chat_models = ["gpt-4o-mini", "gpt-4o"]
|
21
|
-
default_gemini_chat_models = ["gemini-
|
21
|
+
default_gemini_chat_models = ["gemini-2.0-flash", "gemini-1.5-pro"]
|
22
22
|
default_anthropic_chat_models = ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022"]
|
23
23
|
|
24
24
|
empty_config = {
|
@@ -46,6 +46,7 @@ model_to_cost: Dict[str, Dict[str, float]] = {
|
|
46
46
|
"gemini-1.5-flash-002": {"input": 0.075, "output": 0.30},
|
47
47
|
"gemini-1.5-pro": {"input": 1.25, "output": 5.00},
|
48
48
|
"gemini-1.5-pro-002": {"input": 1.25, "output": 5.00},
|
49
|
+
"gemini-2.0-flash": {"input": 0.10, "output": 0.40},
|
49
50
|
# Anthropic Pricing: https://www.anthropic.com/pricing#anthropic-api_
|
50
51
|
"claude-3-5-sonnet-20241022": {"input": 3.0, "output": 15.0},
|
51
52
|
"claude-3-5-haiku-20241022": {"input": 1.0, "output": 5.0},
|
khoj/utils/helpers.py
CHANGED
@@ -321,6 +321,12 @@ def get_device() -> torch.device:
|
|
321
321
|
return torch.device("cpu")
|
322
322
|
|
323
323
|
|
324
|
+
def is_e2b_code_sandbox_enabled():
|
325
|
+
"""Check if E2B code sandbox is enabled.
|
326
|
+
Set E2B_API_KEY environment variable to use it."""
|
327
|
+
return not is_none_or_empty(os.getenv("E2B_API_KEY"))
|
328
|
+
|
329
|
+
|
324
330
|
class ConversationCommand(str, Enum):
|
325
331
|
Default = "default"
|
326
332
|
General = "general"
|
@@ -362,20 +368,23 @@ command_descriptions_for_agent = {
|
|
362
368
|
ConversationCommand.Code: "Agent can run Python code to parse information, run complex calculations, create documents and charts.",
|
363
369
|
}
|
364
370
|
|
371
|
+
e2b_tool_description = "To run Python code in a E2B sandbox with no network access. Helpful to parse complex information, run calculations, create text documents and create charts with quantitative data. Only matplotlib, pandas, numpy, scipy, bs4, sympy, einops, biopython, shapely and rdkit external packages are available."
|
372
|
+
terrarium_tool_description = "To run Python code in a Terrarium, Pyodide sandbox with no network access. Helpful to parse complex information, run complex calculations, create plaintext documents and create charts with quantitative data. Only matplotlib, panda, numpy, scipy, bs4 and sympy external packages are available."
|
373
|
+
|
365
374
|
tool_descriptions_for_llm = {
|
366
375
|
ConversationCommand.Default: "To use a mix of your internal knowledge and the user's personal knowledge, or if you don't entirely understand the query.",
|
367
376
|
ConversationCommand.General: "To use when you can answer the question without any outside information or personal knowledge",
|
368
377
|
ConversationCommand.Notes: "To search the user's personal knowledge base. Especially helpful if the question expects context from the user's notes or documents.",
|
369
378
|
ConversationCommand.Online: "To search for the latest, up-to-date information from the internet. Note: **Questions about Khoj should always use this data source**",
|
370
379
|
ConversationCommand.Webpage: "To use if the user has directly provided the webpage urls or you are certain of the webpage urls to read.",
|
371
|
-
ConversationCommand.Code:
|
380
|
+
ConversationCommand.Code: e2b_tool_description if is_e2b_code_sandbox_enabled() else terrarium_tool_description,
|
372
381
|
}
|
373
382
|
|
374
383
|
function_calling_description_for_llm = {
|
375
384
|
ConversationCommand.Notes: "To search the user's personal knowledge base. Especially helpful if the question expects context from the user's notes or documents.",
|
376
385
|
ConversationCommand.Online: "To search the internet for information. Useful to get a quick, broad overview from the internet. Provide all relevant context to ensure new searches, not in previous iterations, are performed.",
|
377
386
|
ConversationCommand.Webpage: "To extract information from webpages. Useful for more detailed research from the internet. Usually used when you know the webpage links to refer to. Share the webpage links and information to extract in your query.",
|
378
|
-
ConversationCommand.Code:
|
387
|
+
ConversationCommand.Code: e2b_tool_description if is_e2b_code_sandbox_enabled() else terrarium_tool_description,
|
379
388
|
}
|
380
389
|
|
381
390
|
mode_descriptions_for_llm = {
|
khoj/utils/initialization.py
CHANGED
@@ -185,16 +185,18 @@ def initialization(interactive: bool = True):
|
|
185
185
|
)
|
186
186
|
provider_name = provider_name or model_type.name.capitalize()
|
187
187
|
|
188
|
-
default_use_model =
|
189
|
-
|
190
|
-
#
|
191
|
-
|
188
|
+
default_use_model = default_api_key is not None
|
189
|
+
# If not in interactive mode & in the offline setting, it's most likely that we're running in a containerized environment.
|
190
|
+
# This usually means there's not enough RAM to load offline models directly within the application.
|
191
|
+
# In such cases, we default to not using the model -- it's recommended to use another service like Ollama to host the model locally in that case.
|
192
|
+
if is_offline:
|
193
|
+
default_use_model = False
|
192
194
|
|
193
195
|
use_model_provider = (
|
194
|
-
default_use_model if not interactive else input(f"Add {provider_name} chat models? (y/n): ")
|
196
|
+
default_use_model if not interactive else input(f"Add {provider_name} chat models? (y/n): ") == "y"
|
195
197
|
)
|
196
198
|
|
197
|
-
if use_model_provider
|
199
|
+
if not use_model_provider:
|
198
200
|
return False, None
|
199
201
|
|
200
202
|
logger.info(f"️💬 Setting up your {provider_name} chat configuration")
|
@@ -303,4 +305,19 @@ def initialization(interactive: bool = True):
|
|
303
305
|
logger.error(f"🚨 Failed to create chat configuration: {e}", exc_info=True)
|
304
306
|
else:
|
305
307
|
_update_chat_model_options()
|
306
|
-
logger.info("🗣️ Chat model
|
308
|
+
logger.info("🗣️ Chat model options updated")
|
309
|
+
|
310
|
+
# Update the default chat model if it doesn't match
|
311
|
+
chat_config = ConversationAdapters.get_default_chat_model()
|
312
|
+
env_default_chat_model = os.getenv("KHOJ_DEFAULT_CHAT_MODEL")
|
313
|
+
if not chat_config or not env_default_chat_model:
|
314
|
+
return
|
315
|
+
if chat_config.name != env_default_chat_model:
|
316
|
+
chat_model = ConversationAdapters.get_chat_model_by_name(env_default_chat_model)
|
317
|
+
if not chat_model:
|
318
|
+
logger.error(
|
319
|
+
f"🚨 Not setting default chat model. Chat model {env_default_chat_model} not found in existing chat model options."
|
320
|
+
)
|
321
|
+
return
|
322
|
+
ConversationAdapters.set_default_chat_model(chat_model)
|
323
|
+
logger.info(f"🗣️ Default chat model set to {chat_model.name}")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: khoj
|
3
|
-
Version: 1.36.7.
|
3
|
+
Version: 1.36.7.dev18
|
4
4
|
Summary: Your Second Brain
|
5
5
|
Project-URL: Homepage, https://khoj.dev
|
6
6
|
Project-URL: Documentation, https://docs.khoj.dev
|
@@ -35,11 +35,12 @@ Requires-Dist: django-phonenumber-field==7.3.0
|
|
35
35
|
Requires-Dist: django-unfold==0.42.0
|
36
36
|
Requires-Dist: django==5.0.10
|
37
37
|
Requires-Dist: docx2txt==0.8
|
38
|
+
Requires-Dist: e2b-code-interpreter~=1.0.0
|
38
39
|
Requires-Dist: einops==0.8.0
|
39
40
|
Requires-Dist: email-validator==2.2.0
|
40
41
|
Requires-Dist: fastapi>=0.110.0
|
41
42
|
Requires-Dist: google-generativeai==0.8.3
|
42
|
-
Requires-Dist: httpx==0.
|
43
|
+
Requires-Dist: httpx==0.27.2
|
43
44
|
Requires-Dist: huggingface-hub>=0.22.2
|
44
45
|
Requires-Dist: itsdangerous==2.1.2
|
45
46
|
Requires-Dist: jinja2==3.1.5
|