khoj 1.42.8.dev6__py3-none-any.whl → 1.42.9.dev17__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 (57) hide show
  1. khoj/database/adapters/__init__.py +20 -0
  2. khoj/interface/compiled/404/index.html +2 -2
  3. khoj/interface/compiled/_next/static/chunks/app/agents/{page-9a4610474cd59a71.js → page-5db6ad18da10d353.js} +1 -1
  4. khoj/interface/compiled/_next/static/chunks/app/automations/{page-f7bb9d777b7745d4.js → page-6271e2e31c7571d1.js} +1 -1
  5. khoj/interface/compiled/_next/static/chunks/app/chat/layout-ad68326d2f849cec.js +1 -0
  6. khoj/interface/compiled/_next/static/chunks/app/chat/{page-ef738950ea1babc3.js → page-76fc915800aa90f4.js} +1 -1
  7. khoj/interface/compiled/_next/static/chunks/app/{page-2b3056cba8aa96ce.js → page-a19a597629e87fb8.js} +1 -1
  8. khoj/interface/compiled/_next/static/chunks/app/search/layout-484d34239ed0f2b1.js +1 -0
  9. khoj/interface/compiled/_next/static/chunks/app/search/{page-4885df3cd175c957.js → page-fa366ac14b228688.js} +1 -1
  10. khoj/interface/compiled/_next/static/chunks/app/settings/{page-8be3b35178abf2ec.js → page-8f9a85f96088c18b.js} +1 -1
  11. khoj/interface/compiled/_next/static/chunks/app/share/chat/layout-abb6c5f4239ad7be.js +1 -0
  12. khoj/interface/compiled/_next/static/chunks/app/share/chat/{page-4a4b0c0f4749c2b2.js → page-ed7787cf4938b8e3.js} +1 -1
  13. khoj/interface/compiled/_next/static/chunks/{webpack-15412ee214acd999.js → webpack-92ce8aaf95718ec4.js} +1 -1
  14. khoj/interface/compiled/_next/static/css/{e6da1287d41f5409.css → 02f60900b0d89ec7.css} +1 -1
  15. khoj/interface/compiled/_next/static/css/{821d0d60b0b6871d.css → 93eeacc43e261162.css} +1 -1
  16. khoj/interface/compiled/agents/index.html +2 -2
  17. khoj/interface/compiled/agents/index.txt +2 -2
  18. khoj/interface/compiled/automations/index.html +2 -2
  19. khoj/interface/compiled/automations/index.txt +2 -2
  20. khoj/interface/compiled/chat/index.html +2 -2
  21. khoj/interface/compiled/chat/index.txt +2 -2
  22. khoj/interface/compiled/index.html +2 -2
  23. khoj/interface/compiled/index.txt +2 -2
  24. khoj/interface/compiled/search/index.html +2 -2
  25. khoj/interface/compiled/search/index.txt +2 -2
  26. khoj/interface/compiled/settings/index.html +2 -2
  27. khoj/interface/compiled/settings/index.txt +2 -2
  28. khoj/interface/compiled/share/chat/index.html +2 -2
  29. khoj/interface/compiled/share/chat/index.txt +2 -2
  30. khoj/processor/conversation/anthropic/anthropic_chat.py +11 -2
  31. khoj/processor/conversation/anthropic/utils.py +90 -103
  32. khoj/processor/conversation/google/gemini_chat.py +4 -1
  33. khoj/processor/conversation/google/utils.py +80 -18
  34. khoj/processor/conversation/offline/chat_model.py +3 -3
  35. khoj/processor/conversation/openai/gpt.py +13 -38
  36. khoj/processor/conversation/openai/utils.py +113 -12
  37. khoj/processor/conversation/prompts.py +17 -35
  38. khoj/processor/conversation/utils.py +128 -57
  39. khoj/processor/operator/grounding_agent.py +1 -1
  40. khoj/processor/operator/operator_agent_binary.py +4 -3
  41. khoj/processor/tools/online_search.py +18 -0
  42. khoj/processor/tools/run_code.py +1 -1
  43. khoj/routers/api_chat.py +1 -1
  44. khoj/routers/api_subscription.py +22 -0
  45. khoj/routers/helpers.py +293 -26
  46. khoj/routers/research.py +169 -155
  47. khoj/utils/helpers.py +284 -8
  48. {khoj-1.42.8.dev6.dist-info → khoj-1.42.9.dev17.dist-info}/METADATA +1 -1
  49. {khoj-1.42.8.dev6.dist-info → khoj-1.42.9.dev17.dist-info}/RECORD +54 -54
  50. khoj/interface/compiled/_next/static/chunks/app/chat/layout-ad4d1792ab1a4108.js +0 -1
  51. khoj/interface/compiled/_next/static/chunks/app/search/layout-c02531d586972d7d.js +0 -1
  52. khoj/interface/compiled/_next/static/chunks/app/share/chat/layout-e8e5db7830bf3f47.js +0 -1
  53. /khoj/interface/compiled/_next/static/{cJdFAXV3MR9BSimUwQ40G → rRy7eX2lAtmXdtQuJoVrw}/_buildManifest.js +0 -0
  54. /khoj/interface/compiled/_next/static/{cJdFAXV3MR9BSimUwQ40G → rRy7eX2lAtmXdtQuJoVrw}/_ssgManifest.js +0 -0
  55. {khoj-1.42.8.dev6.dist-info → khoj-1.42.9.dev17.dist-info}/WHEEL +0 -0
  56. {khoj-1.42.8.dev6.dist-info → khoj-1.42.9.dev17.dist-info}/entry_points.txt +0 -0
  57. {khoj-1.42.8.dev6.dist-info → khoj-1.42.9.dev17.dist-info}/licenses/LICENSE +0 -0
