ara-cli 0.1.10.1__py3-none-any.whl → 0.1.11.0__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.

Potentially problematic release.


This version of ara-cli might be problematic. Click here for more details.

Files changed (57) hide show
  1. ara_cli/__init__.py +0 -1
  2. ara_cli/__main__.py +95 -2
  3. ara_cli/artefact_autofix.py +44 -6
  4. ara_cli/artefact_models/artefact_model.py +18 -6
  5. ara_cli/artefact_models/artefact_templates.py +2 -1
  6. ara_cli/artefact_models/epic_artefact_model.py +11 -2
  7. ara_cli/artefact_models/feature_artefact_model.py +31 -1
  8. ara_cli/artefact_models/userstory_artefact_model.py +13 -1
  9. ara_cli/chat.py +142 -37
  10. ara_cli/chat_agent/__init__.py +0 -0
  11. ara_cli/chat_agent/agent_communicator.py +62 -0
  12. ara_cli/chat_agent/agent_process_manager.py +211 -0
  13. ara_cli/chat_agent/agent_status_manager.py +73 -0
  14. ara_cli/chat_agent/agent_workspace_manager.py +76 -0
  15. ara_cli/directory_navigator.py +37 -4
  16. ara_cli/file_loaders/text_file_loader.py +2 -2
  17. ara_cli/global_file_lister.py +5 -15
  18. ara_cli/prompt_extractor.py +179 -71
  19. ara_cli/prompt_handler.py +160 -59
  20. ara_cli/tag_extractor.py +26 -23
  21. ara_cli/template_loader.py +1 -1
  22. ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
  23. ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
  24. ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
  25. ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
  26. ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
  27. ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
  28. ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
  29. ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
  30. ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
  31. ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
  32. ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
  33. ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
  34. ara_cli/version.py +1 -1
  35. {ara_cli-0.1.10.1.dist-info → ara_cli-0.1.11.0.dist-info}/METADATA +31 -1
  36. {ara_cli-0.1.10.1.dist-info → ara_cli-0.1.11.0.dist-info}/RECORD +41 -41
  37. tests/test_global_file_lister.py +1 -1
  38. tests/test_prompt_handler.py +12 -4
  39. ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
  40. ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
  41. ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
  42. ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
  43. ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
  44. ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
  45. ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
  46. ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
  47. ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
  48. ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
  49. ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
  50. ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
  51. ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
  52. ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
  53. ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
  54. ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
  55. {ara_cli-0.1.10.1.dist-info → ara_cli-0.1.11.0.dist-info}/WHEEL +0 -0
  56. {ara_cli-0.1.10.1.dist-info → ara_cli-0.1.11.0.dist-info}/entry_points.txt +0 -0
  57. {ara_cli-0.1.10.1.dist-info → ara_cli-0.1.11.0.dist-info}/top_level.txt +0 -0
ara_cli/chat.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import os
2
- import errno
3
2
  import argparse
4
3
  import cmd2
5
4
 
@@ -11,6 +10,7 @@ from ara_cli.error_handler import AraError, AraConfigurationError
11
10
  from ara_cli.file_loaders.document_file_loader import DocumentFileLoader
12
11
  from ara_cli.file_loaders.binary_file_loader import BinaryFileLoader
13
12
  from ara_cli.file_loaders.text_file_loader import TextFileLoader
13
+ from ara_cli.chat_agent.agent_process_manager import AgentProcessManager
14
14
 
15
15
 
16
16
  extract_parser = argparse.ArgumentParser()
@@ -25,26 +25,8 @@ extract_parser.add_argument(
25
25
  )
26
26
 
27
27
  load_parser = argparse.ArgumentParser()
