khoj 1.26.4.dev2__py3-none-any.whl → 1.26.5.dev29__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 (84) hide show
  1. khoj/interface/compiled/404/index.html +1 -1
  2. khoj/interface/compiled/_next/static/chunks/1210.132a7e1910006bbb.js +1 -0
  3. khoj/interface/compiled/_next/static/chunks/1279-f37ee4a388ebf544.js +1 -0
  4. khoj/interface/compiled/_next/static/chunks/1459.690bf20e7d7b7090.js +1 -0
  5. khoj/interface/compiled/_next/static/chunks/1603-b9d95833e0e025e8.js +1 -0
  6. khoj/interface/compiled/_next/static/chunks/1970-1d6d0c1b00b4f343.js +1 -0
  7. khoj/interface/compiled/_next/static/chunks/2697-61fcba89fd87eab4.js +1 -0
  8. khoj/interface/compiled/_next/static/chunks/3423-aad88d6c1f029135.js +1 -0
  9. khoj/interface/compiled/_next/static/chunks/394-6bcb8c429f168f21.js +3 -0
  10. khoj/interface/compiled/_next/static/chunks/4602-8eeb4b76385ad159.js +1 -0
  11. khoj/interface/compiled/_next/static/chunks/5512-94c7c2bbcf58c19d.js +1 -0
  12. khoj/interface/compiled/_next/static/chunks/7113-f2e114d7034a0835.js +1 -0
  13. khoj/interface/compiled/_next/static/chunks/{4086-2c74808ba38a5a0f.js → 8840-b8d7b9f0923c6651.js} +1 -1
  14. khoj/interface/compiled/_next/static/chunks/9417-759984ad62caa3dc.js +1 -0
  15. khoj/interface/compiled/_next/static/chunks/9479-4b443fdcc99141c9.js +1 -0
  16. khoj/interface/compiled/_next/static/chunks/94ca1967.5584df65931cfe83.js +1 -0
  17. khoj/interface/compiled/_next/static/chunks/964ecbae.ea4eab2a3a835ffe.js +1 -0
  18. khoj/interface/compiled/_next/static/chunks/app/agents/page-5ae1e540bb5be8a9.js +1 -0
  19. khoj/interface/compiled/_next/static/chunks/app/automations/{page-5480731341f34450.js → page-774ae3e033f938cd.js} +1 -1
  20. khoj/interface/compiled/_next/static/chunks/app/chat/page-97f5b61aaf46d364.js +1 -0
  21. khoj/interface/compiled/_next/static/chunks/app/factchecker/{page-e7b34316ec6f44de.js → page-d82403db2866bad8.js} +1 -1
  22. khoj/interface/compiled/_next/static/chunks/app/{page-10a5aad6e04f3cf8.js → page-75bbfb564884054b.js} +1 -1
  23. khoj/interface/compiled/_next/static/chunks/app/search/{page-d56541c746fded7d.js → page-9b64f61caa5bd7f9.js} +1 -1
  24. khoj/interface/compiled/_next/static/chunks/app/settings/{page-e044a999468a7c5d.js → page-989cf38b87b19427.js} +1 -1
  25. khoj/interface/compiled/_next/static/chunks/app/share/chat/page-eb9e282691858f2e.js +1 -0
  26. khoj/interface/compiled/_next/static/chunks/webpack-8f4afe09848e24e1.js +1 -0
  27. khoj/interface/compiled/_next/static/css/{c808691c459e3887.css → 3cf13271869a4aeb.css} +1 -1
  28. khoj/interface/compiled/_next/static/css/4cae6c0e5c72fb2d.css +1 -0
  29. khoj/interface/compiled/_next/static/css/76d55eb435962b19.css +25 -0
  30. khoj/interface/compiled/_next/static/css/{3e1f1fdd70775091.css → 80bd6301fc657983.css} +1 -1
  31. khoj/interface/compiled/_next/static/css/ddcc0cf73e062476.css +1 -0
  32. khoj/interface/compiled/agents/index.html +1 -1
  33. khoj/interface/compiled/agents/index.txt +2 -2
  34. khoj/interface/compiled/automations/index.html +1 -1
  35. khoj/interface/compiled/automations/index.txt +2 -2
  36. khoj/interface/compiled/chat/index.html +1 -1
  37. khoj/interface/compiled/chat/index.txt +2 -2
  38. khoj/interface/compiled/factchecker/index.html +1 -1
  39. khoj/interface/compiled/factchecker/index.txt +2 -2
  40. khoj/interface/compiled/index.html +1 -1
  41. khoj/interface/compiled/index.txt +2 -2
  42. khoj/interface/compiled/search/index.html +1 -1
  43. khoj/interface/compiled/search/index.txt +2 -2
  44. khoj/interface/compiled/settings/index.html +1 -1
  45. khoj/interface/compiled/settings/index.txt +2 -2
  46. khoj/interface/compiled/share/chat/index.html +1 -1
  47. khoj/interface/compiled/share/chat/index.txt +2 -2
  48. khoj/processor/conversation/google/gemini_chat.py +28 -13
  49. khoj/processor/conversation/google/utils.py +34 -12
  50. khoj/processor/conversation/openai/gpt.py +4 -4
  51. khoj/processor/conversation/prompts.py +144 -0
  52. khoj/processor/conversation/utils.py +22 -13
  53. khoj/processor/image/generate.py +5 -5
  54. khoj/processor/tools/online_search.py +4 -4
  55. khoj/routers/api.py +4 -2
  56. khoj/routers/api_agents.py +41 -20
  57. khoj/routers/api_chat.py +85 -46
  58. khoj/routers/helpers.py +225 -29
  59. khoj/routers/web_client.py +0 -11
  60. khoj/utils/helpers.py +7 -3
  61. {khoj-1.26.4.dev2.dist-info → khoj-1.26.5.dev29.dist-info}/METADATA +1 -1
  62. {khoj-1.26.4.dev2.dist-info → khoj-1.26.5.dev29.dist-info}/RECORD +67 -62
  63. khoj/interface/compiled/_next/static/chunks/121-7024f479c297aef0.js +0 -1
  64. khoj/interface/compiled/_next/static/chunks/1603-fa3ee48860b9dc5c.js +0 -1
  65. khoj/interface/compiled/_next/static/chunks/2697-a38d01981ad3bdf8.js +0 -1
  66. khoj/interface/compiled/_next/static/chunks/4051-2cf66369d6ca0f1d.js +0 -3
  67. khoj/interface/compiled/_next/static/chunks/477-ec86e93db10571c1.js +0 -1
  68. khoj/interface/compiled/_next/static/chunks/51-e8f5bdb69b5ea421.js +0 -1
  69. khoj/interface/compiled/_next/static/chunks/7762-79f2205740622b5c.js +0 -1
  70. khoj/interface/compiled/_next/static/chunks/9178-899fe9a6b754ecfe.js +0 -1
  71. khoj/interface/compiled/_next/static/chunks/9417-46ed3aaa639c85ef.js +0 -1
  72. khoj/interface/compiled/_next/static/chunks/9479-ea776e73f549090c.js +0 -1
  73. khoj/interface/compiled/_next/static/chunks/app/agents/page-88aa3042711107b7.js +0 -1
  74. khoj/interface/compiled/_next/static/chunks/app/chat/page-702057ccbcf27881.js +0 -1
  75. khoj/interface/compiled/_next/static/chunks/app/share/chat/page-fbbd66a4d4633438.js +0 -1
  76. khoj/interface/compiled/_next/static/chunks/webpack-2651a68f46ac3cb7.js +0 -1
  77. khoj/interface/compiled/_next/static/css/2de69f0be774c768.css +0 -1
  78. khoj/interface/compiled/_next/static/css/592ca99f5122e75a.css +0 -1
  79. khoj/interface/compiled/_next/static/css/b9a6bf04305d98d7.css +0 -25
  80. /khoj/interface/compiled/_next/static/{wyjqS7cuSX-u62BTNYqhU → ZLHCGFLxZSUj0jEJSc99T}/_buildManifest.js +0 -0
  81. /khoj/interface/compiled/_next/static/{wyjqS7cuSX-u62BTNYqhU → ZLHCGFLxZSUj0jEJSc99T}/_ssgManifest.js +0 -0
  82. {khoj-1.26.4.dev2.dist-info → khoj-1.26.5.dev29.dist-info}/WHEEL +0 -0
  83. {khoj-1.26.4.dev2.dist-info → khoj-1.26.5.dev29.dist-info}/entry_points.txt +0 -0
  84. {khoj-1.26.4.dev2.dist-info → khoj-1.26.5.dev29.dist-info}/licenses/LICENSE +0 -0
