ara-cli 0.1.10.5__py3-none-any.whl → 0.1.13.3__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 (106) hide show
  1. ara_cli/__init__.py +51 -6
  2. ara_cli/__main__.py +87 -75
  3. ara_cli/ara_command_action.py +95 -57
  4. ara_cli/ara_config.py +187 -128
  5. ara_cli/ara_subcommands/common.py +2 -2
  6. ara_cli/ara_subcommands/config.py +221 -0
  7. ara_cli/ara_subcommands/convert.py +43 -0
  8. ara_cli/ara_subcommands/fetch.py +41 -0
  9. ara_cli/ara_subcommands/fetch_agents.py +22 -0
  10. ara_cli/ara_subcommands/fetch_scripts.py +19 -0
  11. ara_cli/ara_subcommands/fetch_templates.py +15 -10
  12. ara_cli/ara_subcommands/list.py +97 -23
  13. ara_cli/artefact_autofix.py +115 -62
  14. ara_cli/artefact_converter.py +256 -0
  15. ara_cli/chat.py +283 -62
  16. ara_cli/chat_agent/__init__.py +0 -0
  17. ara_cli/chat_agent/agent_process_manager.py +155 -0
  18. ara_cli/chat_script_runner/__init__.py +0 -0
  19. ara_cli/chat_script_runner/script_completer.py +23 -0
  20. ara_cli/chat_script_runner/script_finder.py +41 -0
  21. ara_cli/chat_script_runner/script_lister.py +36 -0
  22. ara_cli/chat_script_runner/script_runner.py +36 -0
  23. ara_cli/chat_web_search/__init__.py +0 -0
  24. ara_cli/chat_web_search/web_search.py +263 -0
  25. ara_cli/commands/agent_run_command.py +98 -0
  26. ara_cli/commands/fetch_agents_command.py +106 -0
  27. ara_cli/commands/fetch_scripts_command.py +43 -0
  28. ara_cli/commands/fetch_templates_command.py +39 -0
  29. ara_cli/commands/fetch_templates_commands.py +39 -0
  30. ara_cli/commands/list_agents_command.py +39 -0
  31. ara_cli/completers.py +71 -35
  32. ara_cli/constants.py +2 -0
  33. ara_cli/directory_navigator.py +37 -4
  34. ara_cli/llm_utils.py +58 -0
  35. ara_cli/prompt_chat.py +20 -4
  36. ara_cli/prompt_extractor.py +47 -32
  37. ara_cli/template_loader.py +2 -1
  38. ara_cli/template_manager.py +52 -21
  39. ara_cli/templates/global-scripts/hello_global.py +1 -0
  40. ara_cli/templates/prompt-modules/commands/add_scenarios_for_new_behaviour.feature_creation_agent.commands.md +1 -0
  41. ara_cli/templates/prompt-modules/commands/align_feature_with_implementation_changes.interview_agent.commands.md +1 -0
  42. ara_cli/templates/prompt-modules/commands/analyze_codebase_and_plan_tasks.interview_agent.commands.md +1 -0
  43. ara_cli/templates/prompt-modules/commands/choose_best_parent_artefact.interview_agent.commands.md +1 -0
  44. ara_cli/templates/prompt-modules/commands/create_tasks_from_artefact_content.interview_agent.commands.md +1 -0
  45. ara_cli/templates/prompt-modules/commands/create_tests_for_uncovered_modules.test_generation_agent.commands.md +1 -0
  46. ara_cli/templates/prompt-modules/commands/derive_features_from_video_description.feature_creation_agent.commands.md +1 -0
  47. ara_cli/templates/prompt-modules/commands/describe_agent_capabilities.agent.commands.md +1 -0
  48. ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
  49. ara_cli/templates/prompt-modules/commands/execute_scoped_todos_in_task.interview_agent.commands.md +1 -0
  50. ara_cli/templates/prompt-modules/commands/explain_single_file_purpose.interview_agent.commands.md +1 -0
  51. ara_cli/templates/prompt-modules/commands/extract_file_information_bullets.interview_agent.commands.md +1 -0
  52. ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
  53. ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
  54. ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
  55. ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
  56. ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
  57. ara_cli/templates/prompt-modules/commands/fix_failing_behave_step_definitions.interview_agent.commands.md +1 -0
  58. ara_cli/templates/prompt-modules/commands/fix_failing_pytest_tests.interview_agent.commands.md +1 -0
  59. ara_cli/templates/prompt-modules/commands/general_instruction_policy.commands.md +47 -0
  60. ara_cli/templates/prompt-modules/commands/generate_and_fix_pytest_tests.test_generation_agent.commands.md +1 -0
  61. ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
  62. ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
  63. ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
  64. ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
  65. ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
  66. ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
  67. ara_cli/templates/prompt-modules/commands/suggest_next_story_child_tasks.interview_agent.commands.md +1 -0
  68. ara_cli/templates/prompt-modules/commands/summarize_or_transcribe_media.interview_agent.commands.md +1 -0
  69. ara_cli/templates/prompt-modules/commands/update_feature_to_match_implementation.feature_creation_agent.commands.md +1 -0
  70. ara_cli/templates/prompt-modules/commands/update_user_story_with_requirements.interview_agent.commands.md +1 -0
  71. ara_cli/version.py +1 -1
  72. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.13.3.dist-info}/METADATA +33 -1
  73. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.13.3.dist-info}/RECORD +89 -43
  74. tests/test_ara_command_action.py +31 -19
  75. tests/test_ara_config.py +177 -90
  76. tests/test_artefact_autofix.py +170 -97
  77. tests/test_artefact_autofix_integration.py +495 -0
  78. tests/test_artefact_converter.py +357 -0
  79. tests/test_artefact_extraction.py +564 -0
  80. tests/test_chat.py +162 -126
  81. tests/test_chat_givens_images.py +603 -0
  82. tests/test_chat_script_runner.py +454 -0
  83. tests/test_llm_utils.py +164 -0
  84. tests/test_prompt_chat.py +343 -0
  85. tests/test_prompt_extractor.py +683 -0
  86. tests/test_web_search.py +467 -0
  87. ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
  88. ara_cli/templates/prompt-modules/blueprints/pytest_unittest_prompt.blueprint.md +0 -32
  89. ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
  90. ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
  91. ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
  92. ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
  93. ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
  94. ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
  95. ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
  96. ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
  97. ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
  98. ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
  99. ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
  100. ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
  101. ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
  102. ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
  103. ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
  104. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.13.3.dist-info}/WHEEL +0 -0
  105. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.13.3.dist-info}/entry_points.txt +0 -0
  106. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.13.3.dist-info}/top_level.txt +0 -0