28
- load_parser.add_argument("file_name", nargs="?", default="", help="File to load")
29
- load_parser.add_argument(
30
- "--load-images",
31
- action="store_true",
32
- help="Extract and describe images from documents",
33
- )
34
-
35
- extract_parser = argparse.ArgumentParser()
36
- extract_parser.add_argument(
37
- "-f", "--force", action="store_true", help="Force extraction"
38
- )
39
- extract_parser.add_argument(
40
- "-w",
41
- "--write",
42
- action="store_true",
43
- help="Overwrite existing files without using LLM for merging.",
44
- )
45
-
46
- load_parser = argparse.ArgumentParser()
47
- load_parser.add_argument("file_name", nargs="?", default="", help="File to load")
28
+ load_parser.add_argument("file_name", nargs="?",
29
+ default="", help="File to load")
48
30
  load_parser.add_argument(
49
31
  "--load-images",
50
32
  action="store_true",
@@ -55,6 +37,7 @@ load_parser.add_argument(
55
37
  class Chat(cmd2.Cmd):
56
38
  CATEGORY_CHAT_CONTROL = "Chat control commands"
57
39
  CATEGORY_LLM_CONTROL = "Language model controls"
40
+ CATEGORY_AGENT_CONTROL = "Agent control commands"
58
41
 
59
42
  INTRO = """/***************************************/
60
43
  araarar
@@ -78,6 +61,14 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
78
61
  ROLE_PROMPT = "ara prompt"
79
62
  ROLE_RESPONSE = "ara response"
80
63
 
64
+ # Available agents
65
+ AVAILABLE_AGENTS = [
66
+ "interview_agent",
67
+ "autocoder_v2_agent",
68
+ "question_and_answer_agent",
69
+ "feature_creation_guide_agent",
70
+ ]
71
+
81
72
  BINARY_TYPE_MAPPING = {
82
73
  ".png": "image/png",
83
74
  ".jpg": "image/jpeg",
@@ -93,6 +84,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
93
84
  enable_commands: list[str] | None = None,
94
85
  ):
95
86
  from ara_cli.template_loader import TemplateLoader
87
+
96
88
  shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
97
89
  if enable_commands:
98
90
  enable_commands.append("quit") # always allow quitting
@@ -126,6 +118,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
126
118
  self.config = self._retrieve_ara_config()
127
119
  self.template_loader = TemplateLoader(chat_instance=self)
128
120
 
121
+ # Initialize agent process manager
122
+ self.agent_manager = AgentProcessManager(self)
123
+
129
124
  def disable_commands(self, commands: list[str]):
130
125
  for command in commands:
131
126
  setattr(self, f"do_{command}", self.default)
@@ -153,6 +148,11 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
153
148
  self.aliases["lg"] = "LOAD_GIVENS"
154
149
  self.aliases["lb"] = "LOAD_BLUEPRINT"
155
150
  self.aliases["lt"] = "LOAD_TEMPLATE"
151
+ # Agent control aliases
152
+ self.aliases["a"] = "AGENT_RUN"
153
+ self.aliases["as"] = "AGENT_STOP"
154
+ self.aliases["ac"] = "AGENT_CONTINUE"
155
+ self.aliases["astat"] = "AGENT_STATUS"
156
156
 
157
157
  def setup_chat(self, chat_name, reset: bool = None):
158
158
  if os.path.exists(chat_name):
@@ -197,6 +197,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
197
197
  print(f"File {file_name} not found.")
198
198
  return False
199
199
  return method(self, file_path, *args, **kwargs)
200
+
200
201
  return wrapper
201
202
 
202
203
  @staticmethod
@@ -251,7 +252,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
251
252
  for line in message.splitlines():
252
253
  match = image_pattern.search(line)
253
254
  if match:
254
- image_data = {"type": "image_url", "image_url": {"url": match.group(1)}}
255
+ image_data = {"type": "image_url",
256
+ "image_url": {"url": match.group(1)}}
255
257
  image_data_list.append(image_data)
256
258
  else:
257
259
  text_content.append(line)
@@ -378,6 +380,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
378
380
  def add_prompt_tag_if_needed(self, chat_file: str):
379
381
  with open(chat_file, "r", encoding="utf-8") as file:
380
382
  lines = file.readlines()
383
+
381
384
  prompt_tag = f"# {Chat.ROLE_PROMPT}:"
382
385
  if Chat.get_last_role_marker(lines) == prompt_tag:
383
386
  return
@@ -388,7 +391,6 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
388
391
  with open(chat_file, "a", encoding="utf-8") as file:
389
392
  file.write(append)
390
393
 
391
- # @file_exists_check
392
394
  def load_text_file(
393
395
  self,
394
396
  file_path,
@@ -406,7 +408,6 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
406
408
  extract_images=extract_images,
407
409
  )
408
410
 
409
- # @file_exists_check
410
411
  def load_binary_file(
411
412
  self, file_path, mime_type: str, prefix: str = "", suffix: str = ""
412
413
  ):
@@ -420,7 +421,6 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
420
421
  reader = MarkdownReader(file_path)
421
422
  return reader.read(extract_images=extract_images)
422
423
 
423
- # @file_exists_check
424
424
  def load_document_file(
425
425
  self,
426
426
  file_path: str,
@@ -496,7 +496,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
496
496
  return None
497
497
  file_path = files[choice_index]
498
498
  except ValueError as e:
499
- error_handler.report_error(ValueError("Invalid input. Aborting load."))
499
+ error_handler.report_error(
500
+ ValueError("Invalid input. Aborting load."))
500
501
  return None
501
502
  else:
502
503
  file_path = files[0]
@@ -512,6 +513,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
512
513
 
513
514
  def do_quit(self, _):
514
515
  """Exit ara-cli"""
516
+ self.agent_manager.cleanup_agent_process()
515
517
  print("Chat ended")
516
518
  self.last_result = True
517
519
  return True
@@ -531,7 +533,16 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
531
533
  )
532
534
 
533
535
  def default(self, line):
534
- self.message_buffer.append(self.full_input)
536
+ if self.agent_manager.agent_mode:
537
+ if self.full_input.lower() in ["quit", "exit"]:
538
+ self.agent_manager.cleanup_agent_process()
539
+ print("Agent stopped.")
540
+ else:
541
+ # In agent mode, send input directly to agent
542
+ self.agent_manager.send_to_agent(self.full_input)
543
+ else:
544
+ # In normal chat mode, buffer the message
545
+ self.message_buffer.append(self.full_input)
535
546
 
536
547
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
537
548
  @cmd2.with_argparser(load_parser)
@@ -606,7 +617,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
606
617
  file_path=file_name, mime_type=file_type, prefix=prefix, suffix=suffix
607
618
  )
608
619
  error_handler.report_error(
609
- AraError(f"File {file_name} not recognized as image, could not load")
620
+ AraError(
621
+ f"File {file_name} not recognized as image, could not load")
610
622
  )
611
623
 
612
624
  def _verify_llm_choice(self, model_name):
@@ -769,22 +781,29 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
769
781
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
770
782
  def do_LOAD_RULES(self, rules_name):
771
783
  """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"""
772
- self.template_loader.load_template(rules_name, "rules", self.chat_name, "*.rules.md")
784
+ self.template_loader.load_template(
785
+ rules_name, "rules", self.chat_name, "*.rules.md"
786
+ )
773
787
 
774
788
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
775
789
  def do_LOAD_INTENTION(self, intention_name):
776
790
  """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"""
777
- self.template_loader.load_template(intention_name, "intention", self.chat_name, "*.intention.md")
791
+ self.template_loader.load_template(
792
+ intention_name, "intention", self.chat_name, "*.intention.md"
793
+ )
778
794
 
779
795
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
780
796
  def do_LOAD_COMMANDS(self, commands_name):
781
797
  """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"""
782
- self.template_loader.load_template(commands_name, "commands", self.chat_name, "*.commands.md")
798
+ self.template_loader.load_template(
799
+ commands_name, "commands", self.chat_name, "*.commands.md"
800
+ )
783
801
 
784
802
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
785
803
  def do_LOAD_BLUEPRINT(self, blueprint_name):
786
804
  """Load specified blueprint. Specify global/<blueprint_name> to access globally defined blueprints"""
787
- self.template_loader.load_template(blueprint_name, "blueprint", self.chat_name)
805
+ self.template_loader.load_template(
806
+ blueprint_name, "blueprint", self.chat_name)
788
807
 
789
808
  def _load_helper(
790
809
  self,
@@ -795,7 +814,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
795
814
  ):
796
815
  import glob
797
816
 
798
- directory_path = os.path.join(os.path.dirname(self.chat_name), directory)
817
+ directory_path = os.path.join(
818
+ os.path.dirname(self.chat_name), directory)
799
819
  file_pattern = os.path.join(directory_path, pattern)
800
820
 
801
821
  exclude_files = []
@@ -906,12 +926,14 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
906
926
 
907
927
  # If no file_name, check for defaults
908
928
  default_files_to_check = [
909
- os.path.join(base_directory, "prompt.data", "config.prompt_givens.md"),
929
+ os.path.join(base_directory, "prompt.data",
930
+ "config.prompt_givens.md"),
910
931
  os.path.join(
911
932
  base_directory, "prompt.data", "config.prompt_global_givens.md"
912
933
  ),
913
934
  ]
914
- existing_defaults = [f for f in default_files_to_check if os.path.exists(f)]
935
+ existing_defaults = [
936
+ f for f in default_files_to_check if os.path.exists(f)]
915
937
  if existing_defaults:
916
938
  return existing_defaults
917
939
 
@@ -1072,7 +1094,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
1072
1094
 
1073
1095
  def _template_completer(self, text: str, template_type: str) -> list[str]:
1074
1096
  """Generic completer for different template types."""
1075
- available_templates = self.template_loader.get_available_templates(template_type, os.path.dirname(self.chat_name))
1097
+ available_templates = self.template_loader.get_available_templates(
1098
+ template_type, os.path.dirname(self.chat_name)
1099
+ )
1076
1100
  if not text:
1077
1101
  return available_templates
1078
1102
  return [t for t in available_templates if t.startswith(text)]
@@ -1091,4 +1115,85 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
1091
1115
 
1092
1116
  def complete_LOAD_BLUEPRINT(self, text, line, begidx, endidx):
1093
1117
  """Completer for the LOAD_BLUEPRINT command."""
1094
- return self._template_completer(text, "blueprint")
1118
+ return self._template_completer(text, "blueprint")
1119
+
1120
+ # ===== AGENT CONTROL COMMANDS =====
1121
+
1122
+ @cmd2.with_category(CATEGORY_AGENT_CONTROL)
1123
+ def do_AGENT_RUN(self, args):
1124
+ """Run an agent. Usage: AGENT_RUN <agent_name> [artefact_classifier artefact_name]
1125
+
1126
+ Examples:
1127
+ AGENT_RUN interview_agent
1128
+ AGENT_RUN interview_agent feature my_feature
1129
+ """
1130
+ parts = args.split()
1131
+ if not parts:
1132
+ print(f"Available agents: {', '.join(self.AVAILABLE_AGENTS)}")
1133
+ print(
1134
+ "Usage: AGENT_RUN <agent_name> [artefact_classifier artefact_name]")
1135
+ return
1136
+
1137
+ agent_name = parts[0]
1138
+ if agent_name not in self.AVAILABLE_AGENTS:
1139
+ print(f"Unknown agent: {agent_name}")
1140
+ print(f"Available agents: {', '.join(self.AVAILABLE_AGENTS)}")
1141
+ return
1142
+
1143
+ # Get initial prompt
1144
+ initial_prompt = input(
1145
+ "Enter initial prompt (or press Enter for default): "
1146
+ ).strip()
1147
+ if not initial_prompt:
1148
+ initial_prompt = "Let's begin the interview."
1149
+
1150
+ # Extract artefact info if provided
1151
+ artefact_classifier = None
1152
+ artefact_name = None
1153
+ if len(parts) >= 3:
1154
+ artefact_classifier = parts[1]
1155
+ artefact_name = parts[2]
1156
+
1157
+ try:
1158
+ self.agent_manager.start_agent(
1159
+ agent_name, initial_prompt, artefact_classifier, artefact_name
1160
+ )
1161
+ except AraError as e:
1162
+ print(f"Error: {e}")
1163
+
1164
+ @cmd2.with_category(CATEGORY_AGENT_CONTROL)
1165
+ def do_AGENT_STOP(self, _):
1166
+ """Stop the currently running agent gracefully (sends termination signal)."""
1167
+ if not self.agent_manager.agent_process:
1168
+ print("No agent is currently running.")
1169
+ return
1170
+
1171
+ print(f"Stopping agent {self.agent_manager.agent_name}...")
1172
+ self.agent_manager.cleanup_agent_process()
1173
+ print("Agent stopped.")
1174
+
1175
+ @cmd2.with_category(CATEGORY_AGENT_CONTROL)
1176
+ def do_AGENT_CONTINUE(self, _):
1177
+ """Continue with the agent without providing new input (sends empty line)."""
1178
+ self.agent_manager.continue_agent()
1179
+
1180
+ @cmd2.with_category(CATEGORY_AGENT_CONTROL)
1181
+ def do_AGENT_STATUS(self, _):
1182
+ """Show status of the current agent."""
1183
+ print(self.agent_manager.get_agent_status())
1184
+
1185
+ def complete_AGENT_RUN(self, text, line, begidx, endidx):
1186
+ """Completer for AGENT_RUN command."""
1187
+ parts = line.split()
1188
+
1189
+ # Complete agent name
1190
+ if len(parts) <= 2:
1191
+ if not text:
1192
+ return self.AVAILABLE_AGENTS
1193
+ return [a for a in self.AVAILABLE_AGENTS if a.startswith(text)]
1194
+
1195
+ # Complete classifier
1196
+ if len(parts) == 3:
1197
+ return self._complete_classifiers(text, line, begidx, endidx)
1198
+
1199
+ return []
File without changes
@@ -0,0 +1,62 @@
1
+ import re
2
+ import queue
3
+
4
+
5
+ class AgentCommunicator:
6
+ def __init__(self, agent_process_manager):
7
+ self.agent_process_manager = agent_process_manager
8
+ self.agent_process = agent_process_manager.agent_process
9
+ self.agent_output_queue = agent_process_manager.agent_output_queue
10
+ self.chat_instance = agent_process_manager.chat_instance
11
+ self.status_manager = agent_process_manager.status_manager
12
+
13
+ def read_agent_output(self):
14
+ try:
15
+ for line in iter(self.agent_process.stdout.readline, ""):
16
+ if not line:
17
+ break
18
+ self.agent_output_queue.put(line)
19
+ except Exception as e:
20
+ self.agent_output_queue.put(f"Error reading agent output: {e}\n")
21
+
22
+ def _handle_agent_answer(self, lines, answer_index):
23
+ for j in range(answer_index + 1, len(lines) - 1):
24
+ print(lines[j], flush=True)
25
+ print(flush=True)
26
+
27
+ self.agent_process_manager.agent_mode = True
28
+ self.chat_instance.prompt = "ara-agent> "
29
+ self.status_manager.update_status_file(mode="waiting for input")
30
+
31
+ def _process_lines(self, lines):
32
+ for i, line_text in enumerate(lines[:-1]):
33
+ print(line_text, flush=True)
34
+ if re.match(r'^\s*Answer:', line_text):
35
+ self._handle_agent_answer(lines, i)
36
+ return True, lines[-1]
37
+ return False, lines[-1]
38
+
39
+ def process_agent_output(self):
40
+ buffer = ""
41
+ while True:
42
+ try:
43
+ line = self.agent_output_queue.get(timeout=0.1)
44
+ buffer += line
45
+
46
+ if "\n" in buffer:
47
+ lines = buffer.split("\n")
48
+ found_answer, buffer = self._process_lines(lines)
49
+ if found_answer:
50
+ return
51
+
52
+ except queue.Empty:
53
+ if self.agent_process and self.agent_process.poll() is not None:
54
+ if buffer:
55
+ print(buffer, flush=True)
56
+ self.agent_process_manager.cleanup_agent_process()
57
+ return
58
+ continue
59
+ except Exception as e:
60
+ print(f"Error processing agent output: {e}", flush=True)
61
+ self.agent_process_manager.cleanup_agent_process()
62
+ return
@@ -0,0 +1,211 @@
1
+ from ara_cli.chat_agent.agent_workspace_manager import AgentWorkspaceManager
2
+ from ara_cli.error_handler import AraError, ErrorLevel
3
+ from ara_cli.chat_agent.agent_status_manager import AgentStatusManager
4
+ from ara_cli.chat_agent.agent_communicator import AgentCommunicator
5
+ import os
6
+ import subprocess
7
+ import threading
8
+ import psutil
9
+ import queue
10
+
11
+
12
+ class AgentProcessManager:
13
+ _instance = None
14
+
15
+ def __new__(cls, chat_instance=None):
16
+ if cls._instance is None:
17
+ cls._instance = super(AgentProcessManager, cls).__new__(cls)
18
+ cls._instance._initialized = False
19
+ return cls._instance
20
+
21
+ def __init__(self, chat_instance=None):
22
+ if hasattr(self, "_initialized") and self._initialized:
23
+ if chat_instance:
24
+ self.chat_instance = chat_instance
25
+ return
26
+ if chat_instance is None:
27
+ raise ValueError(
28
+ "chat_instance must be provided for the first instantiation"
29
+ )
30
+
31
+ self.chat_instance = chat_instance
32
+ self.agent_process = None
33
+ self.agent_reader_thread = None
34
+ self.agent_output_queue = queue.Queue()
35
+ self.agent_mode = False
36
+ self.agent_name = None
37
+ self.status_manager = AgentStatusManager()
38
+ self._initialized = True
39
+
40
+ def cleanup_agent_process(self):
41
+ pid = self.status_manager.get_agent_pid()
42
+ if pid and psutil.pid_exists(pid):
43
+ try:
44
+ p = psutil.Process(pid)
45
+ p.terminate()
46
+ p.wait(timeout=3)
47
+ except (psutil.NoSuchProcess, psutil.TimeoutExpired):
48
+ pass
49
+
50
+ self.status_manager.clear_status()
51
+
52
+ if self.agent_process:
53
+ try:
54
+ self.agent_process.terminate()
55
+ except:
56
+ pass
57
+ self.agent_process = None
58
+
59
+ if self.agent_reader_thread and self.agent_reader_thread.is_alive():
60
+ self.agent_reader_thread = None
61
+
62
+ self.agent_mode = False
63
+ self.agent_name = None
64
+ if hasattr(self, "chat_instance"):
65
+ self.chat_instance.prompt = "ara> "
66
+
67
+ def start_agent(
68
+ self, agent_name, initial_prompt, artefact_classifier=None, artefact_name=None
69
+ ):
70
+ if self.get_agent_status() != "No agent is currently running.":
71
+ raise AraError(
72
+ "An agent is already running. Use AGENT_STOP to stop it first."
73
+ )
74
+
75
+ if agent_name not in self.chat_instance.AVAILABLE_AGENTS:
76
+ raise AraError(f"Unknown agent: {agent_name}")
77
+
78
+ base_work_dir = AgentWorkspaceManager.determine_base_work_dir(
79
+ self.chat_instance
80
+ )
81
+ agent_workspace_dir = AgentWorkspaceManager.determine_agent_workspace(
82
+ self.chat_instance, artefact_classifier, artefact_name
83
+ )
84
+
85
+ cmd = [
86
+ "ara-agents",
87
+ agent_name,
88
+ "-u",
89
+ initial_prompt,
90
+ "-g",
91
+ "roundrobin",
92
+ "-b",
93
+ ".",
94
+ "-s",
95
+ ]
96
+
97
+ if artefact_classifier and artefact_name:
98
+ try:
99
+ artefact_path = self._find_artefact_path(
100
+ artefact_classifier, artefact_name
101
+ )
102
+ cmd.extend(["-r", artefact_path])
103
+ print(f"Starting {agent_name} with artefact: {artefact_path}")
104
+ print(f"Base work directory: {base_work_dir}")
105
+ print(f"Agent logs directory: {agent_workspace_dir}")
106
+ except AraError as e:
107
+ raise AraError(f"Error: {e}")
108
+ else:
109
+ print(f"Starting {agent_name}...")
110
+ print(f"Base work directory: {base_work_dir}")
111
+ print(f"Agent logs directory: {agent_workspace_dir}")
112
+
113
+ env = os.environ.copy()
114
+ env["CENTRAL_LOG_PATH"] = os.path.join(
115
+ agent_workspace_dir, "io_context.log")
116
+
117
+ try:
118
+ self.agent_process = subprocess.Popen(
119
+ cmd,
120
+ stdin=subprocess.PIPE,
121
+ stdout=subprocess.PIPE,
122
+ stderr=subprocess.STDOUT,
123
+ text=True,
124
+ bufsize=1,
125
+ cwd=base_work_dir,
126
+ env=env,
127
+ )
128
+ self.agent_name = agent_name
129
+ self.status_manager.write_status(
130
+ self.agent_process.pid, self.agent_name, "processing"
131
+ )
132
+
133
+ communicator = AgentCommunicator(self)
134
+ self.agent_reader_thread = threading.Thread(
135
+ target=communicator.read_agent_output, daemon=True
136
+ )
137
+ self.agent_reader_thread.start()
138
+
139
+ print(f"Agent {agent_name} started. Waiting for response...\n")
140
+ communicator.process_agent_output()
141
+
142
+ except FileNotFoundError:
143
+ raise AraError(
144
+ "Agent could not started."
145
+ "\nReason: 'ara-agents' command not found. Make sure ara-agents is locally installed.",
146
+ level=ErrorLevel.CRITICAL,
147
+ )
148
+ except Exception as e:
149
+ self.cleanup_agent_process()
150
+ raise AraError(f"Error starting agent: {e}")
151
+
152
+ def _find_artefact_path(self, artefact_classifier, artefact_name):
153
+ from ara_cli.classifier import Classifier
154
+
155
+ classifier_dir = Classifier.get_sub_directory(artefact_classifier)
156
+ if not classifier_dir:
157
+ raise AraError(f"Unknown classifier: {artefact_classifier}")
158
+
159
+ chat_dir = os.path.dirname(self.chat_instance.chat_name)
160
+ ara_dir = os.path.join(chat_dir, "ara", classifier_dir)
161
+
162
+ for ext in [artefact_classifier, "md"]:
163
+ artefact_path = os.path.join(ara_dir, f"{artefact_name}.{ext}")
164
+ if os.path.exists(artefact_path):
165
+ return artefact_path
166
+
167
+ raise AraError(
168
+ f"Artefact not found: {artefact_name}.{artefact_classifier}")
169
+
170
+ def send_to_agent(self, text):
171
+ if not self.agent_process or self.agent_process.poll() is not None:
172
+ print("Error: Agent process is not running")
173
+ self.cleanup_agent_process()
174
+ return
175
+
176
+ try:
177
+ self.agent_process.stdin.write(text + "\n")
178
+ self.agent_process.stdin.flush()
179
+
180
+ self.agent_mode = False
181
+ self.chat_instance.prompt = "ara> "
182
+ self.status_manager.update_status_file(mode="processing")
183
+
184
+ communicator = AgentCommunicator(self)
185
+ communicator.process_agent_output()
186
+
187
+ except Exception as e:
188
+ print(f"Error sending to agent: {e}")
189
+ self.cleanup_agent_process()
190
+
191
+ def continue_agent(self):
192
+ status = self.get_agent_status()
193
+ if "No agent" in status:
194
+ print("No agent is currently running.")
195
+ return
196
+
197
+ if not self.agent_process or self.agent_process.poll() is not None:
198
+ print(
199
+ "AGENT_CONTINUE can only be used from the chat interface that started the agent."
200
+ )
201
+ return
202
+
203
+ if not self.agent_mode:
204
+ print("Agent is not waiting for input. Wait for 'ara-agent>' prompt.")
205
+ return
206
+
207
+ print("Continuing agent with empty input...")
208
+ self.send_to_agent("")
209
+
210
+ def get_agent_status(self):
211
+ return self.status_manager.get_agent_status()
@@ -0,0 +1,73 @@
1
+ import os
2
+ import json
3
+ import psutil
4
+ from ara_cli.directory_navigator import DirectoryNavigator
5
+
6
+
7
+ class AgentStatusManager:
8
+ def get_status_file_path(self):
9
+ ara_root = DirectoryNavigator.find_ara_directory_root()
10
+ if not ara_root:
11
+ return None
12
+ return os.path.join(ara_root, ".araconfig", "agent_status.json")
13
+
14
+ def get_agent_pid(self):
15
+ status_file = self.get_status_file_path()
16
+ if not status_file or not os.path.exists(status_file):
17
+ return None
18
+
19
+ with open(status_file, "r") as f:
20
+ try:
21
+ status = json.load(f)
22
+ return status.get("pid")
23
+ except json.JSONDecodeError:
24
+ return None
25
+
26
+ def update_status_file(self, mode):
27
+ status_file = self.get_status_file_path()
28
+ if not status_file or not os.path.exists(status_file):
29
+ return
30
+
31
+ try:
32
+ with open(status_file, "r") as f:
33
+ status = json.load(f)
34
+ except (FileNotFoundError, json.JSONDecodeError):
35
+ return
36
+
37
+ status["mode"] = mode
38
+
39
+ with open(status_file, "w") as f:
40
+ json.dump(status, f)
41
+
42
+ def get_agent_status(self):
43
+ status_file = self.get_status_file_path()
44
+ if not status_file or not os.path.exists(status_file):
45
+ return "No agent is currently running."
46
+
47
+ with open(status_file, "r") as f:
48
+ try:
49
+ status = json.load(f)
50
+ except json.JSONDecodeError:
51
+ return "No agent is currently running."
52
+
53
+ pid = status.get("pid")
54
+ if not pid or not psutil.pid_exists(pid):
55
+ if os.path.exists(status_file):
56
+ os.remove(status_file)
57
+ return "No agent is currently running."
58
+
59
+ name = status.get("name")
60
+ mode = status.get("mode")
61
+ return f"Agent: {name} (running - {mode})"
62
+
63
+ def write_status(self, pid, name, mode):
64
+ status = {"pid": pid, "name": name, "mode": mode}
65
+ status_file = self.get_status_file_path()
66
+ if status_file:
67
+ with open(status_file, "w") as f:
68
+ json.dump(status, f)
69
+
70
+ def clear_status(self):
71
+ status_file = self.get_status_file_path()
72
+ if status_file and os.path.exists(status_file):
73
+ os.remove(status_file)