khoj/routers/research.py CHANGED
@@ -3,11 +3,9 @@ import logging
3
3
  import os
4
4
  from copy import deepcopy
5
5
  from datetime import datetime
6
- from enum import Enum
7
- from typing import Callable, Dict, List, Optional, Type
6
+ from typing import Callable, Dict, List, Optional
8
7
 
9
8
  import yaml
10
- from pydantic import BaseModel, Field
11
9
 
12
10
  from khoj.database.adapters import AgentAdapters, EntryAdapters
13
11
  from khoj.database.models import Agent, ChatMessageModel, KhojUser
@@ -15,25 +13,31 @@ from khoj.processor.conversation import prompts
15
13
  from khoj.processor.conversation.utils import (
16
14
  OperatorRun,
17
15
  ResearchIteration,
16
+ ToolCall,
18
17
  construct_iteration_history,
19
18
  construct_tool_chat_history,
20
19
  load_complex_json,
21
20
  )
22
21
  from khoj.processor.operator import operate_environment
23
- from khoj.processor.tools.online_search import read_webpages, search_online
22
+ from khoj.processor.tools.online_search import read_webpages_content, search_online
24
23
  from khoj.processor.tools.run_code import run_code
25
24
  from khoj.routers.helpers import (
26
25
  ChatEvent,
27
26
  generate_summary_from_files,
27
+ grep_files,
28
+ list_files,
28
29
  search_documents,
29
30
  send_message_to_model_wrapper,
31
+ view_file_content,
30
32
  )
31
33
  from khoj.utils.helpers import (
32
34
  ConversationCommand,
35
+ ToolDefinition,
36
+ dict_to_tuple,
33
37
  is_none_or_empty,
34
38
  is_operator_enabled,
35
39
  timer,
36
- tool_description_for_research_llm,
40
+ tools_for_research_llm,
37
41
  truncate_code_context,
38
42
  )
39
43
  from khoj.utils.rawconfig import LocationData
@@ -41,47 +45,6 @@ from khoj.utils.rawconfig import LocationData
41
45
  logger = logging.getLogger(__name__)
42
46
 
43
47
 