@@ -176,6 +176,150 @@ Improved Prompt:
176
176
  """.strip()
177
177
  )
178
178
 
179
+ ## Diagram Generation
180
+ ## --
181
+
182
+ improve_diagram_description_prompt = PromptTemplate.from_template(
183
+ """
184
+ you are an architect working with a novice artist using a diagramming tool.
185
+ {personality_context}
186
+
187
+ you need to convert the user's query to a description format that the novice artist can use very well. you are allowed to use primitives like
188
+ - text
189
+ - rectangle
190
+ - diamond
191
+ - ellipse
192
+ - line
193
+ - arrow
194
+ - frame
195
+
196
+ use these primitives to describe what sort of diagram the drawer should create. the artist must recreate the diagram every time, so include all relevant prior information in your description.
197
+
198
+ use simple, concise language.
199
+
200
+ Today's Date: {current_date}
201
+ User's Location: {location}
202
+
203
+ User's Notes:
204
+ {references}
205
+
206
+ Online References:
207
+ {online_results}
208
+
209
+ Conversation Log:
210
+ {chat_history}
211
+
212
+ Query: {query}
213
+
214
+
215
+ """.strip()
216
+ )
217
+
218
+ excalidraw_diagram_generation_prompt = PromptTemplate.from_template(
219
+ """
220
+ You are a program manager with the ability to describe diagrams to compose in professional, fine detail.
221
+ {personality_context}
222
+
223
+ You need to create a declarative description of the diagram and relevant components, using this base schema. Use the `label` property to specify the text to be rendered in the respective elements. Always use light colors for the `backgroundColor` property, like white, or light blue, green, red. "type", "x", "y", "id", are required properties for all elements.
224
+
225
+ {{
226
+ type: string,
227
+ x: number,
228
+ y: number,
229
+ strokeColor: string,
230
+ backgroundColor: string,
231
+ width: number,
232
+ height: number,
233
+ id: string,
234
+ label: {{
235
+ text: string,
236
+ }}
237
+ }}
238
+
239
+ Valid types:
240
+ - text
241
+ - rectangle
242
+ - diamond
243
+ - ellipse
244
+ - line
245
+ - arrow
246
+
247
+ For arrows and lines, you can use the `points` property to specify the start and end points of the arrow. You may also use the `label` property to specify the text to be rendered. You may use the `start` and `end` properties to connect the linear elements to other elements. The start and end point can either be the ID to map to an existing object, or the `type` to create a new object. Mapping to an existing object is useful if you want to connect it to multiple objects. Lines and arrows can only start and end at rectangle, text, diamond, or ellipse elements.
248
+
249
+ {{
250
+ type: "arrow",
251
+ id: string,
252
+ x: number,
253
+ y: number,
254
+ width: number,
255
+ height: number,
256
+ strokeColor: string,
257
+ start: {{
258
+ id: string,
259
+ type: string,
260
+ }},
261
+ end: {{
262
+ id: string,
263
+ type: string,
264
+ }},
265
+ label: {{
266
+ text: string,
267
+ }}
268
+ points: [
269
+ [number, number],
270
+ [number, number],
271
+ ]
272
+ }}
273
+
274
+ For text, you must use the `text` property to specify the text to be rendered. You may also use `fontSize` property to specify the font size of the text. Only use the `text` element for titles, subtitles, and overviews. For labels, use the `label` property in the respective elements.
275
+
276
+ {{
277
+ type: "text",
278
+ id: string,
279
+ x: number,
280
+ y: number,
281
+ fontSize: number,
282
+ text: string,
283
+ }}
284
+
285
+ For frames, use the `children` property to specify the elements that are inside the frame by their ids.
286
+
287
+ {{
288
+ type: "frame",
289
+ id: string,
290
+ x: number,
291
+ y: number,
292
+ width: number,
293
+ height: number,
294
+ name: string,
295
+ children: [
296
+ string
297
+ ]
298
+ }}
299
+
300
+ Here's an example of a valid diagram:
301
+
302
+ Design Description: Create a diagram describing a circular development process with 3 stages: design, implementation and feedback. The design stage is connected to the implementation stage and the implementation stage is connected to the feedback stage and the feedback stage is connected to the design stage. Each stage should be labeled with the stage name.
303
+
304
+ Response:
305
+
306
+ [
307
+ {{"type":"text","x":-150,"y":50,"width":300,"height":40,"id":"title_text","text":"Circular Development Process","fontSize":24}},
308
+ {{"type":"ellipse","x":-169,"y":113,"width":188,"height":202,"id":"design_ellipse", "label": {{"text": "Design"}}}},
309
+ {{"type":"ellipse","x":62,"y":394,"width":186,"height":188,"id":"implement_ellipse", "label": {{"text": "Implement"}}}},
310
+ {{"type":"ellipse","x":-348,"y":430,"width":184,"height":170,"id":"feedback_ellipse", "label": {{"text": "Feedback"}}}},
311
+ {{"type":"arrow","x":21,"y":273,"id":"design_to_implement_arrow","points":[[0,0],[86,105]],"start":{{"id":"design_ellipse"}}, "end":{{"id":"implement_ellipse"}}}},
312
+ {{"type":"arrow","x":50,"y":519,"id":"implement_to_feedback_arrow","points":[[0,0],[-198,-6]],"start":{{"id":"implement_ellipse"}}, "end":{{"id":"feedback_ellipse"}}}},
313
+ {{"type":"arrow","x":-228,"y":417,"id":"feedback_to_design_arrow","points":[[0,0],[85,-123]],"start":{{"id":"feedback_ellipse"}}, "end":{{"id":"design_ellipse"}}}},
314
+ ]
315
+
316
+ Create a detailed diagram from the provided context and user prompt below. Return a valid JSON object:
317
+
318
+ Diagram Description: {query}
319
+
320
+ """.strip()
321
+ )
322
+
179
323
  ## Online Search Conversation
180
324
  ## --
181
325
  online_search_conversation = PromptTemplate.from_template(
@@ -109,7 +109,7 @@ def save_to_conversation_log(
109
109
  client_application: ClientApplication = None,
110
110
  conversation_id: str = None,
111
111
  automation_id: str = None,
112
- uploaded_image_url: str = None,
112
+ query_images: List[str] = None,
113
113
  ):
114
114
  user_message_time = user_message_time or datetime.now().strftime("%Y-%m-%d %H:%M:%S")
115
115
  updated_conversation = message_to_log(
@@ -117,7 +117,7 @@ def save_to_conversation_log(
117
117
  chat_response=chat_response,
118
118
  user_message_metadata={
119
119
  "created": user_message_time,
120
- "uploadedImageData": uploaded_image_url,
120
+ "images": query_images,
121
121
  },
122
122
  khoj_message_metadata={
123
123
  "context": compiled_references,
@@ -145,10 +145,18 @@ Khoj: "{inferred_queries if ("text-to-image" in intent_type) else chat_response}
145
145
  )
146
146
 
147
147
 
148
- # Format user and system messages to chatml format
149
- def construct_structured_message(message, image_url, model_type, vision_enabled):
150
- if image_url and vision_enabled and model_type == ChatModelOptions.ModelType.OPENAI:
151
- return [{"type": "text", "text": message}, {"type": "image_url", "image_url": {"url": image_url}}]
148
+ def construct_structured_message(message: str, images: list[str], model_type: str, vision_enabled: bool):
149
+ """
150
+ Format messages into appropriate multimedia format for supported chat model types
151
+ """
152
+ if not images or not vision_enabled:
153
+ return message
154
+
155
+ if model_type in [ChatModelOptions.ModelType.OPENAI, ChatModelOptions.ModelType.GOOGLE]:
156
+ return [
157
+ {"type": "text", "text": message},
158
+ *[{"type": "image_url", "image_url": {"url": image}} for image in images],
159
+ ]
152
160
  return message
153
161
 
154
162
 
@@ -160,7 +168,7 @@ def generate_chatml_messages_with_context(
160
168
  loaded_model: Optional[Llama] = None,
161
169
  max_prompt_size=None,
162
170
  tokenizer_name=None,
163
- uploaded_image_url=None,
171
+ query_images=None,
164
172
  vision_enabled=False,
165
173
  model_type="",
166
174
  ):
@@ -181,11 +189,12 @@ def generate_chatml_messages_with_context(
181
189
  message_notes = f'\n\n Notes:\n{chat.get("context")}' if chat.get("context") else "\n"
182
190
  role = "user" if chat["by"] == "you" else "assistant"
183
191
 
184
- message_content = chat["message"] + message_notes
192
+ if chat["by"] == "khoj" and "excalidraw" in chat["intent"].get("type"):
193
+ message_content = chat.get("intent").get("inferred-queries")[0] + message_notes
194
+ else:
195
+ message_content = chat["message"] + message_notes
185
196
 
186
- message_content = construct_structured_message(
187
- message_content, chat.get("uploadedImageData"), model_type, vision_enabled
188
- )
197
+ message_content = construct_structured_message(message_content, chat.get("images"), model_type, vision_enabled)
189
198
 
190
199
  reconstructed_message = ChatMessage(content=message_content, role=role)
191
200
 
@@ -198,7 +207,7 @@ def generate_chatml_messages_with_context(
198
207
  if not is_none_or_empty(user_message):
199
208
  messages.append(
200
209
  ChatMessage(
201
- content=construct_structured_message(user_message, uploaded_image_url, model_type, vision_enabled),
210
+ content=construct_structured_message(user_message, query_images, model_type, vision_enabled),
202
211
  role="user",
203
212
  )
204
213
  )
@@ -222,7 +231,6 @@ def truncate_messages(
222
231
  tokenizer_name=None,
223
232
  ) -> list[ChatMessage]:
224
233
  """Truncate messages to fit within max prompt size supported by model"""
225
-
226
234
  default_tokenizer = "gpt-4o"
227
235
 
228
236
  try:
@@ -252,6 +260,7 @@ def truncate_messages(
252
260
  system_message = messages.pop(idx)
253
261
  break
254
262
 
263
+ # TODO: Handle truncation of multi-part message.content, i.e when message.content is a list[dict] rather than a string
255
264
  system_message_tokens = (
256
265
  len(encoder.encode(system_message.content)) if system_message and type(system_message.content) == str else 0
257
266
  )
@@ -26,7 +26,7 @@ async def text_to_image(
26
26
  references: List[Dict[str, Any]],
27
27
  online_results: Dict[str, Any],
28
28
  send_status_func: Optional[Callable] = None,
29
- uploaded_image_url: Optional[str] = None,
29
+ query_images: Optional[List[str]] = None,
30
30
  agent: Agent = None,
31
31
  ):
32
32
  status_code = 200
@@ -65,7 +65,7 @@ async def text_to_image(
65
65
  note_references=references,
66
66
  online_results=online_results,
67
67
  model_type=text_to_image_config.model_type,
68
- uploaded_image_url=uploaded_image_url,
68
+ query_images=query_images,
69
69
  user=user,
70
70
  agent=agent,
71
71
  )
@@ -87,18 +87,18 @@ async def text_to_image(
87
87
  if "content_policy_violation" in e.message:
88
88
  logger.error(f"Image Generation blocked by OpenAI: {e}")
89
89
  status_code = e.status_code # type: ignore
90
- message = f"Image generation blocked by OpenAI: {e.message}" # type: ignore
90
+ message = f"Image generation blocked by OpenAI due to policy violation" # type: ignore
91
91
  yield image_url or image, status_code, message, intent_type.value
92
92
  return
93
93
  else:
94
94
  logger.error(f"Image Generation failed with {e}", exc_info=True)
95
- message = f"Image generation failed with OpenAI error: {e.message}" # type: ignore
95
+ message = f"Image generation failed using OpenAI" # type: ignore
96
96
  status_code = e.status_code # type: ignore
97
97
  yield image_url or image, status_code, message, intent_type.value
98
98
  return
99
99
  except requests.RequestException as e:
100
100
  logger.error(f"Image Generation failed with {e}", exc_info=True)
101
- message = f"Image generation using {text2image_model} via {text_to_image_config.model_type} failed with error: {e}"
101
+ message = f"Image generation using {text2image_model} via {text_to_image_config.model_type} failed due to a network error."
102
102
  status_code = 502
103
103
  yield image_url or image, status_code, message, intent_type.value
104
104
  return
@@ -62,7 +62,7 @@ async def search_online(
62
62
  user: KhojUser,
63
63
  send_status_func: Optional[Callable] = None,
64
64
  custom_filters: List[str] = [],
65
- uploaded_image_url: str = None,
65
+ query_images: List[str] = None,
66
66
  agent: Agent = None,
67
67
  ):
68
68
  query += " ".join(custom_filters)
@@ -73,7 +73,7 @@ async def search_online(
73
73
 
74
74
  # Breakdown the query into subqueries to get the correct answer
75
75
  subqueries = await generate_online_subqueries(
76
- query, conversation_history, location, user, uploaded_image_url=uploaded_image_url, agent=agent
76
+ query, conversation_history, location, user, query_images=query_images, agent=agent
77
77
  )
78
78
  response_dict = {}
79
79
 
@@ -151,7 +151,7 @@ async def read_webpages(
151
151
  location: LocationData,
152
152
  user: KhojUser,
153
153
  send_status_func: Optional[Callable] = None,
154
- uploaded_image_url: str = None,
154
+ query_images: List[str] = None,
155
155
  agent: Agent = None,
156
156
  ):
157
157
  "Infer web pages to read from the query and extract relevant information from them"
@@ -159,7 +159,7 @@ async def read_webpages(
159
159
  if send_status_func:
160
160
  async for event in send_status_func(f"**Inferring web pages to read**"):
161
161
  yield {ChatEvent.STATUS: event}
162
- urls = await infer_webpage_urls(query, conversation_history, location, user, uploaded_image_url)
162
+ urls = await infer_webpage_urls(query, conversation_history, location, user, query_images)
163
163
 
164
164
  logger.info(f"Reading web pages at: {urls}")
165
165
  if send_status_func:
khoj/routers/api.py CHANGED
@@ -347,7 +347,7 @@ async def extract_references_and_questions(
347
347
  conversation_commands: List[ConversationCommand] = [ConversationCommand.Default],
348
348
  location_data: LocationData = None,
349
349
  send_status_func: Optional[Callable] = None,
350
- uploaded_image_url: Optional[str] = None,
350
+ query_images: Optional[List[str]] = None,
351
351
  agent: Agent = None,
352
352
  ):
353
353
  user = request.user.object if request.user.is_authenticated else None
@@ -438,7 +438,7 @@ async def extract_references_and_questions(
438
438
  conversation_log=meta_log,
439
439
  location_data=location_data,
440
440
  user=user,
441
- uploaded_image_url=uploaded_image_url,
441
+ query_images=query_images,
442
442
  vision_enabled=vision_enabled,
443
443
  personality_context=personality_context,
444
444
  )
@@ -459,12 +459,14 @@ async def extract_references_and_questions(
459
459
  chat_model = conversation_config.chat_model
460
460
  inferred_queries = extract_questions_gemini(
461
461
  defiltered_query,
462
+ query_images=query_images,
462
463
  model=chat_model,
463
464
  api_key=api_key,
464
465
  conversation_log=meta_log,
465
466
  location_data=location_data,
466
467
  max_tokens=conversation_config.max_prompt_size,
467
468
  user=user,
469
+ vision_enabled=vision_enabled,
468
470
  personality_context=personality_context,
469
471
  )
470
472
 
@@ -1,5 +1,7 @@
1
1
  import json
2
2
  import logging
3
+ import random
4
+ from datetime import datetime, timedelta, timezone
3
5
  from typing import Dict, List, Optional
4
6
 
5
7
  from asgiref.sync import sync_to_async
@@ -9,8 +11,8 @@ from fastapi.responses import Response
9
11
  from pydantic import BaseModel
10
12
  from starlette.authentication import requires
11
13
 
12
- from khoj.database.adapters import AgentAdapters
13
- from khoj.database.models import Agent, KhojUser
14
+ from khoj.database.adapters import AgentAdapters, ConversationAdapters
15
+ from khoj.database.models import Agent, Conversation, KhojUser
14
16
  from khoj.routers.helpers import CommonQueryParams, acheck_if_safe_prompt
15
17
  from khoj.utils.helpers import (
16
18
  ConversationCommand,
@@ -45,30 +47,49 @@ async def all_agents(
45
47
  ) -> Response:
46
48
  user: KhojUser = request.user.object if request.user.is_authenticated else None
47
49
  agents = await AgentAdapters.aget_all_accessible_agents(user)
50
+ default_agent = await AgentAdapters.aget_default_agent()
51
+ default_agent_packet = None
48
52
  agents_packet = list()
49
53
  for agent in agents:
50
54
  files = agent.fileobject_set.all()
51
55
  file_names = [file.file_name for file in files]
52
- agents_packet.append(
53
- {
54
- "slug": agent.slug,
55
- "name": agent.name,
56
- "persona": agent.personality,
57
- "creator": agent.creator.username if agent.creator else None,
58
- "managed_by_admin": agent.managed_by_admin,
59
- "color": agent.style_color,
60
- "icon": agent.style_icon,
61
- "privacy_level": agent.privacy_level,
62
- "chat_model": agent.chat_model.chat_model,
63
- "files": file_names,
64
- "input_tools": agent.input_tools,
65
- "output_modes": agent.output_modes,
66
- }
56
+ agent_packet = {
57
+ "slug": agent.slug,
58
+ "name": agent.name,
59
+ "persona": agent.personality,
60
+ "creator": agent.creator.username if agent.creator else None,
61
+ "managed_by_admin": agent.managed_by_admin,
62
+ "color": agent.style_color,
63
+ "icon": agent.style_icon,
64
+ "privacy_level": agent.privacy_level,
65
+ "chat_model": agent.chat_model.chat_model,
66
+ "files": file_names,
67
+ "input_tools": agent.input_tools,
68
+ "output_modes": agent.output_modes,
69
+ }
70
+ if agent.slug == default_agent.slug:
71
+ default_agent_packet = agent_packet
72
+ else:
73
+ agents_packet.append(agent_packet)
74
+
75
+ # Load recent conversation sessions
76
+ min_date = datetime.min.replace(tzinfo=timezone.utc)
77
+ two_weeks_ago = datetime.today() - timedelta(weeks=2)
78
+ conversations = []
79
+ if user:
80
+ conversations = await sync_to_async(list[Conversation])(
81
+ ConversationAdapters.get_conversation_sessions(user, request.user.client_app)
82
+ .filter(updated_at__gte=two_weeks_ago)
83
+ .order_by("-updated_at")[:50]
67
84
  )
85
+ conversation_times = {conv.agent.slug: conv.updated_at for conv in conversations if conv.agent}
86
+
87
+ # Put default agent first, then sort by mru and finally shuffle unused randomly
88
+ random.shuffle(agents_packet)
89
+ agents_packet.sort(key=lambda x: conversation_times.get(x["slug"]) or min_date, reverse=True)
90
+ if default_agent_packet:
91
+ agents_packet.insert(0, default_agent_packet)
68
92
 
69
- # Make sure that the agent named 'khoj' is first in the list. Everything else is sorted by name.
70
- agents_packet.sort(key=lambda x: x["name"])
71
- agents_packet.sort(key=lambda x: x["slug"] == "khoj", reverse=True)
72
93
  return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200)
73
94
 
74
95