khoj 1.24.2.dev4__py3-none-any.whl → 1.24.2.dev7__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 (82) hide show
  1. khoj/database/adapters/__init__.py +125 -13
  2. khoj/database/migrations/0065_remove_agent_avatar_remove_agent_public_and_more.py +49 -0
  3. khoj/database/migrations/0066_remove_agent_tools_agent_input_tools_and_more.py +69 -0
  4. khoj/database/migrations/0067_alter_agent_style_icon.py +50 -0
  5. khoj/database/models/__init__.py +60 -18
  6. khoj/interface/compiled/404/index.html +1 -1
  7. khoj/interface/compiled/_next/static/chunks/1269-2e52d48e7d0e5c61.js +1 -0
  8. khoj/interface/compiled/_next/static/chunks/1603-67a89278e2c5dbe6.js +1 -0
  9. khoj/interface/compiled/_next/static/chunks/2697-f27cc03429fc6482.js +1 -0
  10. khoj/interface/compiled/_next/static/chunks/3110-ef2cacd1b8d79ad8.js +1 -0
  11. khoj/interface/compiled/_next/static/chunks/4086-2c74808ba38a5a0f.js +1 -0
  12. khoj/interface/compiled/_next/static/chunks/477-ec86e93db10571c1.js +1 -0
  13. khoj/interface/compiled/_next/static/chunks/51-e8f5bdb69b5ea421.js +1 -0
  14. khoj/interface/compiled/_next/static/chunks/9178-c024734a1aa3228a.js +1 -0
  15. khoj/interface/compiled/_next/static/chunks/9417-29502e39c3e7d60c.js +1 -0
  16. khoj/interface/compiled/_next/static/chunks/9479-7eed36fc954ef804.js +1 -0
  17. khoj/interface/compiled/_next/static/chunks/app/agents/page-0d0139f61c0e134b.js +1 -0
  18. khoj/interface/compiled/_next/static/chunks/app/automations/page-2edc21f30819def4.js +1 -0
  19. khoj/interface/compiled/_next/static/chunks/app/chat/page-4309c98e6dc497dd.js +1 -0
  20. khoj/interface/compiled/_next/static/chunks/app/factchecker/page-f2c83e3a87a28657.js +1 -0
  21. khoj/interface/compiled/_next/static/chunks/app/page-8660f2e832e57de8.js +1 -0
  22. khoj/interface/compiled/_next/static/chunks/app/search/page-b807caebd7f278c7.js +1 -0
  23. khoj/interface/compiled/_next/static/chunks/app/settings/page-2932356ad11c2f7b.js +1 -0
  24. khoj/interface/compiled/_next/static/chunks/app/share/chat/page-a736a0826570af2b.js +1 -0
  25. khoj/interface/compiled/_next/static/chunks/{webpack-8cb92a712ecd16e2.js → webpack-e51d8a8842d5bc67.js} +1 -1
  26. khoj/interface/compiled/_next/static/css/e294b18216ce4cd6.css +25 -0
  27. khoj/interface/compiled/_next/static/css/{b1094827d745306b.css → f768dddada62459d.css} +1 -1
  28. khoj/interface/compiled/agents/index.html +1 -1
  29. khoj/interface/compiled/agents/index.txt +2 -2
  30. khoj/interface/compiled/automations/index.html +1 -1
  31. khoj/interface/compiled/automations/index.txt +2 -2
  32. khoj/interface/compiled/chat/index.html +1 -1
  33. khoj/interface/compiled/chat/index.txt +2 -2
  34. khoj/interface/compiled/factchecker/index.html +1 -1
  35. khoj/interface/compiled/factchecker/index.txt +2 -2
  36. khoj/interface/compiled/index.html +1 -1
  37. khoj/interface/compiled/index.txt +2 -2
  38. khoj/interface/compiled/search/index.html +1 -1
  39. khoj/interface/compiled/search/index.txt +2 -2
  40. khoj/interface/compiled/settings/index.html +1 -1
  41. khoj/interface/compiled/settings/index.txt +2 -2
  42. khoj/interface/compiled/share/chat/index.html +1 -1
  43. khoj/interface/compiled/share/chat/index.txt +2 -2
  44. khoj/processor/conversation/anthropic/anthropic_chat.py +2 -0
  45. khoj/processor/conversation/google/gemini_chat.py +2 -0
  46. khoj/processor/conversation/offline/chat_model.py +3 -1
  47. khoj/processor/conversation/openai/gpt.py +2 -0
  48. khoj/processor/conversation/prompts.py +56 -5
  49. khoj/processor/image/generate.py +3 -1
  50. khoj/processor/tools/online_search.py +9 -7
  51. khoj/routers/api.py +21 -4
  52. khoj/routers/api_agents.py +224 -4
  53. khoj/routers/api_chat.py +24 -11
  54. khoj/routers/helpers.py +100 -13
  55. khoj/search_type/text_search.py +4 -1
  56. khoj/utils/helpers.py +15 -2
  57. {khoj-1.24.2.dev4.dist-info → khoj-1.24.2.dev7.dist-info}/METADATA +1 -8
  58. {khoj-1.24.2.dev4.dist-info → khoj-1.24.2.dev7.dist-info}/RECORD +63 -60
  59. khoj/interface/compiled/_next/static/chunks/1603-3e2e1528e3b6ea1d.js +0 -1
  60. khoj/interface/compiled/_next/static/chunks/2697-a29cb9191a9e339c.js +0 -1
  61. khoj/interface/compiled/_next/static/chunks/6648-ee109f4ea33a74e2.js +0 -1
  62. khoj/interface/compiled/_next/static/chunks/7071-b4711cecca6619a8.js +0 -1
  63. khoj/interface/compiled/_next/static/chunks/743-1a64254447cda71f.js +0 -1
  64. khoj/interface/compiled/_next/static/chunks/8423-62ac6c832be2461b.js +0 -1
  65. khoj/interface/compiled/_next/static/chunks/9162-0be016519a18568b.js +0 -1
  66. khoj/interface/compiled/_next/static/chunks/9178-7e815211edcb3657.js +0 -1
  67. khoj/interface/compiled/_next/static/chunks/9417-5d14ac74aaab2c66.js +0 -1
  68. khoj/interface/compiled/_next/static/chunks/9984-e410179c6fac7cf1.js +0 -1
  69. khoj/interface/compiled/_next/static/chunks/app/agents/page-d302911777a3e027.js +0 -1
  70. khoj/interface/compiled/_next/static/chunks/app/automations/page-0a5de8c254c29a1c.js +0 -1
  71. khoj/interface/compiled/_next/static/chunks/app/chat/page-d96bf6a84bb05290.js +0 -1
  72. khoj/interface/compiled/_next/static/chunks/app/factchecker/page-32e61af29e6b431d.js +0 -1
  73. khoj/interface/compiled/_next/static/chunks/app/page-96cab08c985716f4.js +0 -1
  74. khoj/interface/compiled/_next/static/chunks/app/search/page-b3193d46c65571c5.js +0 -1
  75. khoj/interface/compiled/_next/static/chunks/app/settings/page-0db9b708366606ec.js +0 -1
  76. khoj/interface/compiled/_next/static/chunks/app/share/chat/page-f06ac16cfe5b5a16.js +0 -1
  77. khoj/interface/compiled/_next/static/css/24f141a6e37cd204.css +0 -25
  78. /khoj/interface/compiled/_next/static/{9Px2MOmCuHSTa2RFA3GJa → Lx6Rbn2r5e9TBU0Pqievq}/_buildManifest.js +0 -0
  79. /khoj/interface/compiled/_next/static/{9Px2MOmCuHSTa2RFA3GJa → Lx6Rbn2r5e9TBU0Pqievq}/_ssgManifest.js +0 -0
  80. {khoj-1.24.2.dev4.dist-info → khoj-1.24.2.dev7.dist-info}/WHEEL +0 -0
  81. {khoj-1.24.2.dev4.dist-info → khoj-1.24.2.dev7.dist-info}/entry_points.txt +0 -0
  82. {khoj-1.24.2.dev4.dist-info → khoj-1.24.2.dev7.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
 
@@ -348,9 +357,10 @@ async def extract_references_and_questions(
348
357
  return
349
358
 
350
359
  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
360
+ if not await sync_to_async(EntryAdapters.agent_has_entries)(agent=agent):
361
+ logger.debug("No documents in knowledge base. Use a Khoj client to sync and chat with your docs.")
362
+ yield compiled_references, inferred_queries, q
363
+ return
354
364
 
355
365
  # Extract filter terms from user message
356
366
  defiltered_query = q
@@ -368,6 +378,8 @@ async def extract_references_and_questions(
368
378
  using_offline_chat = False
369
379
  logger.debug(f"Filters in query: {filters_in_query}")
370
380
 
381
+ personality_context = prompts.personality_context.format(personality=agent.personality) if agent else ""
382
+
371
383
  # Infer search queries from user message
372
384
  with timer("Extracting search queries took", logger):
373
385
  # If we've reached here, either the user has enabled offline chat or the openai model is enabled.
@@ -392,6 +404,7 @@ async def extract_references_and_questions(
392
404
  location_data=location_data,
393
405
  user=user,
394
406
  max_prompt_size=conversation_config.max_prompt_size,
407
+ personality_context=personality_context,
395
408
  )
396
409
  elif conversation_config.model_type == ChatModelOptions.ModelType.OPENAI:
397
410
  openai_chat_config = conversation_config.openai_config
@@ -408,6 +421,7 @@ async def extract_references_and_questions(
408
421
  user=user,
409
422
  uploaded_image_url=uploaded_image_url,
410
423
  vision_enabled=vision_enabled,
424
+ personality_context=personality_context,
411
425
  )
412
426
  elif conversation_config.model_type == ChatModelOptions.ModelType.ANTHROPIC:
413
427
  api_key = conversation_config.openai_config.api_key
@@ -419,6 +433,7 @@ async def extract_references_and_questions(
419
433
  conversation_log=meta_log,
420
434
  location_data=location_data,
421
435
  user=user,
436
+ personality_context=personality_context,
422
437
  )
423
438
  elif conversation_config.model_type == ChatModelOptions.ModelType.GOOGLE:
424
439
  api_key = conversation_config.openai_config.api_key
@@ -431,6 +446,7 @@ async def extract_references_and_questions(
431
446
  location_data=location_data,
432
447
  max_tokens=conversation_config.max_prompt_size,
433
448
  user=user,
449
+ personality_context=personality_context,
434
450
  )
435
451
 
436
452
  # Collate search results as context for GPT
@@ -452,6 +468,7 @@ async def extract_references_and_questions(
452
468
  r=True,
453
469
  max_distance=d,
454
470
  dedupe=False,
471
+ agent=agent,
455
472
  )
456
473
  )
457
474
  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,197 @@ 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 = await acheck_if_safe_prompt(body.persona)
173
+ if not is_safe_prompt:
174
+ return Response(
175
+ content=json.dumps({"error": f"{reason}"}),
176
+ media_type="application/json",
177
+ status_code=400,
178
+ )
179
+
180
+ agent = await AgentAdapters.aupdate_agent(
181
+ user,
182
+ body.name,
183
+ body.persona,
184
+ body.privacy_level,
185
+ body.icon,
186
+ body.color,
187
+ body.chat_model,
188
+ body.files,
189
+ body.input_tools,
190
+ body.output_modes,
191
+ )
192
+
193
+ agents_packet = {
194
+ "slug": agent.slug,
195
+ "name": agent.name,
196
+ "persona": agent.personality,
197
+ "creator": agent.creator.username if agent.creator else None,
198
+ "managed_by_admin": agent.managed_by_admin,
199
+ "color": agent.style_color,
200
+ "icon": agent.style_icon,
201
+ "privacy_level": agent.privacy_level,
202
+ "chat_model": agent.chat_model.chat_model,
203
+ "files": body.files,
204
+ "input_tools": agent.input_tools,
205
+ "output_modes": agent.output_modes,
206
+ }
207
+
208
+ return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200)
209
+
210
+
211
+ @api_agents.patch("", response_class=Response)
212
+ @requires(["authenticated"])
213
+ async def update_agent(
214
+ request: Request,
215
+ common: CommonQueryParams,
216
+ body: ModifyAgentBody,
217
+ ) -> Response:
218
+ user: KhojUser = request.user.object
219
+
220
+ is_safe_prompt, reason = await acheck_if_safe_prompt(body.persona)
221
+ if not is_safe_prompt:
222
+ return Response(
223
+ content=json.dumps({"error": f"{reason}"}),
224
+ media_type="application/json",
225
+ status_code=400,
226
+ )
227
+
228
+ selected_agent = await AgentAdapters.aget_agent_by_name(body.name, user)
229
+
230
+ if not selected_agent:
231
+ return Response(
232
+ content=json.dumps({"error": f"Agent with name {body.name} not found."}),
233
+ media_type="application/json",
234
+ status_code=404,
235
+ )
236
+
237
+ agent = await AgentAdapters.aupdate_agent(
238
+ user,
239
+ body.name,
240
+ body.persona,
241
+ body.privacy_level,
242
+ body.icon,
243
+ body.color,
244
+ body.chat_model,
245
+ body.files,
246
+ body.input_tools,
247
+ body.output_modes,
248
+ )
249
+
250
+ agents_packet = {
251
+ "slug": agent.slug,
252
+ "name": agent.name,
253
+ "persona": agent.personality,
254
+ "creator": agent.creator.username if agent.creator else None,
255
+ "managed_by_admin": agent.managed_by_admin,
256
+ "color": agent.style_color,
257
+ "icon": agent.style_icon,
258
+ "privacy_level": agent.privacy_level,
259
+ "chat_model": agent.chat_model.chat_model,
260
+ "files": body.files,
261
+ "input_tools": agent.input_tools,
262
+ "output_modes": agent.output_modes,
263
+ }
264
+
265
+ 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
@@ -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,11 @@ 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
+
661
664
  await is_ready_to_chat(user)
662
665
 
663
666
  user_name = await aget_user_name(user)
@@ -677,7 +680,12 @@ async def chat(
677
680
 
678
681
  if conversation_commands == [ConversationCommand.Default] or is_automated_task:
679
682
  conversation_commands = await aget_relevant_information_sources(
680
- q, meta_log, is_automated_task, subscribed=subscribed, uploaded_image_url=uploaded_image_url
683
+ q,
684
+ meta_log,
685
+ is_automated_task,
686
+ subscribed=subscribed,
687
+ uploaded_image_url=uploaded_image_url,
688
+ agent=agent,
681
689
  )
682
690
  conversation_commands_str = ", ".join([cmd.value for cmd in conversation_commands])
683
691
  async for result in send_event(
@@ -685,7 +693,7 @@ async def chat(
685
693
  ):
686
694
  yield result
687
695
 
688
- mode = await aget_relevant_output_modes(q, meta_log, is_automated_task, uploaded_image_url)
696
+ mode = await aget_relevant_output_modes(q, meta_log, is_automated_task, uploaded_image_url, agent)
689
697
  async for result in send_event(ChatEvent.STATUS, f"**Decided Response Mode:** {mode.value}"):
690
698
  yield result
691
699
  if mode not in conversation_commands:
@@ -734,7 +742,7 @@ async def chat(
734
742
  yield result
735
743
 
736
744
  response = await extract_relevant_summary(
737
- q, contextual_data, subscribed=subscribed, uploaded_image_url=uploaded_image_url
745
+ q, contextual_data, subscribed=subscribed, uploaded_image_url=uploaded_image_url, agent=agent
738
746
  )
739
747
  response_log = str(response)
740
748
  async for result in send_llm_response(response_log):
@@ -816,6 +824,7 @@ async def chat(
816
824
  location,
817
825
  partial(send_event, ChatEvent.STATUS),
818
826
  uploaded_image_url=uploaded_image_url,
827
+ agent=agent,
819
828
  ):
820
829
  if isinstance(result, dict) and ChatEvent.STATUS in result:
821
830
  yield result[ChatEvent.STATUS]
@@ -853,6 +862,7 @@ async def chat(
853
862
  partial(send_event, ChatEvent.STATUS),
854
863
  custom_filters,
855
864
  uploaded_image_url=uploaded_image_url,
865
+ agent=agent,
856
866
  ):
857
867
  if isinstance(result, dict) and ChatEvent.STATUS in result:
858
868
  yield result[ChatEvent.STATUS]
@@ -876,6 +886,7 @@ async def chat(
876
886
  subscribed,
877
887
  partial(send_event, ChatEvent.STATUS),
878
888
  uploaded_image_url=uploaded_image_url,
889
+ agent=agent,
879
890
  ):
880
891
  if isinstance(result, dict) and ChatEvent.STATUS in result:
881
892
  yield result[ChatEvent.STATUS]
@@ -922,6 +933,7 @@ async def chat(
922
933
  subscribed=subscribed,
923
934
  send_status_func=partial(send_event, ChatEvent.STATUS),
924
935
  uploaded_image_url=uploaded_image_url,
936
+ agent=agent,
925
937
  ):
926
938
  if isinstance(result, dict) and ChatEvent.STATUS in result:
927
939
  yield result[ChatEvent.STATUS]
@@ -1132,6 +1144,7 @@ async def get_chat(
1132
1144
  yield result
1133
1145
  return
1134
1146
  conversation_id = conversation.id
1147
+ agent = conversation.agent if conversation.agent else None
1135
1148
 
1136
1149
  await is_ready_to_chat(user)
1137
1150