44
- class PlanningResponse(BaseModel):
45
- """
46
- Schema for the response from planning agent when deciding the next tool to pick.
47
- """
48
-
49
- scratchpad: str = Field(..., description="Scratchpad to reason about which tool to use next")
50
-
51
- class Config:
52
- arbitrary_types_allowed = True
53
-
54
- @classmethod
55
- def create_model_with_enum(cls: Type["PlanningResponse"], tool_options: dict) -> Type["PlanningResponse"]:
56
- """
57
- Factory method that creates a customized PlanningResponse model
58
- with a properly typed tool field based on available tools.
59
-
60
- The tool field is dynamically generated based on available tools.
61
- The query field should be filled by the model after the tool field for a more logical reasoning flow.
62
-
63
- Args:
64
- tool_options: Dictionary mapping tool names to values
65
-
66
- Returns:
67
- A customized PlanningResponse class
68
- """
69
- # Create dynamic enum from tool options
70
- tool_enum = Enum("ToolEnum", tool_options) # type: ignore
71
-
72
- # Create and return a customized response model with the enum
73
- class PlanningResponseWithTool(PlanningResponse):
74
- """
75
- Use the scratchpad to reason about which tool to use next and the query to send to the tool.
76
- Pick tool from provided options and your query to send to the tool.
77
- """
78
-
79
- tool: tool_enum = Field(..., description="Name of the tool to use")
80
- query: str = Field(..., description="Detailed query for the selected tool")
81
-
82
- return PlanningResponseWithTool
83
-
84
-
85
48
  async def apick_next_tool(
86
49
  query: str,
87
50
  conversation_history: List[ChatMessageModel],
@@ -104,12 +67,13 @@ async def apick_next_tool(
104
67
  # Continue with previous iteration if a multi-step tool use is in progress
105
68
  if (
106
69
  previous_iterations
107
- and previous_iterations[-1].tool == ConversationCommand.Operator
70
+ and previous_iterations[-1].query
71
+ and isinstance(previous_iterations[-1].query, ToolCall)
72
+ and previous_iterations[-1].query.name == ConversationCommand.Operator
108
73
  and not previous_iterations[-1].summarizedResult
109
74
  ):
110
75
  previous_iteration = previous_iterations[-1]
111
76
  yield ResearchIteration(
112
- tool=previous_iteration.tool,
113
77
  query=query,
114
78
  context=previous_iteration.context,
115
79
  onlineContext=previous_iteration.onlineContext,
@@ -120,30 +84,40 @@ async def apick_next_tool(
120
84
  return
121
85
 
122
86
  # Construct tool options for the agent to choose from
123
- tool_options = dict()
87
+ tools = []
124
88
  tool_options_str = ""
125
89
  agent_tools = agent.input_tools if agent else []
126
90
  user_has_entries = await EntryAdapters.auser_has_entries(user)
127
- for tool, description in tool_description_for_research_llm.items():
91
+ for tool, tool_data in tools_for_research_llm.items():
128
92
  # Skip showing operator tool as an option if not enabled
129
93
  if tool == ConversationCommand.Operator and not is_operator_enabled():
130
94
  continue
131
- # Skip showing Notes tool as an option if user has no entries
132
- if tool == ConversationCommand.Notes:
133
- if not user_has_entries:
134
- continue
135
- description = description.format(max_search_queries=max_document_searches)
136
- if tool == ConversationCommand.Webpage:
137
- description = description.format(max_webpages_to_read=max_webpages_to_read)
138
- if tool == ConversationCommand.Online:
139
- description = description.format(max_search_queries=max_online_searches)
95
+ # Skip showing document related tools if user has no documents
96
+ if (
97
+ tool == ConversationCommand.SemanticSearchFiles
98
+ or tool == ConversationCommand.RegexSearchFiles
99
+ or tool == ConversationCommand.ViewFile
100
+ or tool == ConversationCommand.ListFiles
101
+ ) and not user_has_entries:
102
+ continue
103
+ if tool == ConversationCommand.SemanticSearchFiles:
104
+ description = tool_data.description.format(max_search_queries=max_document_searches)
105
+ elif tool == ConversationCommand.Webpage:
106
+ description = tool_data.description.format(max_webpages_to_read=max_webpages_to_read)
107
+ elif tool == ConversationCommand.Online:
108
+ description = tool_data.description.format(max_search_queries=max_online_searches)
109
+ else:
110
+ description = tool_data.description
140
111
  # Add tool if agent does not have any tools defined or the tool is supported by the agent.
141
112
  if len(agent_tools) == 0 or tool.value in agent_tools:
142
- tool_options[tool.name] = tool.value
143
113
  tool_options_str += f'- "{tool.value}": "{description}"\n'
144
-
145
- # Create planning reponse model with dynamically populated tool enum class
146
- planning_response_model = PlanningResponse.create_model_with_enum(tool_options)
114
+ tools.append(
115
+ ToolDefinition(
116
+ name=tool.value,
117
+ description=description,
118
+ schema=tool_data.schema,
119
+ )
120
+ )
147
121
 
148
122
  today = datetime.today()
149
123
  location_data = f"{location}" if location else "Unknown"
@@ -162,24 +136,17 @@ async def apick_next_tool(
162
136
  max_iterations=max_iterations,
163
137
  )
164
138
 
165
- if query_images:
166
- query = f"[placeholder for user attached images]\n{query}"
167
-
168
139
  # Construct chat history with user and iteration history with researcher agent for context
169
- iteration_chat_history = construct_iteration_history(previous_iterations, prompts.previous_iteration, query)
140
+ iteration_chat_history = construct_iteration_history(previous_iterations, query, query_images, query_files)
170
141
  chat_and_research_history = conversation_history + iteration_chat_history
171
142
 
172
- # Plan function execution for the next tool
173
- query = prompts.plan_function_execution_next_tool.format(query=query) if iteration_chat_history else query
174
-
175
143
  try:
176
144
  with timer("Chat actor: Infer information sources to refer", logger):
177
145
  response = await send_message_to_model_wrapper(
178
- query=query,
146
+ query="",
179
147
  system_message=function_planning_prompt,
180
148
  chat_history=chat_and_research_history,
181
- response_type="json_object",
182
- response_schema=planning_response_model,
149
+ tools=tools,
183
150
  deepthought=True,
184
151
  user=user,
185
152
  query_images=query_images,
@@ -190,48 +157,38 @@ async def apick_next_tool(
190
157
  except Exception as e:
191
158
  logger.error(f"Failed to infer information sources to refer: {e}", exc_info=True)
192
159
  yield ResearchIteration(
193
- tool=None,
194
160
  query=None,
195
161
  warning="Failed to infer information sources to refer. Skipping iteration. Try again.",
196
162
  )
197
163
  return
198
164
 
199
165
  try:
200
- response = load_complex_json(response)
201
- if not isinstance(response, dict):
202
- raise ValueError(f"Expected dict response, got {type(response).__name__}: {response}")
203
- selected_tool = response.get("tool", None)
204
- generated_query = response.get("query", None)
205
- scratchpad = response.get("scratchpad", None)
206
- warning = None
207
- logger.info(f"Response for determining relevant tools: {response}")
208
-
209
- # Detect selection of previously used query, tool combination.
210
- previous_tool_query_combinations = {(i.tool, i.query) for i in previous_iterations if i.warning is None}
211
- if (selected_tool, generated_query) in previous_tool_query_combinations:
212
- warning = f"Repeated tool, query combination detected. Skipping iteration. Try something different."
213
- # Only send client status updates if we'll execute this iteration
214
- elif send_status_func:
215
- determined_tool_message = "**Determined Tool**: "
216
- determined_tool_message += (
217
- f"{selected_tool}({generated_query})." if selected_tool != ConversationCommand.Text else "respond."
218
- )
219
- determined_tool_message += f"\nReason: {scratchpad}" if scratchpad else ""
220
- async for event in send_status_func(f"{scratchpad}"):
221
- yield {ChatEvent.STATUS: event}
222
-
223
- yield ResearchIteration(
224
- tool=selected_tool,
225
- query=generated_query,
226
- warning=warning,
227
- )
166
+ # Try parse the response as function call response to infer next tool to use.
167
+ # TODO: Handle multiple tool calls.
168
+ response_text = response.text
169
+ parsed_response = [ToolCall(**item) for item in load_complex_json(response_text)][0]
228
170
  except Exception as e:
229
- logger.error(f"Invalid response for determining relevant tools: {response}. {e}", exc_info=True)
230
- yield ResearchIteration(
231
- tool=None,
232
- query=None,
233
- warning=f"Invalid response for determining relevant tools: {response}. Skipping iteration. Fix error: {e}",
234
- )
171
+ # Otherwise assume the model has decided to end the research run and respond to the user.
172
+ parsed_response = ToolCall(name=ConversationCommand.Text, args={"response": response_text}, id=None)
173
+
174
+ # If we have a valid response, extract the tool and query.
175
+ warning = None
176
+ logger.info(f"Response for determining relevant tools: {parsed_response.name}({parsed_response.args})")
177
+
178
+ # Detect selection of previously used query, tool combination.
179
+ previous_tool_query_combinations = {
180
+ (i.query.name, dict_to_tuple(i.query.args))
181
+ for i in previous_iterations
182
+ if i.warning is None and isinstance(i.query, ToolCall)
183
+ }
184
+ if (parsed_response.name, dict_to_tuple(parsed_response.args)) in previous_tool_query_combinations:
185
+ warning = f"Repeated tool, query combination detected. Skipping iteration. Try something different."
186
+ # Only send client status updates if we'll execute this iteration and model has thoughts to share.
187
+ elif send_status_func and not is_none_or_empty(response.thought):
188
+ async for event in send_status_func(response.thought):
189
+ yield {ChatEvent.STATUS: event}
190
+
191
+ yield ResearchIteration(query=parsed_response, warning=warning, raw_response=response.raw_content)
235
192
 
236
193
 
237
194
  async def research(
@@ -257,10 +214,10 @@ async def research(
257
214
  MAX_ITERATIONS = int(os.getenv("KHOJ_RESEARCH_ITERATIONS", 5))
258
215
 
259
216
  # Incorporate previous partial research into current research chat history
260
- research_conversation_history = deepcopy(conversation_history)
217
+ research_conversation_history = [chat for chat in deepcopy(conversation_history) if chat.message]
261
218
  if current_iteration := len(previous_iterations) > 0:
262
219
  logger.info(f"Continuing research with the previous {len(previous_iterations)} iteration results.")
263
- previous_iterations_history = construct_iteration_history(previous_iterations, prompts.previous_iteration)
220
+ previous_iterations_history = construct_iteration_history(previous_iterations)
264
221
  research_conversation_history += previous_iterations_history
265
222
 
266
223
  while current_iteration < MAX_ITERATIONS:
@@ -273,7 +230,7 @@ async def research(
273
230
  code_results: Dict = dict()
274
231
  document_results: List[Dict[str, str]] = []
275
232
  operator_results: OperatorRun = None
276
- this_iteration = ResearchIteration(tool=None, query=query)
233
+ this_iteration = ResearchIteration(query=query)
277
234
 
278
235
  async for result in apick_next_tool(
279
236
  query,
@@ -303,26 +260,30 @@ async def research(
303
260
  logger.warning(f"Research mode: {this_iteration.warning}.")
304
261
 
305
262
  # Terminate research if selected text tool or query, tool not set for next iteration
306
- elif not this_iteration.query or not this_iteration.tool or this_iteration.tool == ConversationCommand.Text:
263
+ elif (
264
+ not this_iteration.query
265
+ or isinstance(this_iteration.query, str)
266
+ or this_iteration.query.name == ConversationCommand.Text
267
+ ):
307
268
  current_iteration = MAX_ITERATIONS
308
269
 
309
- elif this_iteration.tool == ConversationCommand.Notes:
270
+ elif this_iteration.query.name == ConversationCommand.SemanticSearchFiles:
310
271
  this_iteration.context = []
311
272
  document_results = []
312
273
  previous_inferred_queries = {
313
274
  c["query"] for iteration in previous_iterations if iteration.context for c in iteration.context
314
275
  }
315
276
  async for result in search_documents(
316
- this_iteration.query,
317
- max_document_searches,
318
- None,
319
- user,
320
- construct_tool_chat_history(previous_iterations, ConversationCommand.Notes),
321
- conversation_id,
322
- [ConversationCommand.Default],
323
- location,
324
- send_status_func,
325
- query_images,
277
+ **this_iteration.query.args,
278
+ n=max_document_searches,
279
+ d=None,
280
+ user=user,
281
+ chat_history=construct_tool_chat_history(previous_iterations, ConversationCommand.SemanticSearchFiles),
282
+ conversation_id=conversation_id,
283
+ conversation_commands=[ConversationCommand.Default],
284
+ location_data=location,
285
+ send_status_func=send_status_func,
286
+ query_images=query_images,
326
287
  previous_inferred_queries=previous_inferred_queries,
327
288
  agent=agent,
328
289
  tracer=tracer,
@@ -350,7 +311,7 @@ async def research(
350
311
  else:
351
312
  this_iteration.warning = "No matching document references found"
352
313
 
353
- elif this_iteration.tool == ConversationCommand.Online:
314
+ elif this_iteration.query.name == ConversationCommand.SearchWeb:
354
315
  previous_subqueries = {
355
316
  subquery
356
317
  for iteration in previous_iterations
@@ -359,12 +320,12 @@ async def research(
359
320
  }
360
321
  try:
361
322
  async for result in search_online(
362
- this_iteration.query,
363
- construct_tool_chat_history(previous_iterations, ConversationCommand.Online),
364
- location,
365
- user,
366
- send_status_func,
367
- [],
323
+ **this_iteration.query.args,
324
+ conversation_history=construct_tool_chat_history(previous_iterations, ConversationCommand.Online),
325
+ location=location,
326
+ user=user,
327
+ send_status_func=send_status_func,
328
+ custom_filters=[],
368
329
  max_online_searches=max_online_searches,
369
330
  max_webpages_to_read=0,
370
331
  query_images=query_images,
@@ -383,19 +344,15 @@ async def research(
383
344
  this_iteration.warning = f"Error searching online: {e}"
384
345
  logger.error(this_iteration.warning, exc_info=True)
385
346
 
386
- elif this_iteration.tool == ConversationCommand.Webpage:
347
+ elif this_iteration.query.name == ConversationCommand.ReadWebpage:
387
348
  try:
388
- async for result in read_webpages(
389
- this_iteration.query,
390
- construct_tool_chat_history(previous_iterations, ConversationCommand.Webpage),
391
- location,
392
- user,
393
- send_status_func,
394
- max_webpages_to_read=max_webpages_to_read,
395
- query_images=query_images,
349
+ async for result in read_webpages_content(
350
+ **this_iteration.query.args,
351
+ user=user,
352
+ send_status_func=send_status_func,
353
+ # max_webpages_to_read=max_webpages_to_read,
396
354
  agent=agent,
397
355
  tracer=tracer,
398
- query_files=query_files,
399
356
  ):
400
357
  if isinstance(result, dict) and ChatEvent.STATUS in result:
401
358
  yield result[ChatEvent.STATUS]
@@ -416,15 +373,15 @@ async def research(
416
373
  this_iteration.warning = f"Error reading webpages: {e}"
417
374
  logger.error(this_iteration.warning, exc_info=True)
418
375
 
419
- elif this_iteration.tool == ConversationCommand.Code:
376
+ elif this_iteration.query.name == ConversationCommand.RunCode:
420
377
  try:
421
378
  async for result in run_code(
422
- this_iteration.query,
423
- construct_tool_chat_history(previous_iterations, ConversationCommand.Code),
424
- "",
425
- location,
426
- user,
427
- send_status_func,
379
+ **this_iteration.query.args,
380
+ conversation_history=construct_tool_chat_history(previous_iterations, ConversationCommand.Code),
381
+ context="",
382
+ location_data=location,
383
+ user=user,
384
+ send_status_func=send_status_func,
428
385
  query_images=query_images,
429
386
  agent=agent,
430
387
  query_files=query_files,
@@ -441,14 +398,14 @@ async def research(
441
398
  this_iteration.warning = f"Error running code: {e}"
442
399
  logger.warning(this_iteration.warning, exc_info=True)
443
400
 
444
- elif this_iteration.tool == ConversationCommand.Operator:
401
+ elif this_iteration.query.name == ConversationCommand.OperateComputer:
445
402
  try:
446
403
  async for result in operate_environment(
447
- this_iteration.query,
448
- user,
449
- construct_tool_chat_history(previous_iterations, ConversationCommand.Operator),
450
- location,
451
- previous_iterations[-1].operatorContext if previous_iterations else None,
404
+ **this_iteration.query.args,
405
+ user=user,
406
+ conversation_log=construct_tool_chat_history(previous_iterations, ConversationCommand.Operator),
407
+ location_data=location,
408
+ previous_trajectory=previous_iterations[-1].operatorContext if previous_iterations else None,
452
409
  send_status_func=send_status_func,
453
410
  query_images=query_images,
454
411
  agent=agent,
@@ -474,6 +431,63 @@ async def research(
474
431
  this_iteration.warning = f"Error operating browser: {e}"
475
432
  logger.error(this_iteration.warning, exc_info=True)
476
433
 
434
+ elif this_iteration.query.name == ConversationCommand.ViewFile:
435
+ try:
436
+ async for result in view_file_content(
437
+ **this_iteration.query.args,
438
+ user=user,
439
+ ):
440
+ if isinstance(result, dict) and ChatEvent.STATUS in result:
441
+ yield result[ChatEvent.STATUS]
442
+ else:
443
+ if this_iteration.context is None:
444
+ this_iteration.context = []
445
+ document_results: List[Dict[str, str]] = result # type: ignore
446
+ this_iteration.context += document_results
447
+ async for result in send_status_func(f"**Viewed file**: {this_iteration.query.args['path']}"):
448
+ yield result
449
+ except Exception as e:
450
+ this_iteration.warning = f"Error viewing file: {e}"
451
+ logger.error(this_iteration.warning, exc_info=True)
452
+
453
+ elif this_iteration.query.name == ConversationCommand.ListFiles:
454
+ try:
455
+ async for result in list_files(
456
+ **this_iteration.query.args,
457
+ user=user,
458
+ ):
459
+ if isinstance(result, dict) and ChatEvent.STATUS in result:
460
+ yield result[ChatEvent.STATUS]
461
+ else:
462
+ if this_iteration.context is None:
463
+ this_iteration.context = []
464
+ document_results: List[Dict[str, str]] = [result] # type: ignore
465
+ this_iteration.context += document_results
466
+ async for result in send_status_func(result["query"]):
467
+ yield result
468
+ except Exception as e:
469
+ this_iteration.warning = f"Error listing files: {e}"
470
+ logger.error(this_iteration.warning, exc_info=True)
471
+
472
+ elif this_iteration.query.name == ConversationCommand.RegexSearchFiles:
473
+ try:
474
+ async for result in grep_files(
475
+ **this_iteration.query.args,
476
+ user=user,
477
+ ):
478
+ if isinstance(result, dict) and ChatEvent.STATUS in result:
479
+ yield result[ChatEvent.STATUS]
480
+ else:
481
+ if this_iteration.context is None:
482
+ this_iteration.context = []
483
+ document_results: List[Dict[str, str]] = [result] # type: ignore
484
+ this_iteration.context += document_results
485
+ async for result in send_status_func(result["query"]):
486
+ yield result
487
+ except Exception as e:
488
+ this_iteration.warning = f"Error searching with regex: {e}"
489
+ logger.error(this_iteration.warning, exc_info=True)
490
+
477
491
  else:
478
492
  # No valid tools. This is our exit condition.
479
493
  current_iteration = MAX_ITERATIONS
@@ -481,7 +495,7 @@ async def research(
481
495
  current_iteration += 1
482
496
 
483
497
  if document_results or online_results or code_results or operator_results or this_iteration.warning:
484
- results_data = f"\n<iteration>{current_iteration}\n<tool>{this_iteration.tool}</tool>\n<query>{this_iteration.query}</query>\n<results>"
498
+ results_data = f"\n<iteration_{current_iteration}_results>"
485
499
  if document_results:
486
500
  results_data += f"\n<document_references>\n{yaml.dump(document_results, allow_unicode=True, sort_keys=False, default_flow_style=False)}\n</document_references>"
487
501
  if online_results:
@@ -494,7 +508,7 @@ async def research(
494
508
  )
495
509
  if this_iteration.warning:
496
510
  results_data += f"\n<warning>\n{this_iteration.warning}\n</warning>"
497
- results_data += "\n</results>\n</iteration>"
511
+ results_data += f"\n</results>\n</iteration_{current_iteration}_results>"
498
512
 
499
513
  # intermediate_result = await extract_relevant_info(this_iteration.query, results_data, agent)
500
514
  this_iteration.summarizedResult = results_data