khoj 1.24.2.dev2__py3-none-any.whl → 1.24.2.dev16__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 (88) hide show
  1. khoj/database/adapters/__init__.py +139 -16
  2. khoj/database/admin.py +2 -0
  3. khoj/database/migrations/0065_remove_agent_avatar_remove_agent_public_and_more.py +49 -0
  4. khoj/database/migrations/0066_remove_agent_tools_agent_input_tools_and_more.py +69 -0
  5. khoj/database/migrations/0067_alter_agent_style_icon.py +50 -0
  6. khoj/database/models/__init__.py +60 -18
  7. khoj/interface/compiled/404/index.html +1 -1
  8. khoj/interface/compiled/_next/static/chunks/1269-2e52d48e7d0e5c61.js +1 -0
  9. khoj/interface/compiled/_next/static/chunks/1603-67a89278e2c5dbe6.js +1 -0
  10. khoj/interface/compiled/_next/static/chunks/2697-a38d01981ad3bdf8.js +1 -0
  11. khoj/interface/compiled/_next/static/chunks/3110-ef2cacd1b8d79ad8.js +1 -0
  12. khoj/interface/compiled/_next/static/chunks/4086-2c74808ba38a5a0f.js +1 -0
  13. khoj/interface/compiled/_next/static/chunks/477-ec86e93db10571c1.js +1 -0
  14. khoj/interface/compiled/_next/static/chunks/51-e8f5bdb69b5ea421.js +1 -0
  15. khoj/interface/compiled/_next/static/chunks/9178-899fe9a6b754ecfe.js +1 -0
  16. khoj/interface/compiled/_next/static/chunks/9417-29502e39c3e7d60c.js +1 -0
  17. khoj/interface/compiled/_next/static/chunks/9479-7eed36fc954ef804.js +1 -0
  18. khoj/interface/compiled/_next/static/chunks/app/agents/page-df26b497b7356151.js +1 -0
  19. khoj/interface/compiled/_next/static/chunks/app/automations/page-1688dead2f21270d.js +1 -0
  20. khoj/interface/compiled/_next/static/chunks/app/chat/page-91abcb71846922b7.js +1 -0
  21. khoj/interface/compiled/_next/static/chunks/app/factchecker/page-7ab093711c27041c.js +1 -0
  22. khoj/interface/compiled/_next/static/chunks/app/page-fada198096eab47f.js +1 -0
  23. khoj/interface/compiled/_next/static/chunks/app/search/page-a7e036689b6507ff.js +1 -0
  24. khoj/interface/compiled/_next/static/chunks/app/settings/page-fa11cafaec7ab39f.js +1 -0
  25. khoj/interface/compiled/_next/static/chunks/app/share/chat/page-c5d2b9076e5390b2.js +1 -0
  26. khoj/interface/compiled/_next/static/chunks/{webpack-878fd47921816d3c.js → webpack-f52083d548d804fa.js} +1 -1
  27. khoj/interface/compiled/_next/static/css/4cae6c0e5c72fb2d.css +1 -0
  28. khoj/interface/compiled/_next/static/css/50d972a8c787730b.css +25 -0
  29. khoj/interface/compiled/_next/static/css/dfb67a9287720a2b.css +1 -0
  30. khoj/interface/compiled/agents/index.html +1 -1
  31. khoj/interface/compiled/agents/index.txt +2 -2
  32. khoj/interface/compiled/automations/index.html +1 -1
  33. khoj/interface/compiled/automations/index.txt +2 -2
  34. khoj/interface/compiled/chat/index.html +1 -1
  35. khoj/interface/compiled/chat/index.txt +2 -2
  36. khoj/interface/compiled/factchecker/index.html +1 -1
  37. khoj/interface/compiled/factchecker/index.txt +2 -2
  38. khoj/interface/compiled/index.html +1 -1
  39. khoj/interface/compiled/index.txt +2 -2
  40. khoj/interface/compiled/search/index.html +1 -1
  41. khoj/interface/compiled/search/index.txt +2 -2
  42. khoj/interface/compiled/settings/index.html +1 -1
  43. khoj/interface/compiled/settings/index.txt +2 -2
  44. khoj/interface/compiled/share/chat/index.html +1 -1
  45. khoj/interface/compiled/share/chat/index.txt +2 -2
  46. khoj/processor/content/notion/notion_to_entries.py +2 -1
  47. khoj/processor/conversation/anthropic/anthropic_chat.py +2 -0
  48. khoj/processor/conversation/google/gemini_chat.py +2 -0
  49. khoj/processor/conversation/offline/chat_model.py +3 -1
  50. khoj/processor/conversation/openai/gpt.py +2 -0
  51. khoj/processor/conversation/prompts.py +56 -5
  52. khoj/processor/image/generate.py +3 -1
  53. khoj/processor/tools/online_search.py +9 -7
  54. khoj/routers/api.py +34 -5
  55. khoj/routers/api_agents.py +232 -4
  56. khoj/routers/api_chat.py +46 -17
  57. khoj/routers/api_content.py +14 -0
  58. khoj/routers/helpers.py +113 -13
  59. khoj/search_type/text_search.py +4 -1
  60. khoj/utils/helpers.py +15 -2
  61. {khoj-1.24.2.dev2.dist-info → khoj-1.24.2.dev16.dist-info}/METADATA +1 -8
  62. {khoj-1.24.2.dev2.dist-info → khoj-1.24.2.dev16.dist-info}/RECORD +67 -64
  63. khoj/interface/compiled/_next/static/chunks/1603-3e2e1528e3b6ea1d.js +0 -1
  64. khoj/interface/compiled/_next/static/chunks/2697-a29cb9191a9e339c.js +0 -1
  65. khoj/interface/compiled/_next/static/chunks/6648-ee109f4ea33a74e2.js +0 -1
  66. khoj/interface/compiled/_next/static/chunks/7071-b4711cecca6619a8.js +0 -1
  67. khoj/interface/compiled/_next/static/chunks/743-1a64254447cda71f.js +0 -1
  68. khoj/interface/compiled/_next/static/chunks/8423-62ac6c832be2461b.js +0 -1
  69. khoj/interface/compiled/_next/static/chunks/9162-0be016519a18568b.js +0 -1
  70. khoj/interface/compiled/_next/static/chunks/9178-409f672ab573b8fd.js +0 -1
  71. khoj/interface/compiled/_next/static/chunks/9417-5d14ac74aaab2c66.js +0 -1
  72. khoj/interface/compiled/_next/static/chunks/9984-e410179c6fac7cf1.js +0 -1
  73. khoj/interface/compiled/_next/static/chunks/app/agents/page-a3db5b3869f83937.js +0 -1
  74. khoj/interface/compiled/_next/static/chunks/app/automations/page-e68cb1eba3cc41de.js +0 -1
  75. khoj/interface/compiled/_next/static/chunks/app/chat/page-5b1626fc2882c1f9.js +0 -1
  76. khoj/interface/compiled/_next/static/chunks/app/factchecker/page-b01f8a9b9107ecbe.js +0 -1
  77. khoj/interface/compiled/_next/static/chunks/app/page-ee9ee504f0d5ace6.js +0 -1
  78. khoj/interface/compiled/_next/static/chunks/app/search/page-53c2494182551684.js +0 -1
  79. khoj/interface/compiled/_next/static/chunks/app/settings/page-2a7e60e3782ed95e.js +0 -1
  80. khoj/interface/compiled/_next/static/chunks/app/share/chat/page-9d9faa4a155bbf58.js +0 -1
  81. khoj/interface/compiled/_next/static/css/24f141a6e37cd204.css +0 -25
  82. khoj/interface/compiled/_next/static/css/3e1f1fdd70775091.css +0 -1
  83. khoj/interface/compiled/_next/static/css/60fc94dfe42ddfe9.css +0 -1
  84. /khoj/interface/compiled/_next/static/{sXEsDJ1Vi3HypDes8jcxW → MyYNlmGMz32TGV_-febR4}/_buildManifest.js +0 -0
  85. /khoj/interface/compiled/_next/static/{sXEsDJ1Vi3HypDes8jcxW → MyYNlmGMz32TGV_-febR4}/_ssgManifest.js +0 -0
  86. {khoj-1.24.2.dev2.dist-info → khoj-1.24.2.dev16.dist-info}/WHEEL +0 -0
  87. {khoj-1.24.2.dev2.dist-info → khoj-1.24.2.dev16.dist-info}/entry_points.txt +0 -0
  88. {khoj-1.24.2.dev2.dist-info → khoj-1.24.2.dev16.dist-info}/licenses/LICENSE +0 -0