ara_cli/chat.py CHANGED
@@ -1,16 +1,35 @@
1
1
  import os
2
- import errno
2
+ import sys
3
3
  import argparse
4
4
  import cmd2
5
5
 
6
6
  from ara_cli.prompt_handler import send_prompt
7
7
 
8
+ from . import (
9
+ CATEGORY_CHAT_CONTROL,
10
+ CATEGORY_LLM_CONTROL,
11
+ CATEGORY_SCRIPT_CONTROL,
12
+ CATEGORY_AGENT_CONTROL,
13
+ )
14
+ from . import ROLE_PROMPT, ROLE_RESPONSE, INTRO
15
+ from . import BINARY_TYPE_MAPPING, DOCUMENT_TYPE_EXTENSIONS
16
+
8
17
  from . import error_handler
9
18
  from ara_cli.error_handler import AraError, AraConfigurationError
10
19
 
11
20
  from ara_cli.file_loaders.document_file_loader import DocumentFileLoader
12
21
  from ara_cli.file_loaders.binary_file_loader import BinaryFileLoader
13
22
  from ara_cli.file_loaders.text_file_loader import TextFileLoader
23
+ from ara_cli.chat_agent.agent_process_manager import AgentProcessManager
24
+
25
+ from ara_cli.chat_script_runner.script_runner import ScriptRunner
26
+ from ara_cli.chat_script_runner.script_completer import ScriptCompleter
27
+ from ara_cli.chat_script_runner.script_lister import ScriptLister
28
+ from ara_cli.chat_web_search.web_search import (
29
+ perform_web_search_completion,
30
+ is_web_search_supported,
31
+ get_supported_models_message,
32
+ )
14
33
 
