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.
Files changed (61) hide show
  1. khoj/database/migrations/0087_alter_aimodelapi_api_key.py +17 -0
  2. khoj/database/models/__init__.py +1 -1
  3. khoj/interface/compiled/404/index.html +2 -2
  4. khoj/interface/compiled/_next/static/chunks/{2117-f99825f0a867a42d.js → 2117-1c18aa2098982bf9.js} +1 -1
  5. khoj/interface/compiled/_next/static/chunks/{2327-b21ecded25471e6c.js → 2327-0bbe3ee35f80659f.js} +1 -1
  6. khoj/interface/compiled/_next/static/chunks/{5477-9ff77f49e6cf375c.js → 5477-a5b2688736f51b8c.js} +1 -1
  7. khoj/interface/compiled/_next/static/chunks/{8515-010dd769c584b672.js → 8515-f305779d95dd5780.js} +1 -1
  8. khoj/interface/compiled/_next/static/chunks/app/agents/{layout-948ca256650845ce.js → layout-dd7f2b45a9c30bd7.js} +1 -1
  9. khoj/interface/compiled/_next/static/chunks/app/chat/{layout-603285e3b1400e74.js → layout-904fbbb3974588da.js} +1 -1
  10. khoj/interface/compiled/_next/static/chunks/app/chat/{page-50cb9b62b10b5f3d.js → page-5175e747d3cb4a33.js} +1 -1
  11. khoj/interface/compiled/_next/static/chunks/app/{page-29e3b092fe46f190.js → page-44ac22beb2619af0.js} +1 -1
  12. khoj/interface/compiled/_next/static/chunks/app/search/{layout-d7f7528ff387fba5.js → layout-51d73830842461d5.js} +1 -1
  13. khoj/interface/compiled/_next/static/chunks/app/share/chat/{layout-246d0e8125219fff.js → layout-d090bd23befd0207.js} +1 -1
  14. khoj/interface/compiled/_next/static/chunks/app/share/chat/{page-6f26fe7f2f7edc56.js → page-e8f0cc65930b214e.js} +1 -1
  15. khoj/interface/compiled/_next/static/chunks/main-876327ac335776ab.js +1 -0
  16. khoj/interface/compiled/_next/static/chunks/{webpack-1169ca6e9e7e6247.js → webpack-d1d79c1576702da7.js} +1 -1
  17. khoj/interface/compiled/_next/static/css/440ae0f0f650dc35.css +1 -0
  18. khoj/interface/compiled/_next/static/css/b061a6aedf367349.css +25 -0
  19. khoj/interface/compiled/_next/static/css/b62829e3bf683b86.css +1 -0
  20. khoj/interface/compiled/agents/index.html +2 -2
  21. khoj/interface/compiled/agents/index.txt +3 -3
  22. khoj/interface/compiled/automations/index.html +2 -2
  23. khoj/interface/compiled/automations/index.txt +2 -2
  24. khoj/interface/compiled/chat/index.html +2 -2
  25. khoj/interface/compiled/chat/index.txt +3 -3
  26. khoj/interface/compiled/index.html +2 -2
  27. khoj/interface/compiled/index.txt +2 -2
  28. khoj/interface/compiled/search/index.html +2 -2
  29. khoj/interface/compiled/search/index.txt +3 -3
  30. khoj/interface/compiled/settings/index.html +2 -2
  31. khoj/interface/compiled/settings/index.txt +2 -2
  32. khoj/interface/compiled/share/chat/index.html +2 -2
  33. khoj/interface/compiled/share/chat/index.txt +3 -3
  34. khoj/processor/conversation/anthropic/anthropic_chat.py +9 -4
  35. khoj/processor/conversation/anthropic/utils.py +32 -12
  36. khoj/processor/conversation/google/gemini_chat.py +14 -5
  37. khoj/processor/conversation/google/utils.py +49 -6
  38. khoj/processor/conversation/openai/gpt.py +18 -6
  39. khoj/processor/conversation/openai/utils.py +37 -46
  40. khoj/processor/conversation/utils.py +24 -2
  41. khoj/processor/image/generate.py +2 -2
  42. khoj/processor/tools/run_code.py +1 -1
  43. khoj/routers/api.py +4 -0
  44. khoj/routers/api_chat.py +6 -4
  45. khoj/routers/auth.py +2 -5
  46. khoj/routers/helpers.py +23 -3
  47. khoj/routers/research.py +44 -3
  48. khoj/routers/storage.py +28 -29
  49. khoj/utils/constants.py +2 -0
  50. khoj/utils/helpers.py +58 -2
  51. {khoj-1.36.7.dev66.dist-info → khoj-1.37.1.dist-info}/METADATA +5 -6
  52. {khoj-1.36.7.dev66.dist-info → khoj-1.37.1.dist-info}/RECORD +57 -56
  53. khoj/interface/compiled/_next/static/chunks/main-98eb5932d6b2e3fa.js +0 -1
  54. khoj/interface/compiled/_next/static/css/5384e98d63fe6f0e.css +0 -25
  55. khoj/interface/compiled/_next/static/css/8051073dc55b92b3.css +0 -1
  56. khoj/interface/compiled/_next/static/css/b15666ef52060cd0.css +0 -1
  57. /khoj/interface/compiled/_next/static/{iZ9Zhm-BkOf7hfAqqzokr → PzXuumAYUnzr_Egd_JDmj}/_buildManifest.js +0 -0
  58. /khoj/interface/compiled/_next/static/{iZ9Zhm-BkOf7hfAqqzokr → PzXuumAYUnzr_Egd_JDmj}/_ssgManifest.js +0 -0
  59. {khoj-1.36.7.dev66.dist-info → khoj-1.37.1.dist-info}/WHEEL +0 -0
  60. {khoj-1.36.7.dev66.dist-info → khoj-1.37.1.dist-info}/entry_points.txt +0 -0
  61. {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
- openai_response = send_message_to_model(
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 typing import Callable, Dict, List, Optional
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
- AWS_UPLOAD_IMAGE_BUCKET_NAME = os.getenv("AWS_IMAGE_UPLOAD_BUCKET")
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 and AWS_UPLOAD_IMAGE_BUCKET_NAME 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 upload_image(image: bytes, user_id: uuid.UUID):
23
- """Upload the image to the S3 bucket"""
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
- image_key = f"{user_id}/{uuid.uuid4()}.webp"
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=AWS_USER_UPLOADED_IMAGES_BUCKET_NAME,
35
+ Bucket=bucket_name,
55
36
  Key=image_key,
56
- Body=image,
37
+ Body=webp_image,
57
38
  ACL="public-read",
58
39
  ContentType="image/webp",
59
40
  )
60
- return f"https://{AWS_USER_UPLOADED_IMAGES_BUCKET_NAME}/{image_key}"
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.36.7.dev66
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.0.1
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.2.2
77
- Requires-Dist: transformers>=4.28.0
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'