@@ -10,7 +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
+ from khoj.database.models import Agent, KhojUser
14
14
  from khoj.routers.helpers import (
15
15
  ChatEvent,
16
16
  extract_relevant_info,
@@ -57,16 +57,17 @@ async def search_online(
57
57
  send_status_func: Optional[Callable] = None,
58
58
  custom_filters: List[str] = [],
59
59
  uploaded_image_url: str = None,
60
+ agent: Agent = None,
60
61
  ):
61
62
  query += " ".join(custom_filters)
62
63
  if not is_internet_connected():
63
- logger.warn("Cannot search online as not connected to internet")
64
+ logger.warning("Cannot search online as not connected to internet")
64
65
  yield {}
65
66
  return
66
67
 
67
68
  # Breakdown the query into subqueries to get the correct answer
68
69
  subqueries = await generate_online_subqueries(
69
- query, conversation_history, location, user, uploaded_image_url=uploaded_image_url
70
+ query, conversation_history, location, user, uploaded_image_url=uploaded_image_url, agent=agent
70
71
  )
71
72
  response_dict = {}
72
73
 
@@ -101,7 +102,7 @@ async def search_online(
101
102
  async for event in send_status_func(f"**Reading web pages**: {webpage_links_str}"):
102
103
  yield {ChatEvent.STATUS: event}
103
104
  tasks = [
104
- read_webpage_and_extract_content(subquery, link, content, subscribed=subscribed)
105
+ read_webpage_and_extract_content(subquery, link, content, subscribed=subscribed, agent=agent)
105
106
  for link, subquery, content in webpages
106
107
  ]
107
108
  results = await asyncio.gather(*tasks)
@@ -143,6 +144,7 @@ async def read_webpages(
143
144
  subscribed: bool = False,
144
145
  send_status_func: Optional[Callable] = None,
145
146
  uploaded_image_url: str = None,
147
+ agent: Agent = None,
146
148
  ):
147
149
  "Infer web pages to read from the query and extract relevant information from them"
148
150
  logger.info(f"Inferring web pages to read")
@@ -156,7 +158,7 @@ async def read_webpages(
156
158
  webpage_links_str = "\n- " + "\n- ".join(list(urls))
157
159
  async for event in send_status_func(f"**Reading web pages**: {webpage_links_str}"):
158
160
  yield {ChatEvent.STATUS: event}
159
- tasks = [read_webpage_and_extract_content(query, url, subscribed=subscribed) for url in urls]
161
+ tasks = [read_webpage_and_extract_content(query, url, subscribed=subscribed, agent=agent) for url in urls]
160
162
  results = await asyncio.gather(*tasks)
161
163
 
162
164
  response: Dict[str, Dict] = defaultdict(dict)
@@ -167,14 +169,14 @@ async def read_webpages(
167
169
 
168
170
 
169
171
  async def read_webpage_and_extract_content(
170
- subquery: str, url: str, content: str = None, subscribed: bool = False
172
+ subquery: str, url: str, content: str = None, subscribed: bool = False, agent: Agent = None
171
173
  ) -> Tuple[str, Union[None, str], str]:
172
174
  try:
173
175
  if is_none_or_empty(content):
174
176
  with timer(f"Reading web page at '{url}' took", logger):
175
177
  content = await read_webpage_with_olostep(url) if OLOSTEP_API_KEY else await read_webpage_with_jina(url)
176
178
  with timer(f"Extracting relevant information from web page at '{url}' took", logger):
177
- extracted_info = await extract_relevant_info(subquery, content, subscribed=subscribed)
179
+ extracted_info = await extract_relevant_info(subquery, content, subscribed=subscribed, agent=agent)
178
180
  return subquery, extracted_info, url
179
181
  except Exception as e:
180
182
  logger.error(f"Failed to read web page at '{url}' with {e}")
khoj/routers/api.py CHANGED
@@ -27,7 +27,13 @@ from khoj.database.adapters import (
27
27
  get_user_photo,
28
28
  get_user_search_model_or_default,
29
29
  )
30
- from khoj.database.models import ChatModelOptions, KhojUser, SpeechToTextModelOptions
30
+ from khoj.database.models import (
31
+ Agent,
32
+ ChatModelOptions,
33
+ KhojUser,
34
+ SpeechToTextModelOptions,
35
+ )
36
+ from khoj.processor.conversation import prompts
31
37
  from khoj.processor.conversation.anthropic.anthropic_chat import (
32
38
  extract_questions_anthropic,
33
39
  )
@@ -106,6 +112,7 @@ async def execute_search(
106
112
  r: Optional[bool] = False,
107
113
  max_distance: Optional[Union[float, None]] = None,
108
114
  dedupe: Optional[bool] = True,
115
+ agent: Optional[Agent] = None,
109
116
  ):
110
117
  start_time = time.time()
111
118
 
@@ -157,6 +164,7 @@ async def execute_search(
157
164
  t,
158
165
  question_embedding=encoded_asymmetric_query,
159
166
  max_distance=max_distance,
167
+ agent=agent,
160
168
  )
161
169
  ]
162
170
 
@@ -333,6 +341,7 @@ async def extract_references_and_questions(
333
341
  location_data: LocationData = None,
334
342
  send_status_func: Optional[Callable] = None,
335
343
  uploaded_image_url: Optional[str] = None,
344
+ agent: Agent = None,
336
345
  ):
337
346
  user = request.user.object if request.user.is_authenticated else None
338
347
 
@@ -340,17 +349,30 @@ async def extract_references_and_questions(
340
349
  compiled_references: List[Any] = []
341
350
  inferred_queries: List[str] = []
342
351
 
352
+ agent_has_entries = False
353
+
354
+ if agent:
355
+ agent_has_entries = await sync_to_async(EntryAdapters.agent_has_entries)(agent=agent)
356
+
343
357
  if (
344
358
  not ConversationCommand.Notes in conversation_commands
345
359
  and not ConversationCommand.Default in conversation_commands
360
+ and not agent_has_entries
346
361
  ):
347
362
  yield compiled_references, inferred_queries, q
348
363
  return
349
364
 
365
+ # If Notes or Default is not in the conversation command, then the search should be restricted to the agent's knowledge base
366
+ should_limit_to_agent_knowledge = (
367
+ ConversationCommand.Notes not in conversation_commands
368
+ and ConversationCommand.Default not in conversation_commands
369
+ )
370
+
350
371
  if not await sync_to_async(EntryAdapters.user_has_entries)(user=user):
351
- logger.debug("No documents in knowledge base. Use a Khoj client to sync and chat with your docs.")
352
- yield compiled_references, inferred_queries, q
353
- return
372
+ if not agent_has_entries:
373
+ logger.debug("No documents in knowledge base. Use a Khoj client to sync and chat with your docs.")
374
+ yield compiled_references, inferred_queries, q
375
+ return
354
376
 
355
377
  # Extract filter terms from user message
356
378
  defiltered_query = q
@@ -368,6 +390,8 @@ async def extract_references_and_questions(
368
390
  using_offline_chat = False
369
391
  logger.debug(f"Filters in query: {filters_in_query}")
370
392
 
393
+ personality_context = prompts.personality_context.format(personality=agent.personality) if agent else ""
394
+
371
395
  # Infer search queries from user message
372
396
  with timer("Extracting search queries took", logger):
373
397
  # If we've reached here, either the user has enabled offline chat or the openai model is enabled.
@@ -392,6 +416,7 @@ async def extract_references_and_questions(
392
416
  location_data=location_data,
393
417
  user=user,
394
418
  max_prompt_size=conversation_config.max_prompt_size,
419
+ personality_context=personality_context,
395
420
  )
396
421
  elif conversation_config.model_type == ChatModelOptions.ModelType.OPENAI:
397
422
  openai_chat_config = conversation_config.openai_config
@@ -408,6 +433,7 @@ async def extract_references_and_questions(
408
433
  user=user,
409
434
  uploaded_image_url=uploaded_image_url,
410
435
  vision_enabled=vision_enabled,
436
+ personality_context=personality_context,
411
437
  )
412
438
  elif conversation_config.model_type == ChatModelOptions.ModelType.ANTHROPIC:
413
439
  api_key = conversation_config.openai_config.api_key
@@ -419,6 +445,7 @@ async def extract_references_and_questions(
419
445
  conversation_log=meta_log,
420
446
  location_data=location_data,
421
447
  user=user,
448
+ personality_context=personality_context,
422
449
  )
423
450
  elif conversation_config.model_type == ChatModelOptions.ModelType.GOOGLE:
424
451
  api_key = conversation_config.openai_config.api_key
@@ -431,6 +458,7 @@ async def extract_references_and_questions(
431
458
  location_data=location_data,
432
459
  max_tokens=conversation_config.max_prompt_size,
433
460
  user=user,
461
+ personality_context=personality_context,
434
462
  )
435
463
 
436
464
  # Collate search results as context for GPT
@@ -445,13 +473,14 @@ async def extract_references_and_questions(
445
473
  n_items = min(n, 3) if using_offline_chat else n
446
474
  search_results.extend(
447
475
  await execute_search(
448
- user,
476
+ user if not should_limit_to_agent_knowledge else None,
449
477
  f"{query} {filters_in_query}",
450
478
  n=n_items,
451
479
  t=SearchType.All,
452
480
  r=True,
453
481
  max_distance=d,
454
482
  dedupe=False,
483
+ agent=agent,
455
484
  )
456
485
  )
457
486
  search_results = text_search.deduplicated_search_responses(search_results)
@@ -1,13 +1,22 @@
1
1
  import json
2
2
  import logging
3
+ from typing import Dict, List, Optional
3
4
 
5
+ from asgiref.sync import sync_to_async
4
6
  from fastapi import APIRouter, Request
5
7
  from fastapi.requests import Request
6
8
  from fastapi.responses import Response
9
+ from pydantic import BaseModel
10
+ from starlette.authentication import requires
7
11
 
8
12
  from khoj.database.adapters import AgentAdapters
9
- from khoj.database.models import KhojUser
10
- from khoj.routers.helpers import CommonQueryParams
13
+ from khoj.database.models import Agent, KhojUser
14
+ from khoj.routers.helpers import CommonQueryParams, acheck_if_safe_prompt
15
+ from khoj.utils.helpers import (
16
+ ConversationCommand,
17
+ command_descriptions_for_agent,
18
+ mode_descriptions_for_agent,
19
+ )
11
20
 
12
21
  # Initialize Router
13
22
  logger = logging.getLogger(__name__)
@@ -16,6 +25,18 @@ logger = logging.getLogger(__name__)
16
25
  api_agents = APIRouter()
17
26
 
18
27
 
28
+ class ModifyAgentBody(BaseModel):
29
+ name: str
30
+ persona: str
31
+ privacy_level: str
32
+ icon: str
33
+ color: str
34
+ chat_model: str
35
+ files: Optional[List[str]] = []
36
+ input_tools: Optional[List[str]] = []
37
+ output_modes: Optional[List[str]] = []
38
+
39
+
19
40
  @api_agents.get("", response_class=Response)
20
41
  async def all_agents(
21
42
  request: Request,
@@ -25,17 +46,22 @@ async def all_agents(
25
46
  agents = await AgentAdapters.aget_all_accessible_agents(user)
26
47
  agents_packet = list()
27
48
  for agent in agents:
49
+ files = agent.fileobject_set.all()
50
+ file_names = [file.file_name for file in files]
28
51
  agents_packet.append(
29
52
  {
30
53
  "slug": agent.slug,
31
- "avatar": agent.avatar,
32
54
  "name": agent.name,
33
55
  "persona": agent.personality,
34
- "public": agent.public,
35
56
  "creator": agent.creator.username if agent.creator else None,
36
57
  "managed_by_admin": agent.managed_by_admin,
37
58
  "color": agent.style_color,
38
59
  "icon": agent.style_icon,
60
+ "privacy_level": agent.privacy_level,
61
+ "chat_model": agent.chat_model.chat_model,
62
+ "files": file_names,
63
+ "input_tools": agent.input_tools,
64
+ "output_modes": agent.output_modes,
39
65
  }
40
66
  )
41
67
 
@@ -43,3 +69,205 @@ async def all_agents(
43
69
  agents_packet.sort(key=lambda x: x["name"])
44
70
  agents_packet.sort(key=lambda x: x["slug"] == "khoj", reverse=True)
45
71
  return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200)
72
+
73
+
74
+ @api_agents.get("/options", response_class=Response)
75
+ async def get_agent_configuration_options(
76
+ request: Request,
77
+ common: CommonQueryParams,
78
+ ) -> Response:
79
+ agent_input_tools = [key for key, _ in Agent.InputToolOptions.choices]
80
+ agent_output_modes = [key for key, _ in Agent.OutputModeOptions.choices]
81
+
82
+ agent_input_tool_with_descriptions: Dict[str, str] = {}
83
+ for key in agent_input_tools:
84
+ conversation_command = ConversationCommand(key)
85
+ agent_input_tool_with_descriptions[key] = command_descriptions_for_agent[conversation_command]
86
+
87
+ agent_output_modes_with_descriptions: Dict[str, str] = {}
88
+ for key in agent_output_modes:
89
+ conversation_command = ConversationCommand(key)
90
+ agent_output_modes_with_descriptions[key] = mode_descriptions_for_agent[conversation_command]
91
+
92
+ return Response(
93
+ content=json.dumps(
94
+ {
95
+ "input_tools": agent_input_tool_with_descriptions,
96
+ "output_modes": agent_output_modes_with_descriptions,
97
+ }
98
+ ),
99
+ media_type="application/json",
100
+ status_code=200,
101
+ )
102
+
103
+
104
+ @api_agents.get("/{agent_slug}", response_class=Response)
105
+ async def get_agent(
106
+ request: Request,
107
+ common: CommonQueryParams,
108
+ agent_slug: str,
109
+ ) -> Response:
110
+ user: KhojUser = request.user.object if request.user.is_authenticated else None
111
+ agent = await AgentAdapters.aget_readonly_agent_by_slug(agent_slug, user)
112
+
113
+ if not agent:
114
+ return Response(
115
+ content=json.dumps({"error": f"Agent with name {agent_slug} not found."}),
116
+ media_type="application/json",
117
+ status_code=404,
118
+ )
119
+
120
+ files = agent.fileobject_set.all()
121
+ file_names = [file.file_name for file in files]
122
+ agents_packet = {
123
+ "slug": agent.slug,
124
+ "name": agent.name,
125
+ "persona": agent.personality,
126
+ "creator": agent.creator.username if agent.creator else None,
127
+ "managed_by_admin": agent.managed_by_admin,
128
+ "color": agent.style_color,
129
+ "icon": agent.style_icon,
130
+ "privacy_level": agent.privacy_level,
131
+ "chat_model": agent.chat_model.chat_model,
132
+ "files": file_names,
133
+ "input_tools": agent.input_tools,
134
+ "output_modes": agent.output_modes,
135
+ }
136
+
137
+ return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200)
138
+
139
+
140
+ @api_agents.delete("/{agent_slug}", response_class=Response)
141
+ @requires(["authenticated"])
142
+ async def delete_agent(
143
+ request: Request,
144
+ common: CommonQueryParams,
145
+ agent_slug: str,
146
+ ) -> Response:
147
+ user: KhojUser = request.user.object
148
+
149
+ agent = await AgentAdapters.aget_agent_by_slug(agent_slug, user)
150
+
151
+ if not agent:
152
+ return Response(
153
+ content=json.dumps({"error": f"Agent with name {agent_slug} not found."}),
154
+ media_type="application/json",
155
+ status_code=404,
156
+ )
157
+
158
+ await AgentAdapters.adelete_agent_by_slug(agent_slug, user)
159
+
160
+ return Response(content=json.dumps({"message": "Agent deleted."}), media_type="application/json", status_code=200)
161
+
162
+
163
+ @api_agents.post("", response_class=Response)
164
+ @requires(["authenticated"])
165
+ async def create_agent(
166
+ request: Request,
167
+ common: CommonQueryParams,
168
+ body: ModifyAgentBody,
169
+ ) -> Response:
170
+ user: KhojUser = request.user.object
171
+
172
+ is_safe_prompt, reason = True, ""
173
+
174
+ if body.privacy_level != Agent.PrivacyLevel.PRIVATE:
175
+ is_safe_prompt, reason = await acheck_if_safe_prompt(body.persona)
176
+
177
+ if not is_safe_prompt:
178
+ return Response(
179
+ content=json.dumps({"error": f"{reason}"}),
180
+ media_type="application/json",
181
+ status_code=400,
182
+ )
183
+
184
+ agent = await AgentAdapters.aupdate_agent(
185
+ user,
186
+ body.name,
187
+ body.persona,
188
+ body.privacy_level,
189
+ body.icon,
190
+ body.color,
191
+ body.chat_model,
192
+ body.files,
193
+ body.input_tools,
194
+ body.output_modes,
195
+ )
196
+
197
+ agents_packet = {
198
+ "slug": agent.slug,
199
+ "name": agent.name,
200
+ "persona": agent.personality,
201
+ "creator": agent.creator.username if agent.creator else None,
202
+ "managed_by_admin": agent.managed_by_admin,
203
+ "color": agent.style_color,
204
+ "icon": agent.style_icon,
205
+ "privacy_level": agent.privacy_level,
206
+ "chat_model": agent.chat_model.chat_model,
207
+ "files": body.files,
208
+ "input_tools": agent.input_tools,
209
+ "output_modes": agent.output_modes,
210
+ }
211
+
212
+ return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200)
213
+
214
+
215
+ @api_agents.patch("", response_class=Response)
216
+ @requires(["authenticated"])
217
+ async def update_agent(
218
+ request: Request,
219
+ common: CommonQueryParams,
220
+ body: ModifyAgentBody,
221
+ ) -> Response:
222
+ user: KhojUser = request.user.object
223
+
224
+ is_safe_prompt, reason = True, ""
225
+
226
+ if body.privacy_level != Agent.PrivacyLevel.PRIVATE:
227
+ is_safe_prompt, reason = await acheck_if_safe_prompt(body.persona)
228
+
229
+ if not is_safe_prompt:
230
+ return Response(
231
+ content=json.dumps({"error": f"{reason}"}),
232
+ media_type="application/json",
233
+ status_code=400,
234
+ )
235
+
236
+ selected_agent = await AgentAdapters.aget_agent_by_name(body.name, user)
237
+
238
+ if not selected_agent:
239
+ return Response(
240
+ content=json.dumps({"error": f"Agent with name {body.name} not found."}),
241
+ media_type="application/json",
242
+ status_code=404,
243
+ )
244
+
245
+ agent = await AgentAdapters.aupdate_agent(
246
+ user,
247
+ body.name,
248
+ body.persona,
249
+ body.privacy_level,
250
+ body.icon,
251
+ body.color,
252
+ body.chat_model,
253
+ body.files,
254
+ body.input_tools,
255
+ body.output_modes,
256
+ )
257
+
258
+ agents_packet = {
259
+ "slug": agent.slug,
260
+ "name": agent.name,
261
+ "persona": agent.personality,
262
+ "creator": agent.creator.username if agent.creator else None,
263
+ "managed_by_admin": agent.managed_by_admin,
264
+ "color": agent.style_color,
265
+ "icon": agent.style_icon,
266
+ "privacy_level": agent.privacy_level,
267
+ "chat_model": agent.chat_model.chat_model,
268
+ "files": body.files,
269
+ "input_tools": agent.input_tools,
270
+ "output_modes": agent.output_modes,
271
+ }
272
+
273
+ return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200)
khoj/routers/api_chat.py CHANGED
@@ -17,13 +17,14 @@ from starlette.authentication import has_required_scope, requires
17
17
 
18
18
  from khoj.app.settings import ALLOWED_HOSTS
19
19
  from khoj.database.adapters import (
20
+ AgentAdapters,
20
21
  ConversationAdapters,
21
22
  EntryAdapters,
22
23
  FileObjectAdapters,
23
24
  PublicConversationAdapters,
24
25
  aget_user_name,
25
26
  )
26
- from khoj.database.models import KhojUser
27
+ from khoj.database.models import Agent, KhojUser
27
28
  from khoj.processor.conversation.prompts import help_message, no_entries_found
28
29
  from khoj.processor.conversation.utils import save_to_conversation_log
29
30
  from khoj.processor.image.generate import text_to_image
@@ -65,7 +66,7 @@ from khoj.utils.rawconfig import FileFilterRequest, FilesFilterRequest, Location
65
66
  # Initialize Router
66
67
  logger = logging.getLogger(__name__)
67
68
  conversation_command_rate_limiter = ConversationCommandRateLimiter(
68
- trial_rate_limit=100, subscribed_rate_limit=100, slug="command"
69
+ trial_rate_limit=100, subscribed_rate_limit=6000, slug="command"
69
70
  )
70
71
 
71
72
 
@@ -211,7 +212,6 @@ def chat_history(
211
212
  agent_metadata = {
212
213
  "slug": conversation.agent.slug,
213
214
  "name": conversation.agent.name,
214
- "avatar": conversation.agent.avatar,
215
215
  "isCreator": conversation.agent.creator == user,
216
216
  "color": conversation.agent.style_color,
217
217
  "icon": conversation.agent.style_icon,
@@ -268,7 +268,6 @@ def get_shared_chat(
268
268
  agent_metadata = {
269
269
  "slug": conversation.agent.slug,
270
270
  "name": conversation.agent.name,
271
- "avatar": conversation.agent.avatar,
272
271
  "isCreator": conversation.agent.creator == user,
273
272
  "color": conversation.agent.style_color,
274
273
  "icon": conversation.agent.style_icon,
@@ -418,7 +417,7 @@ def chat_sessions(
418
417
  conversations = conversations[:8]
419
418
 
420
419
  sessions = conversations.values_list(
421
- "id", "slug", "title", "agent__slug", "agent__name", "agent__avatar", "created_at", "updated_at"
420
+ "id", "slug", "title", "agent__slug", "agent__name", "created_at", "updated_at"
422
421
  )
423
422
 
424
423
  session_values = [
@@ -426,9 +425,8 @@ def chat_sessions(
426
425
  "conversation_id": str(session[0]),
427
426
  "slug": session[2] or session[1],
428
427
  "agent_name": session[4],
429
- "agent_avatar": session[5],
430
- "created": session[6].strftime("%Y-%m-%d %H:%M:%S"),
431
- "updated": session[7].strftime("%Y-%m-%d %H:%M:%S"),
428
+ "created": session[5].strftime("%Y-%m-%d %H:%M:%S"),
429
+ "updated": session[6].strftime("%Y-%m-%d %H:%M:%S"),
432
430
  }
433
431
  for session in sessions
434
432
  ]
@@ -590,7 +588,7 @@ async def chat(
590
588
  nonlocal connection_alive, ttft
591
589
  if not connection_alive or await request.is_disconnected():
592
590
  connection_alive = False
593
- logger.warn(f"User {user} disconnected from {common.client} client")
591
+ logger.warning(f"User {user} disconnected from {common.client} client")
594
592
  return
595
593
  try:
596
594
  if event_type == ChatEvent.END_LLM_RESPONSE:
@@ -658,6 +656,16 @@ async def chat(
658
656
  return
659
657
  conversation_id = conversation.id
660
658
 
659
+ agent: Agent | None = None
660
+ default_agent = await AgentAdapters.aget_default_agent()
661
+ if conversation.agent and conversation.agent != default_agent:
662
+ agent = conversation.agent
663
+
664
+ if not conversation.agent:
665
+ conversation.agent = default_agent
666
+ await conversation.asave()
667
+ agent = default_agent
668
+
661
669
  await is_ready_to_chat(user)
662
670
 
663
671
  user_name = await aget_user_name(user)
@@ -677,7 +685,12 @@ async def chat(
677
685
 
678
686
  if conversation_commands == [ConversationCommand.Default] or is_automated_task:
679
687
  conversation_commands = await aget_relevant_information_sources(
680
- q, meta_log, is_automated_task, subscribed=subscribed, uploaded_image_url=uploaded_image_url
688
+ q,
689
+ meta_log,
690
+ is_automated_task,
691
+ subscribed=subscribed,
692
+ uploaded_image_url=uploaded_image_url,
693
+ agent=agent,
681
694
  )
682
695
  conversation_commands_str = ", ".join([cmd.value for cmd in conversation_commands])
683
696
  async for result in send_event(
@@ -685,7 +698,7 @@ async def chat(
685
698
  ):
686
699
  yield result
687
700
 
688
- mode = await aget_relevant_output_modes(q, meta_log, is_automated_task, uploaded_image_url)
701
+ mode = await aget_relevant_output_modes(q, meta_log, is_automated_task, uploaded_image_url, agent)
689
702
  async for result in send_event(ChatEvent.STATUS, f"**Decided Response Mode:** {mode.value}"):
690
703
  yield result
691
704
  if mode not in conversation_commands:
@@ -709,19 +722,30 @@ async def chat(
709
722
  conversation_commands.remove(ConversationCommand.Summarize)
710
723
  elif ConversationCommand.Summarize in conversation_commands:
711
724
  response_log = ""
712
- if len(file_filters) == 0:
725
+ agent_has_entries = await EntryAdapters.aagent_has_entries(agent)
726
+ if len(file_filters) == 0 and not agent_has_entries:
713
727
  response_log = "No files selected for summarization. Please add files using the section on the left."
714
728
  async for result in send_llm_response(response_log):
715
729
  yield result
716
- elif len(file_filters) > 1:
730
+ elif len(file_filters) > 1 and not agent_has_entries:
717
731
  response_log = "Only one file can be selected for summarization."
718
732
  async for result in send_llm_response(response_log):
719
733
  yield result
720
734
  else:
721
735
  try:
722
- file_object = await FileObjectAdapters.async_get_file_objects_by_name(user, file_filters[0])
736
+ file_object = None
737
+ if await EntryAdapters.aagent_has_entries(agent):
738
+ file_names = await EntryAdapters.aget_agent_entry_filepaths(agent)
739
+ if len(file_names) > 0:
740
+ file_object = await FileObjectAdapters.async_get_file_objects_by_name(
741
+ None, file_names[0], agent
742
+ )
743
+
744
+ if len(file_filters) > 0:
745
+ file_object = await FileObjectAdapters.async_get_file_objects_by_name(user, file_filters[0])
746
+
723
747
  if len(file_object) == 0:
724
- response_log = "Sorry, we couldn't find the full text of this file. Please re-upload the document and try again."
748
+ response_log = "Sorry, I couldn't find the full text of this file. Please re-upload the document and try again."
725
749
  async for result in send_llm_response(response_log):
726
750
  yield result
727
751
  return
@@ -734,13 +758,13 @@ async def chat(
734
758
  yield result
735
759
 
736
760
  response = await extract_relevant_summary(
737
- q, contextual_data, subscribed=subscribed, uploaded_image_url=uploaded_image_url
761
+ q, contextual_data, subscribed=subscribed, uploaded_image_url=uploaded_image_url, agent=agent
738
762
  )
739
763
  response_log = str(response)
740
764
  async for result in send_llm_response(response_log):
741
765
  yield result
742
766
  except Exception as e:
743
- response_log = "Error summarizing file."
767
+ response_log = "Error summarizing file. Please try again, or contact support."
744
768
  logger.error(f"Error summarizing file for {user.email}: {e}", exc_info=True)
745
769
  async for result in send_llm_response(response_log):
746
770
  yield result
@@ -816,6 +840,7 @@ async def chat(
816
840
  location,
817
841
  partial(send_event, ChatEvent.STATUS),
818
842
  uploaded_image_url=uploaded_image_url,
843
+ agent=agent,
819
844
  ):
820
845
  if isinstance(result, dict) and ChatEvent.STATUS in result:
821
846
  yield result[ChatEvent.STATUS]
@@ -853,6 +878,7 @@ async def chat(
853
878
  partial(send_event, ChatEvent.STATUS),
854
879
  custom_filters,
855
880
  uploaded_image_url=uploaded_image_url,
881
+ agent=agent,
856
882
  ):
857
883
  if isinstance(result, dict) and ChatEvent.STATUS in result:
858
884
  yield result[ChatEvent.STATUS]
@@ -876,6 +902,7 @@ async def chat(
876
902
  subscribed,
877
903
  partial(send_event, ChatEvent.STATUS),
878
904
  uploaded_image_url=uploaded_image_url,
905
+ agent=agent,
879
906
  ):
880
907
  if isinstance(result, dict) and ChatEvent.STATUS in result:
881
908
  yield result[ChatEvent.STATUS]
@@ -922,6 +949,7 @@ async def chat(
922
949
  subscribed=subscribed,
923
950
  send_status_func=partial(send_event, ChatEvent.STATUS),
924
951
  uploaded_image_url=uploaded_image_url,
952
+ agent=agent,
925
953
  ):
926
954
  if isinstance(result, dict) and ChatEvent.STATUS in result:
927
955
  yield result[ChatEvent.STATUS]
@@ -1132,6 +1160,7 @@ async def get_chat(
1132
1160
  yield result
1133
1161
  return
1134
1162
  conversation_id = conversation.id
1163
+ agent = conversation.agent if conversation.agent else None
1135
1164
 
1136
1165
  await is_ready_to_chat(user)
1137
1166