15
34
 
16
35
  extract_parser = argparse.ArgumentParser()
@@ -34,39 +53,6 @@ load_parser.add_argument(
34
53
 
35
54
 
36
55
  class Chat(cmd2.Cmd):
37
- CATEGORY_CHAT_CONTROL = "Chat control commands"
38
- CATEGORY_LLM_CONTROL = "Language model controls"
39
-
40
- INTRO = """/***************************************/
41
- araarar
42
- aa ara
43
- aa aa aara
44
- a araarar
45
- a ar ar
46
- aa ara
47
- a a
48
- a aa
49
- a a
50
- ar aa aa
51
- (c) ara chat by talsen team
52
- aa aa
53
- aa a
54
- a aa
55
- aa
56
- /***************************************/
57
- Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat mode):"""
58
-
59
- ROLE_PROMPT = "ara prompt"
60
- ROLE_RESPONSE = "ara response"
61
-
62
- BINARY_TYPE_MAPPING = {
63
- ".png": "image/png",
64
- ".jpg": "image/jpeg",
65
- ".jpeg": "image/jpeg",
66
- }
67
-
68
- DOCUMENT_TYPE_EXTENSIONS = [".docx", ".doc", ".odt", ".pdf"]
69
-
70
56
  def __init__(
71
57
  self,
72
58
  chat_name: str,
@@ -74,6 +60,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
74
60
  enable_commands: list[str] | None = None,
75
61
  ):
76
62
  from ara_cli.template_loader import TemplateLoader
63
+
77
64
  shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
78
65
  if enable_commands:
79
66
  enable_commands.append("quit") # always allow quitting
@@ -97,15 +84,21 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
97
84
  self.disable_commands(commands_to_disable)
98
85
 
99
86
  self.prompt = "ara> "
100
- self.intro = Chat.INTRO
87
+ self.intro = INTRO
101
88
 
102
- self.default_chat_content = f"# {Chat.ROLE_PROMPT}:\n"
89
+ self.default_chat_content = f"# {ROLE_PROMPT}:\n"
103
90
  self.chat_name = self.setup_chat(chat_name, reset)
104
91
  self.chat_name = os.path.abspath(self.chat_name)
105
92
  self.chat_history = []
106
93
  self.message_buffer = []
107
94
  self.config = self._retrieve_ara_config()
108
95
  self.template_loader = TemplateLoader(chat_instance=self)
96
+ self.script_runner = ScriptRunner(chat_instance=self)
97
+ self.script_lister = ScriptLister()
98
+ self.script_completer = ScriptCompleter()
99
+
100
+ # Initialize agent process manager
101
+ self.agent_manager = AgentProcessManager(self)
109
102
 
110
103
  def disable_commands(self, commands: list[str]):
111
104
  for command in commands:
@@ -127,6 +120,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
127
120
  self.aliases["h"] = "help"
128
121
  self.aliases["n"] = "NEW"
129
122
  self.aliases["e"] = "EXTRACT"
123
+ self.aliases["SEARCH"] = "search"
130
124
  self.aliases["l"] = "LOAD"
131
125
  self.aliases["lr"] = "LOAD_RULES"
132
126
  self.aliases["li"] = "LOAD_INTENTION"
@@ -134,6 +128,11 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
134
128
  self.aliases["lg"] = "LOAD_GIVENS"
135
129
  self.aliases["lb"] = "LOAD_BLUEPRINT"
136
130
  self.aliases["lt"] = "LOAD_TEMPLATE"
131
+ self.aliases["rpy"] = "run_pyscript"
132
+ self.aliases["a"] = "AGENT_RUN"
133
+ self.aliases["al"] = "LIST_AGENTS"
134
+ self.aliases["la"] = "LIST_AGENTS"
135
+ self.aliases["AGENT_LIST"] = "LIST_AGENTS"
137
136
 
138
137
  def setup_chat(self, chat_name, reset: bool = None):
139
138
  if os.path.exists(chat_name):
@@ -148,9 +147,12 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
148
147
  chat_file_short = os.path.split(chat_file)[-1]
149
148
 
150
149
  if reset is None:
151
- user_input = input(
152
- f"{chat_file_short} already exists. Do you want to reset the chat? (y/N): "
150
+ print(
151
+ f"{chat_file_short} already exists. Do you want to reset the chat? (y/N): ",
152
+ end="",
153
+ flush=True,
153
154
  )
155
+ user_input = sys.stdin.readline().strip()
154
156
  if user_input.lower() == "y":
155
157
  self.create_empty_chat_file(chat_file)
156
158
  if reset:
@@ -178,13 +180,14 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
178
180
  print(f"File {file_name} not found.")
179
181
  return False
180
182
  return method(self, file_path, *args, **kwargs)
183
+
181
184
  return wrapper
182
185
 
183
186
  @staticmethod
184
187
  def get_last_role_marker(lines):
185
188
  if not lines:
186
189
  return
187
- role_markers = [f"# {Chat.ROLE_PROMPT}:", f"# {Chat.ROLE_RESPONSE}"]
190
+ role_markers = [f"# {ROLE_PROMPT}:", f"# {ROLE_RESPONSE}"]
188
191
  for line in reversed(lines):
189
192
  stripped_line = line.strip()
190
193
  if stripped_line.startswith(tuple(role_markers)):
@@ -246,8 +249,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
246
249
  import re
247
250
  from ara_cli.prompt_handler import prepend_system_prompt
248
251
 
249
- prompt_marker = f"# {self.ROLE_PROMPT}:"
250
- response_marker = f"# {self.ROLE_RESPONSE}:"
252
+ prompt_marker = f"# {ROLE_PROMPT}:"
253
+ response_marker = f"# {ROLE_RESPONSE}:"
251
254
 
252
255
  split_pattern = re.compile(f"({prompt_marker}|{response_marker})")
253
256
 
@@ -283,7 +286,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
283
286
  def send_message(self):
284
287
  self.chat_history = self.load_chat_history(self.chat_name)
285
288
  prompt_to_send = self.assemble_prompt()
286
- role_marker = f"# {Chat.ROLE_RESPONSE}:"
289
+ role_marker = f"# {ROLE_RESPONSE}:"
287
290
 
288
291
  with open(self.chat_name, "a+", encoding="utf-8") as file:
289
292
  last_line = self.get_last_line(file)
@@ -332,9 +335,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
332
335
  def find_last_reply_index(self, lines: list[str]):
333
336
  index_to_remove = None
334
337
  for i, line in enumerate(reversed(lines)):
335
- if line.strip().startswith(f"# {Chat.ROLE_PROMPT}"):
338
+ if line.strip().startswith(f"# {ROLE_PROMPT}"):
336
339
  break
337
- if line.strip().startswith(f"# {Chat.ROLE_RESPONSE}"):
340
+ if line.strip().startswith(f"# {ROLE_RESPONSE}"):
338
341
  index_to_remove = len(lines) - i - 1
339
342
  break
340
343
  return index_to_remove
@@ -359,7 +362,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
359
362
  def add_prompt_tag_if_needed(self, chat_file: str):
360
363
  with open(chat_file, "r", encoding="utf-8") as file:
361
364
  lines = file.readlines()
362
- prompt_tag = f"# {Chat.ROLE_PROMPT}:"
365
+
366
+ prompt_tag = f"# {ROLE_PROMPT}:"
363
367
  if Chat.get_last_role_marker(lines) == prompt_tag:
364
368
  return
365
369
  append = prompt_tag
@@ -369,7 +373,6 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
369
373
  with open(chat_file, "a", encoding="utf-8") as file:
370
374
  file.write(append)
371
375
 
372
- # @file_exists_check
373
376
  def load_text_file(
374
377
  self,
375
378
  file_path,
@@ -387,7 +390,6 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
387
390
  extract_images=extract_images,
388
391
  )
389
392
 
390
- # @file_exists_check
391
393
  def load_binary_file(
392
394
  self, file_path, mime_type: str, prefix: str = "", suffix: str = ""
393
395
  ):
@@ -401,7 +403,6 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
401
403
  reader = MarkdownReader(file_path)
402
404
  return reader.read(extract_images=extract_images)
403
405
 
404
- # @file_exists_check
405
406
  def load_document_file(
406
407
  self,
407
408
  file_path: str,
@@ -427,8 +428,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
427
428
  block_delimiter: str = "",
428
429
  extract_images: bool = False,
429
430
  ):
430
- binary_type_mapping = Chat.BINARY_TYPE_MAPPING
431
- document_type_extensions = Chat.DOCUMENT_TYPE_EXTENSIONS
431
+ binary_type_mapping = BINARY_TYPE_MAPPING
432
+ document_type_extensions = DOCUMENT_TYPE_EXTENSIONS
432
433
 
433
434
  file_type = None
434
435
  file_name_lower = file_name.lower()
@@ -467,7 +468,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
467
468
  files.sort()
468
469
  for i, file in enumerate(files):
469
470
  print(f"{i + 1}: {os.path.basename(file)}")
470
- choice = input("Please choose a file to load (enter number): ")
471
+ print("Please choose a file to load (enter number): ", end="", flush=True)
472
+ choice = sys.stdin.readline().strip()
471
473
  try:
472
474
  choice_index = int(choice) - 1
473
475
  if choice_index < 0 or choice_index >= len(files):
@@ -493,6 +495,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
493
495
 
494
496
  def do_quit(self, _):
495
497
  """Exit ara-cli"""
498
+ self.agent_manager.cleanup_agent_process()
496
499
  print("Chat ended")
497
500
  self.last_result = True
498
501
  return True
@@ -562,7 +565,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
562
565
  import glob
563
566
 
564
567
  if file_name == "":
565
- file_name = input("What file do you want to load? ")
568
+ print("What file do you want to load? ", end="", flush=True)
569
+ file_name = sys.stdin.readline().strip()
566
570
  file_pattern = os.path.join(os.path.dirname(self.chat_name), file_name)
567
571
  matching_files = glob.glob(file_pattern)
568
572
  if not matching_files:
@@ -573,7 +577,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
573
577
  return matching_files
574
578
 
575
579
  def load_image(self, file_name: str, prefix: str = "", suffix: str = ""):
576
- binary_type_mapping = Chat.BINARY_TYPE_MAPPING
580
+ binary_type_mapping = BINARY_TYPE_MAPPING
577
581
 
578
582
  file_type = None
579
583
  file_name_lower = file_name.lower()
@@ -602,6 +606,62 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
602
606
  return False
603
607
  return True
604
608
 
609
+ @cmd2.with_category(CATEGORY_CHAT_CONTROL)
610
+ def do_search(self, query: str):
611
+ """Perform a web search and append the results to the chat.
612
+ Usage: search <query>
613
+ """
614
+ if not query:
615
+ self.poutput("Please provide a search query.")
616
+ return
617
+
618
+ # Check if web search is supported by the current model
619
+ from ara_cli.prompt_handler import LLMSingleton
620
+
621
+ chat_instance = LLMSingleton.get_instance()
622
+ config_parameters = chat_instance.get_config_by_purpose("default")
623
+ default_llm = config_parameters.get("model")
624
+
625
+ is_supported, _ = is_web_search_supported(default_llm)
626
+ if not is_supported:
627
+ self.poutput(get_supported_models_message(default_llm))
628
+ return
629
+
630
+ self.add_prompt_tag_if_needed(self.chat_name)
631
+
632
+ role_marker = f"# Web Search Results for '{query}':"
633
+
634
+ with open(self.chat_name, "a+", encoding="utf-8") as file:
635
+ last_line = self.get_last_line(file)
636
+
637
+ self.poutput(role_marker)
638
+
639
+ if not last_line.startswith(role_marker):
640
+ if last_line:
641
+ file.write("\n")
642
+ file.write(role_marker + "\n")
643
+
644
+ try:
645
+ # perform_web_search_completion now returns a generator or a string
646
+ search_result = perform_web_search_completion(query)
647
+
648
+ if isinstance(search_result, str):
649
+ # If it's a string, it's an error/info message
650
+ self.poutput(search_result)
651
+ file.write(search_result + "\n")
652
+ else:
653
+ # Otherwise, it's a generator, stream the content
654
+ for chunk in search_result:
655
+ chunk_content = chunk.choices[0].delta.content
656
+ if not chunk_content:
657
+ continue
658
+ self.poutput(chunk_content, end="")
659
+ file.write(chunk_content)
660
+ file.flush()
661
+ self.poutput("")
662
+ except Exception as e:
663
+ error_handler.report_error(e)
664
+
605
665
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
606
666
  def do_LOAD_IMAGE(self, file_name):
607
667
  """Load an image file and append it to chat file. Can be given the file name in-line. Will attempt to find the file relative to chat file first, then treat the given path as absolute"""
@@ -619,7 +679,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
619
679
  # Determine mime type
620
680
  file_type = None
621
681
  file_path_lower = file_path.lower()
622
- for extension, mime_type in Chat.BINARY_TYPE_MAPPING.items():
682
+ for extension, mime_type in BINARY_TYPE_MAPPING.items():
623
683
  if file_path_lower.endswith(extension):
624
684
  file_type = mime_type
625
685
  break
@@ -726,7 +786,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
726
786
  def do_NEW(self, chat_name):
727
787
  """Create a new chat. Optionally provide a chat name in-line: NEW new_chat"""
728
788
  if chat_name == "":
729
- chat_name = input("What should be the new chat name? ")
789
+ print("What should be the new chat name? ", end="", flush=True)
790
+ chat_name = sys.stdin.readline().strip()
730
791
  current_directory = os.path.dirname(self.chat_name)
731
792
  chat_file_path = os.path.join(current_directory, chat_name)
732
793
  self.__init__(chat_file_path)
@@ -739,7 +800,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
739
800
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
740
801
  def do_CLEAR(self, _):
741
802
  """Clear the chat and the file containing it"""
742
- user_input = input("Are you sure you want to clear the chat? (y/N): ")
803
+ print("Are you sure you want to clear the chat? (y/N): ", end="", flush=True)
804
+ user_input = sys.stdin.readline().strip()
743
805
  if user_input.lower() != "y":
744
806
  return
745
807
  self.create_empty_chat_file(self.chat_name)
@@ -750,17 +812,23 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
750
812
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
751
813
  def do_LOAD_RULES(self, rules_name):
752
814
  """Load rules from ./prompt.data/*.rules.md or from a specified template directory if an argument is given. Specify global/<rules_template> to access globally defined rules templates"""
753
- self.template_loader.load_template(rules_name, "rules", self.chat_name, "*.rules.md")
815
+ self.template_loader.load_template(
816
+ rules_name, "rules", self.chat_name, "*.rules.md"
817
+ )
754
818
 
755
819
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
756
820
  def do_LOAD_INTENTION(self, intention_name):
757
821
  """Load intention from ./prompt.data/*.intention.md or from a specified template directory if an argument is given. Specify global/<intention_template> to access globally defined intention templates"""
758
- self.template_loader.load_template(intention_name, "intention", self.chat_name, "*.intention.md")
822
+ self.template_loader.load_template(
823
+ intention_name, "intention", self.chat_name, "*.intention.md"
824
+ )
759
825
 
760
826
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
761
827
  def do_LOAD_COMMANDS(self, commands_name):
762
828
  """Load commands from ./prompt.data/*.commands.md or from a specified template directory if an argument is given. Specify global/<commands_template> to access globally defined commands templates"""
763
- self.template_loader.load_template(commands_name, "commands", self.chat_name, "*.commands.md")
829
+ self.template_loader.load_template(
830
+ commands_name, "commands", self.chat_name, "*.commands.md"
831
+ )
764
832
 
765
833
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
766
834
  def do_LOAD_BLUEPRINT(self, blueprint_name):
@@ -934,7 +1002,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
934
1002
  def do_SEND(self, _):
935
1003
  """Send prompt to the LLM"""
936
1004
  message = "\n".join(self.message_buffer)
937
- self.save_message(Chat.ROLE_PROMPT, message)
1005
+ self.save_message(ROLE_PROMPT, message)
938
1006
  self.send_message()
939
1007
 
940
1008
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
@@ -1053,7 +1121,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
1053
1121
 
1054
1122
  def _template_completer(self, text: str, template_type: str) -> list[str]:
1055
1123
  """Generic completer for different template types."""
1056
- available_templates = self.template_loader.get_available_templates(template_type, os.path.dirname(self.chat_name))
1124
+ available_templates = self.template_loader.get_available_templates(
1125
+ template_type, os.path.dirname(self.chat_name)
1126
+ )
1057
1127
  if not text:
1058
1128
  return available_templates
1059
1129
  return [t for t in available_templates if t.startswith(text)]
@@ -1072,4 +1142,155 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
1072
1142
 
1073
1143
  def complete_LOAD_BLUEPRINT(self, text, line, begidx, endidx):
1074
1144
  """Completer for the LOAD_BLUEPRINT command."""
1075
- return self._template_completer(text, "blueprint")
1145
+ return self._template_completer(text, "blueprint")
1146
+
1147
+ def _select_script_from_list(
1148
+ self, scripts: list[str], not_found_message: str, prompt: str
1149
+ ) -> str | None:
1150
+ """Displays a list of scripts and prompts the user to select one."""
1151
+ if not scripts:
1152
+ self.poutput(not_found_message)
1153
+ return None
1154
+
1155
+ # Sort the scripts alphabetically by their basename for consistent display
1156
+ # Create a list of (basename, full_script_name) tuples for sorting and later retrieval
1157
+ scripts_with_basenames = [(os.path.basename(s), s) for s in scripts]
1158
+ scripts_with_basenames.sort(
1159
+ key=lambda x: x[0].lower()
1160
+ ) # Sort by lowercase basename
1161
+
1162
+ for i, (basename, full_script_name) in enumerate(scripts_with_basenames):
1163
+ self.poutput(f"{i + 1}: {basename}")
1164
+
1165
+ try:
1166
+ choice = input(prompt)
1167
+ choice_index = int(choice) - 1
1168
+ if 0 <= choice_index < len(scripts_with_basenames):
1169
+ # Return the full script name from the sorted list
1170
+ return scripts_with_basenames[choice_index][1]
1171
+ else:
1172
+ self.poutput("Invalid choice. Aborting.")
1173
+ return None
1174
+ except (ValueError, EOFError):
1175
+ self.poutput("Invalid input. Aborting.")
1176
+ return None
1177
+
1178
+ @cmd2.with_category(CATEGORY_SCRIPT_CONTROL)
1179
+ def do_run_pyscript(self, args):
1180
+ """Run a python script from the chat.
1181
+ Usage: run_pyscript <script_name> [args...]
1182
+ """
1183
+ script_name, script_args = self._parse_run_pyscript_args(args)
1184
+
1185
+ # If no script name provided, list available scripts grouped by type
1186
+ if not script_name:
1187
+ self._list_available_scripts()
1188
+ return
1189
+
1190
+ script_to_run = self._resolve_script_to_run(script_name, script_args)
1191
+
1192
+ if not script_to_run:
1193
+ return
1194
+
1195
+ # Pass arguments to script runner
1196
+ output = self.script_runner.run_script(script_to_run, script_args)
1197
+ if output:
1198
+ self.poutput(output.strip())
1199
+
1200
+ def _list_available_scripts(self):
1201
+ """Lists available scripts grouped by type (global and custom)."""
1202
+ global_scripts = self.script_lister.get_global_scripts()
1203
+ custom_scripts = self.script_lister.get_custom_scripts()
1204
+
1205
+ if not global_scripts and not custom_scripts:
1206
+ self.poutput("No scripts found.")
1207
+ return
1208
+
1209
+ self.poutput("Available scripts:")
1210
+ self.poutput("")
1211
+
1212
+ if custom_scripts:
1213
+ self.poutput("Custom scripts:")
1214
+ for script in sorted(custom_scripts):
1215
+ self.poutput(f" {script}")
1216
+ self.poutput("")
1217
+
1218
+ if global_scripts:
1219
+ self.poutput("Global scripts:")
1220
+ for script in sorted(global_scripts):
1221
+ self.poutput(f" global/{script}")
1222
+
1223
+ def _parse_run_pyscript_args(self, args):
1224
+ """Parses arguments for run_pyscript command."""
1225
+ import shlex
1226
+
1227
+ if not args:
1228
+ return "", []
1229
+
1230
+ # args is a cmd2.Statement (subclass of str), so we can use it directly
1231
+ full_args = str(args)
1232
+ # Use shlex to split arguments, enabling quoted args support
1233
+ split_args = shlex.split(full_args)
1234
+ if not split_args:
1235
+ return "", []
1236
+
1237
+ script_name = split_args[0]
1238
+ script_args = split_args[1:] if len(split_args) > 1 else []
1239
+ return script_name, script_args
1240
+
1241
+ def _resolve_script_to_run(self, script_name, script_args):
1242
+ """Resolves the script name to run."""
1243
+ return script_name
1244
+
1245
+ def complete_run_pyscript(self, text, line, begidx, endidx):
1246
+ """Completer for the run_pyscript command."""
1247
+ # Get all scripts: ['custom.py', 'global/global.py']
1248
+ available_scripts = self.script_lister.get_all_scripts()
1249
+
1250
+ # Add special commands
1251
+ special_commands = [
1252
+ # "global/"
1253
+ # "*", "global/*"
1254
+ ]
1255
+
1256
+ possible_completions = sorted(list(set(available_scripts + special_commands)))
1257
+
1258
+ # Filter based on what the user has typed
1259
+ return [s for s in possible_completions if s.startswith(text)]
1260
+
1261
+ # ===== AGENT CONTROL COMMANDS =====
1262
+
1263
+ @cmd2.with_category(CATEGORY_AGENT_CONTROL)
1264
+ def do_LIST_AGENTS(self, _):
1265
+ """Lists all available executable binary agents."""
1266
+ from ara_cli.commands.list_agents_command import ListAgentsCommand
1267
+
1268
+ command = ListAgentsCommand(chat_instance=self)
1269
+ command.execute()
1270
+
1271
+ @cmd2.with_category(CATEGORY_AGENT_CONTROL)
1272
+ def do_AGENT_RUN(self, args):
1273
+ """Run a binary agent interactively from the 'ara/.araconfig/agents' directory.
1274
+ Usage: AGENT_RUN <agent_name> [arg1] [arg2] ...
1275
+ Example:
1276
+ AGENT_RUN feature-creation -b .
1277
+ """
1278
+
1279
+ from ara_cli.commands.agent_run_command import AgentRunCommand
1280
+
1281
+ command = AgentRunCommand(self, args)
1282
+ command.execute()
1283
+
1284
+ def complete_AGENT_RUN(self, text, line, begidx, endidx):
1285
+ """Completer for AGENT_RUN command."""
1286
+ from ara_cli.commands.list_agents_command import list_available_binary_agents
1287
+
1288
+ parts = line.split()
1289
+ # This completer runs when the user is typing the first argument (the agent name)
1290
+ if len(parts) < 2 or (len(parts) == 2 and not line.endswith(" ")):
1291
+ available_agents = list_available_binary_agents(self)
1292
+ if not text:
1293
+ return available_agents
1294
+ return [a for a in available_agents if a.startswith(text)]
1295
+ # For subsequent arguments, we can offer file/directory completion
1296
+ return self.path_complete(text, line, begidx, endidx)
File without changes