khoj 1.36.7.dev66__py3-none-any.whl → 1.37.1__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/migrations/0087_alter_aimodelapi_api_key.py +17 -0
- khoj/database/models/__init__.py +1 -1
- khoj/interface/compiled/404/index.html +2 -2
- khoj/interface/compiled/_next/static/chunks/{2117-f99825f0a867a42d.js → 2117-1c18aa2098982bf9.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/{2327-b21ecded25471e6c.js → 2327-0bbe3ee35f80659f.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/{5477-9ff77f49e6cf375c.js → 5477-a5b2688736f51b8c.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/{8515-010dd769c584b672.js → 8515-f305779d95dd5780.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/agents/{layout-948ca256650845ce.js → layout-dd7f2b45a9c30bd7.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/{layout-603285e3b1400e74.js → layout-904fbbb3974588da.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/{page-50cb9b62b10b5f3d.js → page-5175e747d3cb4a33.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/{page-29e3b092fe46f190.js → page-44ac22beb2619af0.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/search/{layout-d7f7528ff387fba5.js → layout-51d73830842461d5.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/share/chat/{layout-246d0e8125219fff.js → layout-d090bd23befd0207.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/share/chat/{page-6f26fe7f2f7edc56.js → page-e8f0cc65930b214e.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/main-876327ac335776ab.js +1 -0
- khoj/interface/compiled/_next/static/chunks/{webpack-1169ca6e9e7e6247.js → webpack-d1d79c1576702da7.js} +1 -1
- khoj/interface/compiled/_next/static/css/440ae0f0f650dc35.css +1 -0
- khoj/interface/compiled/_next/static/css/b061a6aedf367349.css +25 -0
- khoj/interface/compiled/_next/static/css/b62829e3bf683b86.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 +2 -2
- 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 +2 -2
- khoj/interface/compiled/share/chat/index.html +2 -2
- khoj/interface/compiled/share/chat/index.txt +3 -3
- khoj/processor/conversation/anthropic/anthropic_chat.py +9 -4
- khoj/processor/conversation/anthropic/utils.py +32 -12
- khoj/processor/conversation/google/gemini_chat.py +14 -5
- khoj/processor/conversation/google/utils.py +49 -6
- khoj/processor/conversation/openai/gpt.py +18 -6
- khoj/processor/conversation/openai/utils.py +37 -46
- khoj/processor/conversation/utils.py +24 -2
- khoj/processor/image/generate.py +2 -2
- khoj/processor/tools/run_code.py +1 -1
- khoj/routers/api.py +4 -0
- khoj/routers/api_chat.py +6 -4
- khoj/routers/auth.py +2 -5
- khoj/routers/helpers.py +23 -3
- khoj/routers/research.py +44 -3
- khoj/routers/storage.py +28 -29
- khoj/utils/constants.py +2 -0
- khoj/utils/helpers.py +58 -2
- {khoj-1.36.7.dev66.dist-info → khoj-1.37.1.dist-info}/METADATA +5 -6
- {khoj-1.36.7.dev66.dist-info → khoj-1.37.1.dist-info}/RECORD +57 -56
- khoj/interface/compiled/_next/static/chunks/main-98eb5932d6b2e3fa.js +0 -1
- khoj/interface/compiled/_next/static/css/5384e98d63fe6f0e.css +0 -25
- khoj/interface/compiled/_next/static/css/8051073dc55b92b3.css +0 -1
- khoj/interface/compiled/_next/static/css/b15666ef52060cd0.css +0 -1
- /khoj/interface/compiled/_next/static/{iZ9Zhm-BkOf7hfAqqzokr → PzXuumAYUnzr_Egd_JDmj}/_buildManifest.js +0 -0
- /khoj/interface/compiled/_next/static/{iZ9Zhm-BkOf7hfAqqzokr → PzXuumAYUnzr_Egd_JDmj}/_ssgManifest.js +0 -0
- {khoj-1.36.7.dev66.dist-info → khoj-1.37.1.dist-info}/WHEEL +0 -0
- {khoj-1.36.7.dev66.dist-info → khoj-1.37.1.dist-info}/entry_points.txt +0 -0
- {khoj-1.36.7.dev66.dist-info → khoj-1.37.1.dist-info}/licenses/LICENSE +0 -0
khoj/routers/helpers.py
CHANGED
@@ -540,11 +540,15 @@ async def generate_online_subqueries(
|
|
540
540
|
|
541
541
|
agent_chat_model = agent.chat_model if agent else None
|
542
542
|
|
543
|
+
class OnlineQueries(BaseModel):
|
544
|
+
queries: List[str]
|
545
|
+
|
543
546
|
with timer("Chat actor: Generate online search subqueries", logger):
|
544
547
|
response = await send_message_to_model_wrapper(
|
545
548
|
online_queries_prompt,
|
546
549
|
query_images=query_images,
|
547
550
|
response_type="json_object",
|
551
|
+
response_schema=OnlineQueries,
|
548
552
|
user=user,
|
549
553
|
query_files=query_files,
|
550
554
|
agent_chat_model=agent_chat_model,
|
@@ -1129,6 +1133,7 @@ async def send_message_to_model_wrapper(
|
|
1129
1133
|
query: str,
|
1130
1134
|
system_message: str = "",
|
1131
1135
|
response_type: str = "text",
|
1136
|
+
response_schema: BaseModel = None,
|
1132
1137
|
deepthought: bool = False,
|
1133
1138
|
user: KhojUser = None,
|
1134
1139
|
query_images: List[str] = None,
|
@@ -1209,11 +1214,13 @@ async def send_message_to_model_wrapper(
|
|
1209
1214
|
api_key=api_key,
|
1210
1215
|
model=chat_model_name,
|
1211
1216
|
response_type=response_type,
|
1217
|
+
response_schema=response_schema,
|
1212
1218
|
api_base_url=api_base_url,
|
1213
1219
|
tracer=tracer,
|
1214
1220
|
)
|
1215
1221
|
elif model_type == ChatModel.ModelType.ANTHROPIC:
|
1216
1222
|
api_key = chat_model.ai_model_api.api_key
|
1223
|
+
api_base_url = chat_model.ai_model_api.api_base_url
|
1217
1224
|
truncated_messages = generate_chatml_messages_with_context(
|
1218
1225
|
user_message=query,
|
1219
1226
|
context_message=context,
|
@@ -1233,10 +1240,12 @@ async def send_message_to_model_wrapper(
|
|
1233
1240
|
model=chat_model_name,
|
1234
1241
|
response_type=response_type,
|
1235
1242
|
deepthought=deepthought,
|
1243
|
+
api_base_url=api_base_url,
|
1236
1244
|
tracer=tracer,
|
1237
1245
|
)
|
1238
1246
|
elif model_type == ChatModel.ModelType.GOOGLE:
|
1239
1247
|
api_key = chat_model.ai_model_api.api_key
|
1248
|
+
api_base_url = chat_model.ai_model_api.api_base_url
|
1240
1249
|
truncated_messages = generate_chatml_messages_with_context(
|
1241
1250
|
user_message=query,
|
1242
1251
|
context_message=context,
|
@@ -1255,6 +1264,8 @@ async def send_message_to_model_wrapper(
|
|
1255
1264
|
api_key=api_key,
|
1256
1265
|
model=chat_model_name,
|
1257
1266
|
response_type=response_type,
|
1267
|
+
response_schema=response_schema,
|
1268
|
+
api_base_url=api_base_url,
|
1258
1269
|
tracer=tracer,
|
1259
1270
|
)
|
1260
1271
|
else:
|
@@ -1265,6 +1276,7 @@ def send_message_to_model_wrapper_sync(
|
|
1265
1276
|
message: str,
|
1266
1277
|
system_message: str = "",
|
1267
1278
|
response_type: str = "text",
|
1279
|
+
response_schema: BaseModel = None,
|
1268
1280
|
user: KhojUser = None,
|
1269
1281
|
query_images: List[str] = None,
|
1270
1282
|
query_files: str = "",
|
@@ -1320,19 +1332,19 @@ def send_message_to_model_wrapper_sync(
|
|
1320
1332
|
query_files=query_files,
|
1321
1333
|
)
|
1322
1334
|
|
1323
|
-
|
1335
|
+
return send_message_to_model(
|
1324
1336
|
messages=truncated_messages,
|
1325
1337
|
api_key=api_key,
|
1326
1338
|
api_base_url=api_base_url,
|
1327
1339
|
model=chat_model_name,
|
1328
1340
|
response_type=response_type,
|
1341
|
+
response_schema=response_schema,
|
1329
1342
|
tracer=tracer,
|
1330
1343
|
)
|
1331
1344
|
|
1332
|
-
return openai_response
|
1333
|
-
|
1334
1345
|
elif chat_model.model_type == ChatModel.ModelType.ANTHROPIC:
|
1335
1346
|
api_key = chat_model.ai_model_api.api_key
|
1347
|
+
api_base_url = chat_model.ai_model_api.api_base_url
|
1336
1348
|
truncated_messages = generate_chatml_messages_with_context(
|
1337
1349
|
user_message=message,
|
1338
1350
|
system_message=system_message,
|
@@ -1347,6 +1359,7 @@ def send_message_to_model_wrapper_sync(
|
|
1347
1359
|
return anthropic_send_message_to_model(
|
1348
1360
|
messages=truncated_messages,
|
1349
1361
|
api_key=api_key,
|
1362
|
+
api_base_url=api_base_url,
|
1350
1363
|
model=chat_model_name,
|
1351
1364
|
response_type=response_type,
|
1352
1365
|
tracer=tracer,
|
@@ -1354,6 +1367,7 @@ def send_message_to_model_wrapper_sync(
|
|
1354
1367
|
|
1355
1368
|
elif chat_model.model_type == ChatModel.ModelType.GOOGLE:
|
1356
1369
|
api_key = chat_model.ai_model_api.api_key
|
1370
|
+
api_base_url = chat_model.ai_model_api.api_base_url
|
1357
1371
|
truncated_messages = generate_chatml_messages_with_context(
|
1358
1372
|
user_message=message,
|
1359
1373
|
system_message=system_message,
|
@@ -1368,8 +1382,10 @@ def send_message_to_model_wrapper_sync(
|
|
1368
1382
|
return gemini_send_message_to_model(
|
1369
1383
|
messages=truncated_messages,
|
1370
1384
|
api_key=api_key,
|
1385
|
+
api_base_url=api_base_url,
|
1371
1386
|
model=chat_model_name,
|
1372
1387
|
response_type=response_type,
|
1388
|
+
response_schema=response_schema,
|
1373
1389
|
tracer=tracer,
|
1374
1390
|
)
|
1375
1391
|
else:
|
@@ -1500,6 +1516,7 @@ def generate_chat_response(
|
|
1500
1516
|
|
1501
1517
|
elif chat_model.model_type == ChatModel.ModelType.ANTHROPIC:
|
1502
1518
|
api_key = chat_model.ai_model_api.api_key
|
1519
|
+
api_base_url = chat_model.ai_model_api.api_base_url
|
1503
1520
|
chat_response = converse_anthropic(
|
1504
1521
|
compiled_references,
|
1505
1522
|
query_to_run,
|
@@ -1509,6 +1526,7 @@ def generate_chat_response(
|
|
1509
1526
|
conversation_log=meta_log,
|
1510
1527
|
model=chat_model.name,
|
1511
1528
|
api_key=api_key,
|
1529
|
+
api_base_url=api_base_url,
|
1512
1530
|
completion_func=partial_completion,
|
1513
1531
|
conversation_commands=conversation_commands,
|
1514
1532
|
max_prompt_size=chat_model.max_prompt_size,
|
@@ -1526,6 +1544,7 @@ def generate_chat_response(
|
|
1526
1544
|
)
|
1527
1545
|
elif chat_model.model_type == ChatModel.ModelType.GOOGLE:
|
1528
1546
|
api_key = chat_model.ai_model_api.api_key
|
1547
|
+
api_base_url = chat_model.ai_model_api.api_base_url
|
1529
1548
|
chat_response = converse_gemini(
|
1530
1549
|
compiled_references,
|
1531
1550
|
query_to_run,
|
@@ -1534,6 +1553,7 @@ def generate_chat_response(
|
|
1534
1553
|
meta_log,
|
1535
1554
|
model=chat_model.name,
|
1536
1555
|
api_key=api_key,
|
1556
|
+
api_base_url=api_base_url,
|
1537
1557
|
completion_func=partial_completion,
|
1538
1558
|
conversation_commands=conversation_commands,
|
1539
1559
|
max_prompt_size=chat_model.max_prompt_size,
|
khoj/routers/research.py
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
import logging
|
2
|
+
import os
|
2
3
|
from datetime import datetime
|
3
|
-
from
|
4
|
+
from enum import Enum
|
5
|
+
from typing import Callable, Dict, List, Optional, Type
|
4
6
|
|
5
7
|
import yaml
|
6
8
|
from fastapi import Request
|
9
|
+
from pydantic import BaseModel, Field
|
7
10
|
|
8
11
|
from khoj.database.adapters import EntryAdapters
|
9
12
|
from khoj.database.models import Agent, KhojUser
|
@@ -35,6 +38,40 @@ from khoj.utils.rawconfig import LocationData
|
|
35
38
|
logger = logging.getLogger(__name__)
|
36
39
|
|
37
40
|
|
41
|
+
class PlanningResponse(BaseModel):
|
42
|
+
"""
|
43
|
+
Schema for the response from planning agent when deciding the next tool to pick.
|
44
|
+
The tool field is dynamically validated based on available tools.
|
45
|
+
"""
|
46
|
+
|
47
|
+
scratchpad: str = Field(..., description="Reasoning about which tool to use next")
|
48
|
+
query: str = Field(..., description="Detailed query for the selected tool")
|
49
|
+
|
50
|
+
class Config:
|
51
|
+
arbitrary_types_allowed = True
|
52
|
+
|
53
|
+
@classmethod
|
54
|
+
def create_model_with_enum(cls: Type["PlanningResponse"], tool_options: dict) -> Type["PlanningResponse"]:
|
55
|
+
"""
|
56
|
+
Factory method that creates a customized PlanningResponse model
|
57
|
+
with a properly typed tool field based on available tools.
|
58
|
+
|
59
|
+
Args:
|
60
|
+
tool_options: Dictionary mapping tool names to values
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
A customized PlanningResponse class
|
64
|
+
"""
|
65
|
+
# Create dynamic enum from tool options
|
66
|
+
tool_enum = Enum("ToolEnum", tool_options) # type: ignore
|
67
|
+
|
68
|
+
# Create and return a customized response model with the enum
|
69
|
+
class PlanningResponseWithTool(PlanningResponse):
|
70
|
+
tool: tool_enum = Field(..., description="Name of the tool to use")
|
71
|
+
|
72
|
+
return PlanningResponseWithTool
|
73
|
+
|
74
|
+
|
38
75
|
async def apick_next_tool(
|
39
76
|
query: str,
|
40
77
|
conversation_history: dict,
|
@@ -60,10 +97,13 @@ async def apick_next_tool(
|
|
60
97
|
# Skip showing Notes tool as an option if user has no entries
|
61
98
|
if tool == ConversationCommand.Notes and not user_has_entries:
|
62
99
|
continue
|
63
|
-
tool_options[tool.value] = description
|
64
100
|
if len(agent_tools) == 0 or tool.value in agent_tools:
|
101
|
+
tool_options[tool.name] = tool.value
|
65
102
|
tool_options_str += f'- "{tool.value}": "{description}"\n'
|
66
103
|
|
104
|
+
# Create planning reponse model with dynamically populated tool enum class
|
105
|
+
planning_response_model = PlanningResponse.create_model_with_enum(tool_options)
|
106
|
+
|
67
107
|
# Construct chat history with user and iteration history with researcher agent for context
|
68
108
|
chat_history = construct_chat_history(conversation_history, agent_name=agent.name if agent else "Khoj")
|
69
109
|
previous_iterations_history = construct_iteration_history(previous_iterations, prompts.previous_iteration)
|
@@ -95,6 +135,7 @@ async def apick_next_tool(
|
|
95
135
|
query=query,
|
96
136
|
context=function_planning_prompt,
|
97
137
|
response_type="json_object",
|
138
|
+
response_schema=planning_response_model,
|
98
139
|
deepthought=True,
|
99
140
|
user=user,
|
100
141
|
query_images=query_images,
|
@@ -160,7 +201,7 @@ async def execute_information_collection(
|
|
160
201
|
query_files: str = None,
|
161
202
|
):
|
162
203
|
current_iteration = 0
|
163
|
-
MAX_ITERATIONS = 5
|
204
|
+
MAX_ITERATIONS = int(os.getenv("KHOJ_RESEARCH_ITERATIONS", 5))
|
164
205
|
previous_iterations: List[InformationCollectionIteration] = []
|
165
206
|
while current_iteration < MAX_ITERATIONS:
|
166
207
|
online_results: Dict = dict()
|
khoj/routers/storage.py
CHANGED
@@ -9,9 +9,10 @@ AWS_SECRET_KEY = os.getenv("AWS_SECRET_KEY")
|
|
9
9
|
# S3 supports serving assets via your domain. Khoj expects this to be used in production. To enable it:
|
10
10
|
# 1. Your bucket name for images should be of the form sub.domain.tld. For example, generated.khoj.dev
|
11
11
|
# 2. Add CNAME entry to your domain's DNS records pointing to the S3 bucket. For example, CNAME generated.khoj.dev generated-khoj-dev.s3.amazonaws.com
|
12
|
-
|
12
|
+
AWS_KHOJ_IMAGES_BUCKET_NAME = os.getenv("AWS_IMAGE_UPLOAD_BUCKET")
|
13
|
+
AWS_USER_IMAGES_BUCKET_NAME = os.getenv("AWS_USER_UPLOADED_IMAGES_BUCKET_NAME")
|
13
14
|
|
14
|
-
aws_enabled = AWS_ACCESS_KEY is not None and AWS_SECRET_KEY is not None
|
15
|
+
aws_enabled = AWS_ACCESS_KEY is not None and AWS_SECRET_KEY is not None
|
15
16
|
|
16
17
|
if aws_enabled:
|
17
18
|
from boto3 import client
|
@@ -19,45 +20,43 @@ if aws_enabled:
|
|
19
20
|
s3_client = client("s3", aws_access_key_id=AWS_ACCESS_KEY, aws_secret_access_key=AWS_SECRET_KEY)
|
20
21
|
|
21
22
|
|
22
|
-
def
|
23
|
-
"""Upload
|
23
|
+
def upload_image_to_bucket(webp_image: bytes, user_id: uuid.UUID, bucket_name: str):
|
24
|
+
"""Upload webp image to an S3 bucket"""
|
24
25
|
if not aws_enabled:
|
25
26
|
logger.info("AWS is not enabled. Skipping image upload")
|
26
27
|
return None
|
27
|
-
|
28
|
-
|
29
|
-
try:
|
30
|
-
s3_client.put_object(Bucket=AWS_UPLOAD_IMAGE_BUCKET_NAME, Key=image_key, Body=image, ACL="public-read")
|
31
|
-
url = f"https://{AWS_UPLOAD_IMAGE_BUCKET_NAME}/{image_key}"
|
32
|
-
return url
|
33
|
-
except Exception as e:
|
34
|
-
logger.error(f"Failed to upload image to S3: {e}")
|
35
|
-
return None
|
36
|
-
|
37
|
-
|
38
|
-
AWS_USER_UPLOADED_IMAGES_BUCKET_NAME = os.getenv("AWS_USER_UPLOADED_IMAGES_BUCKET_NAME")
|
39
|
-
|
40
|
-
|
41
|
-
def upload_image_to_bucket(image: bytes, user_id: uuid.UUID):
|
42
|
-
"""Upload the image to the S3 bucket"""
|
43
|
-
if not aws_enabled:
|
44
|
-
logger.info("AWS is not enabled. Skipping image upload")
|
28
|
+
if not bucket_name:
|
29
|
+
logger.error(f"{bucket_name} is not set")
|
45
30
|
return None
|
46
31
|
|
47
32
|
image_key = f"{user_id}/{uuid.uuid4()}.webp"
|
48
|
-
if not AWS_USER_UPLOADED_IMAGES_BUCKET_NAME:
|
49
|
-
logger.error("AWS_USER_UPLOADED_IMAGES_BUCKET_NAME is not set")
|
50
|
-
return None
|
51
|
-
|
52
33
|
try:
|
53
34
|
s3_client.put_object(
|
54
|
-
Bucket=
|
35
|
+
Bucket=bucket_name,
|
55
36
|
Key=image_key,
|
56
|
-
Body=
|
37
|
+
Body=webp_image,
|
57
38
|
ACL="public-read",
|
58
39
|
ContentType="image/webp",
|
59
40
|
)
|
60
|
-
return f"https://{
|
41
|
+
return f"https://{bucket_name}/{image_key}"
|
61
42
|
except Exception as e:
|
62
43
|
logger.error(f"Failed to upload image to S3: {e}")
|
63
44
|
return None
|
45
|
+
|
46
|
+
|
47
|
+
def upload_generated_image_to_bucket(image: bytes, user_id: uuid.UUID):
|
48
|
+
"""Upload khoj generated image to an S3 bucket"""
|
49
|
+
return upload_image_to_bucket(
|
50
|
+
webp_image=image,
|
51
|
+
user_id=user_id,
|
52
|
+
bucket_name=AWS_KHOJ_IMAGES_BUCKET_NAME,
|
53
|
+
)
|
54
|
+
|
55
|
+
|
56
|
+
def upload_user_image_to_bucket(image: bytes, user_id: uuid.UUID):
|
57
|
+
"""Upload user attached image to an S3 bucket"""
|
58
|
+
return upload_image_to_bucket(
|
59
|
+
webp_image=image,
|
60
|
+
user_id=user_id,
|
61
|
+
bucket_name=AWS_USER_IMAGES_BUCKET_NAME,
|
62
|
+
)
|
khoj/utils/constants.py
CHANGED
@@ -49,8 +49,10 @@ model_to_cost: Dict[str, Dict[str, float]] = {
|
|
49
49
|
"gemini-2.0-flash": {"input": 0.10, "output": 0.40},
|
50
50
|
# Anthropic Pricing: https://www.anthropic.com/pricing#anthropic-api_
|
51
51
|
"claude-3-5-haiku-20241022": {"input": 1.0, "output": 5.0},
|
52
|
+
"claude-3-5-haiku@20241022": {"input": 1.0, "output": 5.0},
|
52
53
|
"claude-3-5-sonnet-20241022": {"input": 3.0, "output": 15.0},
|
53
54
|
"claude-3-5-sonnet-latest": {"input": 3.0, "output": 15.0},
|
54
55
|
"claude-3-7-sonnet-20250219": {"input": 3.0, "output": 15.0},
|
56
|
+
"claude-3-7-sonnet@20250219": {"input": 3.0, "output": 15.0},
|
55
57
|
"claude-3-7-sonnet-latest": {"input": 3.0, "output": 15.0},
|
56
58
|
}
|
khoj/utils/helpers.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from __future__ import annotations # to avoid quoting type hints
|
2
2
|
|
3
|
+
import base64
|
3
4
|
import copy
|
4
5
|
import datetime
|
5
6
|
import io
|
@@ -19,15 +20,18 @@ from itertools import islice
|
|
19
20
|
from os import path
|
20
21
|
from pathlib import Path
|
21
22
|
from time import perf_counter
|
22
|
-
from typing import TYPE_CHECKING, Any, Optional, Union
|
23
|
-
from urllib.parse import urlparse
|
23
|
+
from typing import TYPE_CHECKING, Any, NamedTuple, Optional, Tuple, Union
|
24
|
+
from urllib.parse import ParseResult, urlparse
|
24
25
|
|
25
26
|
import openai
|
26
27
|
import psutil
|
28
|
+
import pyjson5
|
27
29
|
import requests
|
28
30
|
import torch
|
29
31
|
from asgiref.sync import sync_to_async
|
30
32
|
from email_validator import EmailNotValidError, EmailUndeliverableError, validate_email
|
33
|
+
from google.auth.credentials import Credentials
|
34
|
+
from google.oauth2 import service_account
|
31
35
|
from magika import Magika
|
32
36
|
from PIL import Image
|
33
37
|
from pytz import country_names, country_timezones
|
@@ -618,6 +622,58 @@ def get_chat_usage_metrics(
|
|
618
622
|
}
|
619
623
|
|
620
624
|
|
625
|
+
class AiApiInfo(NamedTuple):
|
626
|
+
region: str
|
627
|
+
project: str
|
628
|
+
credentials: Credentials
|
629
|
+
api_key: str
|
630
|
+
|
631
|
+
|
632
|
+
def get_gcp_credentials(credentials_b64: str) -> Optional[Credentials]:
|
633
|
+
"""
|
634
|
+
Get GCP credentials from base64 encoded service account credentials json keyfile
|
635
|
+
"""
|
636
|
+
credentials_json = base64.b64decode(credentials_b64).decode("utf-8")
|
637
|
+
credentials_dict = pyjson5.loads(credentials_json)
|
638
|
+
credentials = service_account.Credentials.from_service_account_info(credentials_dict)
|
639
|
+
return credentials.with_scopes(scopes=["https://www.googleapis.com/auth/cloud-platform"])
|
640
|
+
|
641
|
+
|
642
|
+
def get_gcp_project_info(parsed_api_url: ParseResult) -> Tuple[str, str]:
|
643
|
+
"""
|
644
|
+
Extract region, project id from GCP API url
|
645
|
+
API url is of form https://{REGION}-aiplatform.googleapis.com/v1/projects/{PROJECT_ID}...
|
646
|
+
"""
|
647
|
+
# Extract region from hostname
|
648
|
+
hostname = parsed_api_url.netloc
|
649
|
+
region = hostname.split("-aiplatform")[0] if "-aiplatform" in hostname else ""
|
650
|
+
|
651
|
+
# Extract project ID from path (e.g., "/v1/projects/my-project/...")
|
652
|
+
path_parts = parsed_api_url.path.split("/")
|
653
|
+
project_id = ""
|
654
|
+
for i, part in enumerate(path_parts):
|
655
|
+
if part == "projects" and i + 1 < len(path_parts):
|
656
|
+
project_id = path_parts[i + 1]
|
657
|
+
break
|
658
|
+
|
659
|
+
return region, project_id
|
660
|
+
|
661
|
+
|
662
|
+
def get_ai_api_info(api_key, api_base_url: str = None) -> AiApiInfo:
|
663
|
+
"""
|
664
|
+
Get the GCP Vertex or default AI API client info based on the API key and URL.
|
665
|
+
"""
|
666
|
+
region, project_id, credentials = None, None, None
|
667
|
+
# Check if AI model to be used via GCP Vertex API
|
668
|
+
parsed_api_url = urlparse(api_base_url)
|
669
|
+
if parsed_api_url.hostname and parsed_api_url.hostname.endswith(".googleapis.com"):
|
670
|
+
region, project_id = get_gcp_project_info(parsed_api_url)
|
671
|
+
credentials = get_gcp_credentials(api_key)
|
672
|
+
if credentials:
|
673
|
+
api_key = None
|
674
|
+
return AiApiInfo(region=region, project=project_id, credentials=credentials, api_key=api_key)
|
675
|
+
|
676
|
+
|
621
677
|
def get_openai_client(api_key: str, api_base_url: str) -> Union[openai.OpenAI, openai.AzureOpenAI]:
|
622
678
|
"""Get OpenAI or AzureOpenAI client based on the API Base URL"""
|
623
679
|
parsed_url = urlparse(api_base_url)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: khoj
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.37.1
|
4
4
|
Summary: Your Second Brain
|
5
5
|
Project-URL: Homepage, https://khoj.dev
|
6
6
|
Project-URL: Documentation, https://docs.khoj.dev
|
@@ -39,6 +39,7 @@ Requires-Dist: e2b-code-interpreter~=1.0.0
|
|
39
39
|
Requires-Dist: einops==0.8.0
|
40
40
|
Requires-Dist: email-validator==2.2.0
|
41
41
|
Requires-Dist: fastapi>=0.110.0
|
42
|
+
Requires-Dist: google-auth~=2.23.3
|
42
43
|
Requires-Dist: google-genai==1.5.0
|
43
44
|
Requires-Dist: httpx==0.28.1
|
44
45
|
Requires-Dist: huggingface-hub>=0.22.2
|
@@ -69,12 +70,12 @@ Requires-Dist: requests>=2.26.0
|
|
69
70
|
Requires-Dist: resend==1.0.1
|
70
71
|
Requires-Dist: rich>=13.3.1
|
71
72
|
Requires-Dist: schedule==1.1.0
|
72
|
-
Requires-Dist: sentence-transformers==3.
|
73
|
+
Requires-Dist: sentence-transformers==3.4.1
|
73
74
|
Requires-Dist: tenacity==8.3.0
|
74
75
|
Requires-Dist: tenacity>=8.2.2
|
75
76
|
Requires-Dist: tiktoken>=0.3.2
|
76
|
-
Requires-Dist: torch==2.
|
77
|
-
Requires-Dist: transformers
|
77
|
+
Requires-Dist: torch==2.6.0
|
78
|
+
Requires-Dist: transformers<4.50.0,>=4.28.0
|
78
79
|
Requires-Dist: tzdata==2023.3
|
79
80
|
Requires-Dist: uvicorn==0.30.6
|
80
81
|
Requires-Dist: websockets==13.0
|
@@ -85,7 +86,6 @@ Requires-Dist: datasets; extra == 'dev'
|
|
85
86
|
Requires-Dist: factory-boy>=3.2.1; extra == 'dev'
|
86
87
|
Requires-Dist: freezegun>=1.2.0; extra == 'dev'
|
87
88
|
Requires-Dist: gitpython~=3.1.43; extra == 'dev'
|
88
|
-
Requires-Dist: google-auth==2.23.3; extra == 'dev'
|
89
89
|
Requires-Dist: gunicorn==22.0.0; extra == 'dev'
|
90
90
|
Requires-Dist: mypy>=1.0.1; extra == 'dev'
|
91
91
|
Requires-Dist: pandas; extra == 'dev'
|
@@ -98,7 +98,6 @@ Requires-Dist: stripe==7.3.0; extra == 'dev'
|
|
98
98
|
Requires-Dist: twilio==8.11; extra == 'dev'
|
99
99
|
Provides-Extra: prod
|
100
100
|
Requires-Dist: boto3>=1.34.57; extra == 'prod'
|
101
|
-
Requires-Dist: google-auth==2.23.3; extra == 'prod'
|
102
101
|
Requires-Dist: gunicorn==22.0.0; extra == 'prod'
|
103
102
|
Requires-Dist: stripe==7.3.0; extra == 'prod'
|
104
103
|
Requires-Dist: twilio==8.11; extra == 'prod'
|