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.
- ara_cli/__init__.py +0 -1
- ara_cli/__main__.py +95 -2
- ara_cli/artefact_autofix.py +44 -6
- ara_cli/artefact_models/artefact_model.py +18 -6
- ara_cli/artefact_models/artefact_templates.py +2 -1
- ara_cli/artefact_models/epic_artefact_model.py +11 -2
- ara_cli/artefact_models/feature_artefact_model.py +31 -1
- ara_cli/artefact_models/userstory_artefact_model.py +13 -1
- ara_cli/chat.py +142 -37
- ara_cli/chat_agent/__init__.py +0 -0
- ara_cli/chat_agent/agent_communicator.py +62 -0
- ara_cli/chat_agent/agent_process_manager.py +211 -0
- ara_cli/chat_agent/agent_status_manager.py +73 -0
- ara_cli/chat_agent/agent_workspace_manager.py +76 -0
- ara_cli/directory_navigator.py +37 -4
- ara_cli/file_loaders/text_file_loader.py +2 -2
- ara_cli/global_file_lister.py +5 -15
- ara_cli/prompt_extractor.py +179 -71
- ara_cli/prompt_handler.py +160 -59
- ara_cli/tag_extractor.py +26 -23
- ara_cli/template_loader.py +1 -1
- ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
- ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
- ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
- ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
- ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
- ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
- ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
- ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
- ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
- ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
- ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
- ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
- ara_cli/version.py +1 -1
- {ara_cli-0.1.10.1.dist-info → ara_cli-0.1.11.0.dist-info}/METADATA +31 -1
- {ara_cli-0.1.10.1.dist-info → ara_cli-0.1.11.0.dist-info}/RECORD +41 -41
- tests/test_global_file_lister.py +1 -1
- tests/test_prompt_handler.py +12 -4
- ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
- ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
- ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
- ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
- ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
- ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
- ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
- ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
- {ara_cli-0.1.10.1.dist-info → ara_cli-0.1.11.0.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.10.1.dist-info → ara_cli-0.1.11.0.dist-info}/entry_points.txt +0 -0
- {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="?",
|
|
29
|
-
|
|
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",
|
|
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(
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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",
|
|
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 = [
|
|
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(
|
|
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)
|