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
@@ -0,0 +1,106 @@
1
+ import os
2
+
3
+ from ara_cli.commands.command import Command
4
+ from ara_cli.directory_navigator import DirectoryNavigator
5
+
6
+
7
+ class FetchAgentsCommand(Command):
8
+ """Command to fetch binary agents from a remote URL.
9
+
10
+ This command downloads a binary agent from a hardcoded URL and
11
+ saves it to the project's ara/.araconfig/agents/ directory.
12
+ """
13
+
14
+ AGENT_URL = "https://s3-public.talsen.team/so-agents/feature-creation"
15
+
16
+ def __init__(self, output=None):
17
+ """Initialize the FetchAgentsCommand.
18
+
19
+ Parameters
20
+ ----------
21
+ output : callable, optional
22
+ Output function for displaying messages. Defaults to print.
23
+ """
24
+ self.output = output or print
25
+
26
+ def execute(self):
27
+ """Execute the fetch-agents command.
28
+
29
+ Downloads a binary agent from a remote URL and saves it to the
30
+ project's .araconfig/agents directory.
31
+ """
32
+ navigator = DirectoryNavigator()
33
+ original_directory = os.getcwd()
34
+
35
+ import requests
36
+ from rich.progress import (
37
+ BarColumn,
38
+ DownloadColumn,
39
+ Progress,
40
+ TextColumn,
41
+ TimeRemainingColumn,
42
+ TransferSpeedColumn,
43
+ )
44
+
45
+ try:
46
+ # Navigate to ara directory
47
+ navigator.navigate_to_target()
48
+
49
+ dest_dir = self._get_project_agents_dir()
50
+ os.makedirs(dest_dir, exist_ok=True)
51
+
52
+ agent_name = self.AGENT_URL.split("/")[-1]
53
+ dest_path = os.path.join(dest_dir, agent_name)
54
+
55
+ self.output(f"Downloading agent from {self.AGENT_URL}...")
56
+
57
+ response = requests.get(self.AGENT_URL, stream=True)
58
+ response.raise_for_status()
59
+
60
+ total_size = int(response.headers.get("content-length", 0))
61
+ block_size = 1024
62
+ progress = Progress(
63
+ TextColumn("[bold blue]{task.description}", justify="right"),
64
+ BarColumn(bar_width=None),
65
+ "[progress.percentage]{task.percentage:>3.1f}%",
66
+ "•",
67
+ DownloadColumn(),
68
+ "•",
69
+ TransferSpeedColumn(),
70
+ "•",
71
+ TimeRemainingColumn(),
72
+ )
73
+
74
+ with progress:
75
+ task_id = progress.add_task(
76
+ f"Downloading {agent_name}", total=total_size
77
+ )
78
+ with open(dest_path, "wb") as f:
79
+ for data in response.iter_content(block_size):
80
+ progress.update(task_id, advance=len(data))
81
+ f.write(data)
82
+
83
+ if total_size != 0 and os.path.getsize(dest_path) != total_size:
84
+ raise Exception("ERROR, something went wrong during download")
85
+
86
+ # Make the binary executable
87
+ os.chmod(dest_path, 0o755)
88
+
89
+ self.output(f"Downloaded {agent_name} to ara/.araconfig/agents/")
90
+ self.output("Binary agents fetched successfully to ara/.araconfig/agents/")
91
+
92
+ except requests.exceptions.RequestException as e:
93
+ self.output(f"Error downloading agent: {e}")
94
+ finally:
95
+ # Return to original directory
96
+ os.chdir(original_directory)
97
+
98
+ def _get_project_agents_dir(self):
99
+ """Get the path to the project agents directory.
100
+
101
+ Returns
102
+ -------
103
+ str
104
+ Path to ara/.araconfig/agents directory.
105
+ """
106
+ return os.path.join(".araconfig", "agents")
@@ -0,0 +1,43 @@
1
+ import os
2
+ import shutil
3
+ from ara_cli.commands.command import Command
4
+ from ara_cli.ara_config import ConfigManager
5
+ from ara_cli.directory_navigator import DirectoryNavigator
6
+
7
+ class FetchScriptsCommand(Command):
8
+ def __init__(self, output=None):
9
+ self.output = output or print
10
+ self.config = ConfigManager.get_config()
11
+
12
+ def execute(self):
13
+ navigator = DirectoryNavigator()
14
+ original_directory = os.getcwd()
15
+ navigator.navigate_to_target()
16
+ os.chdir('..')
17
+
18
+ global_scripts_dir = self._get_global_scripts_dir()
19
+ global_scripts_config_dir = self._get_global_scripts_config_dir()
20
+
21
+ if not os.path.exists(global_scripts_dir):
22
+ self.output("Global scripts directory not found.")
23
+ os.chdir(original_directory)
24
+ return
25
+
26
+ if not os.path.exists(global_scripts_config_dir):
27
+ os.makedirs(global_scripts_config_dir)
28
+
29
+ for item in os.listdir(global_scripts_dir):
30
+ source = os.path.join(global_scripts_dir, item)
31
+ destination = os.path.join(global_scripts_config_dir, item)
32
+ if os.path.isfile(source):
33
+ shutil.copy2(source, destination)
34
+ self.output(f"Copied {item} to global scripts directory.")
35
+
36
+ os.chdir(original_directory)
37
+
38
+ def _get_global_scripts_dir(self):
39
+ base_path = os.path.dirname(os.path.dirname(__file__))
40
+ return os.path.join(base_path, "templates", "global-scripts")
41
+
42
+ def _get_global_scripts_config_dir(self):
43
+ return os.path.join(self.config.local_prompt_templates_dir, "global-scripts")
@@ -0,0 +1,39 @@
1
+ from os.path import join
2
+ import os
3
+ import shutil
4
+ from ara_cli.commands.command import Command
5
+ from ara_cli.ara_config import ConfigManager
6
+ from ara_cli.template_manager import TemplatePathManager
7
+
8
+
9
+ class FetchTemplatesCommand(Command):
10
+ def __init__(self, output=None):
11
+ self.output = output or print
12
+
13
+ def execute(self):
14
+ config = ConfigManager().get_config()
15
+ prompt_templates_dir = config.local_prompt_templates_dir
16
+ template_base_path = TemplatePathManager.get_template_base_path()
17
+ global_prompt_templates_path = join(
18
+ template_base_path, "prompt-modules")
19
+
20
+ subdirs = ["commands", "rules", "intentions", "blueprints"]
21
+
22
+ os.makedirs(join(prompt_templates_dir,
23
+ "global-prompt-modules"), exist_ok=True)
24
+ for subdir in subdirs:
25
+ target_dir = join(prompt_templates_dir,
26
+ "global-prompt-modules", subdir)
27
+ source_dir = join(global_prompt_templates_path, subdir)
28
+ os.makedirs(target_dir, exist_ok=True)
29
+ for item in os.listdir(source_dir):
30
+ source = join(source_dir, item)
31
+ target = join(target_dir, item)
32
+ shutil.copy2(source, target)
33
+
34
+ custom_prompt_templates_subdir = config.custom_prompt_templates_subdir
35
+ local_prompt_modules_dir = join(
36
+ prompt_templates_dir, custom_prompt_templates_subdir)
37
+ os.makedirs(local_prompt_modules_dir, exist_ok=True)
38
+ for subdir in subdirs:
39
+ os.makedirs(join(local_prompt_modules_dir, subdir), exist_ok=True)
@@ -0,0 +1,39 @@
1
+ from os.path import join
2
+ import os
3
+ import shutil
4
+ from ara_cli.commands.command import Command
5
+ from ara_cli.ara_config import ConfigManager
6
+ from ara_cli.template_manager import TemplatePathManager
7
+
8
+
9
+ class FetchTemplatesCommand(Command):
10
+ def __init__(self, output=None):
11
+ self.output = output or print
12
+
13
+ def execute(self):
14
+ config = ConfigManager().get_config()
15
+ prompt_templates_dir = config.local_prompt_templates_dir
16
+ template_base_path = TemplatePathManager.get_template_base_path()
17
+ global_prompt_templates_path = join(
18
+ template_base_path, "prompt-modules")
19
+
20
+ subdirs = ["commands", "rules", "intentions", "blueprints"]
21
+
22
+ os.makedirs(join(prompt_templates_dir,
23
+ "global-prompt-modules"), exist_ok=True)
24
+ for subdir in subdirs:
25
+ target_dir = join(prompt_templates_dir,
26
+ "global-prompt-modules", subdir)
27
+ source_dir = join(global_prompt_templates_path, subdir)
28
+ os.makedirs(target_dir, exist_ok=True)
29
+ for item in os.listdir(source_dir):
30
+ source = join(source_dir, item)
31
+ target = join(target_dir, item)
32
+ shutil.copy2(source, target)
33
+
34
+ custom_prompt_templates_subdir = config.custom_prompt_templates_subdir
35
+ local_prompt_modules_dir = join(
36
+ prompt_templates_dir, custom_prompt_templates_subdir)
37
+ os.makedirs(local_prompt_modules_dir, exist_ok=True)
38
+ for subdir in subdirs:
39
+ os.makedirs(join(local_prompt_modules_dir, subdir), exist_ok=True)
@@ -0,0 +1,39 @@
1
+ import os
2
+ from ara_cli.commands.command import Command
3
+
4
+
5
+ def list_available_binary_agents(chat_instance):
6
+ """Helper to list executable files in the agents directory."""
7
+ try:
8
+ base_dir = chat_instance._find_project_root()
9
+ if not base_dir:
10
+ return [] # Can't find project root
11
+
12
+ agents_dir = os.path.join(base_dir, "ara", ".araconfig", "agents")
13
+ if not os.path.isdir(agents_dir):
14
+ return []
15
+
16
+ available_agents = []
17
+ for f in os.listdir(agents_dir):
18
+ path = os.path.join(agents_dir, f)
19
+ if os.path.isfile(path) and os.access(path, os.X_OK):
20
+ available_agents.append(f)
21
+ return available_agents
22
+ except Exception:
23
+ return [] # Fail silently
24
+
25
+
26
+ class ListAgentsCommand(Command):
27
+ def __init__(self, chat_instance):
28
+ self.chat_instance = chat_instance
29
+
30
+ def execute(self):
31
+ """Lists all available executable binary agents."""
32
+ print("Searching for available agents in 'ara/.araconfig/agents/'...")
33
+ available_agents = list_available_binary_agents(self.chat_instance)
34
+ if available_agents:
35
+ print("\nAvailable binary agents:")
36
+ for agent in available_agents:
37
+ print(f" - {agent}")
38
+ else:
39
+ print("No executable binary agents found.")
ara_cli/completers.py CHANGED
@@ -4,18 +4,17 @@ from pathlib import Path
4
4
  import typer
5
5
 
6
6
  from ara_cli.classifier import Classifier
7
- from ara_cli.template_manager import SpecificationBreakdownAspects
7
+ from ara_cli.constants import VALID_ASPECTS
8
8
 
9
9
 
10
10
  def complete_classifier(incomplete: str) -> List[str]:
11
11
  """Complete classifier names."""
12
- classifiers = Classifier.ordered_classifiers()
13
- return [c for c in classifiers if c.startswith(incomplete)]
12
+ return [c for c in Classifier.ordered_classifiers() if c.startswith(incomplete)]
14
13
 
15
14
 
16
15
  def complete_aspect(incomplete: str) -> List[str]:
17
16
  """Complete aspect names."""
18
- aspects = SpecificationBreakdownAspects.VALID_ASPECTS
17
+ aspects = VALID_ASPECTS
19
18
  return [a for a in aspects if a.startswith(incomplete)]
20
19
 
21
20
 
@@ -31,23 +30,31 @@ def complete_template_type(incomplete: str) -> List[str]:
31
30
  return [t for t in template_types if t.startswith(incomplete)]
32
31
 
33
32
 
34
- def complete_artefact_name(classifier: str) -> List[str]:
33
+ def complete_artefact_name(classifier: str, incomplete: str = "") -> List[str]:
35
34
  """Complete artefact names for a given classifier."""
36
35
  try:
37
36
  # Get the directory for the classifier
38
37
  classifier_dir = f"ara/{Classifier.get_sub_directory(classifier)}"
39
-
38
+
40
39
  if not os.path.exists(classifier_dir):
41
40
  return []
42
-
41
+
43
42
  # Find all files with the classifier extension
44
43
  artefacts = []
44
+ suffix = f".{classifier}"
45
+
46
+ # We only care about files that match the incomplete prefix AND the suffix
47
+ # Since os.listdir gives filenames, we can check startswith(incomplete)
48
+ # but we must be careful because the file starts with the artefact name, not necessarily the incomplete part + suffix directly unless full match.
49
+ # Actually, incomplete corresponds to the artefact name part.
50
+ # So file must start with incomplete AND end with suffix.
51
+
45
52
  for file in os.listdir(classifier_dir):
46
- if file.endswith(f'.{classifier}'):
53
+ if file.startswith(incomplete) and file.endswith(suffix):
47
54
  # Remove the extension to get the artefact name
48
- name = file[:-len(f'.{classifier}')]
55
+ name = file[: -len(suffix)]
49
56
  artefacts.append(name)
50
-
57
+
51
58
  return sorted(artefacts)
52
59
  except Exception:
53
60
  return []
@@ -55,9 +62,10 @@ def complete_artefact_name(classifier: str) -> List[str]:
55
62
 
56
63
  def complete_artefact_name_for_classifier(classifier: str):
57
64
  """Create a completer function for artefact names of a specific classifier."""
65
+
58
66
  def completer(incomplete: str) -> List[str]:
59
- artefacts = complete_artefact_name(classifier)
60
- return [a for a in artefacts if a.startswith(incomplete)]
67
+ return complete_artefact_name(classifier, incomplete)
68
+
61
69
  return completer
62
70
 
63
71
 
@@ -66,13 +74,14 @@ def complete_chat_files(incomplete: str) -> List[str]:
66
74
  try:
67
75
  chat_files = []
68
76
  current_dir = Path.cwd()
69
-
70
- # Look for .md files in current directory
71
- for file in current_dir.glob("*.md"):
72
- name = file.stem
73
- if name.startswith(incomplete):
74
- chat_files.append(name)
75
-
77
+
78
+ # Optimize by using glob with the incomplete prefix
79
+ # Note: glob pattern needs to be careful with special chars, but for autocompletion usually fine.
80
+ pattern = f"{incomplete}*.md" if incomplete else "*.md"
81
+
82
+ for file in current_dir.glob(pattern):
83
+ chat_files.append(file.stem)
84
+
76
85
  return sorted(chat_files)
77
86
  except Exception:
78
87
  return []
@@ -83,62 +92,89 @@ class DynamicCompleters:
83
92
  @staticmethod
84
93
  def create_classifier_completer():
85
94
  """Create a completer for classifiers."""
95
+
86
96
  def completer(ctx: typer.Context, incomplete: str) -> List[str]:
87
97
  return complete_classifier(incomplete)
98
+
88
99
  return completer
89
-
100
+
90
101
  @staticmethod
91
102
  def create_aspect_completer():
92
103
  """Create a completer for aspects."""
104
+
93
105
  def completer(ctx: typer.Context, incomplete: str) -> List[str]:
94
106
  return complete_aspect(incomplete)
107
+
95
108
  return completer
96
-
109
+
97
110
  @staticmethod
98
111
  def create_status_completer():
99
112
  """Create a completer for status values."""
113
+
100
114
  def completer(ctx: typer.Context, incomplete: str) -> List[str]:
101
115
  return complete_status(incomplete)
116
+
102
117
  return completer
103
-
118
+
104
119
  @staticmethod
105
120
  def create_template_type_completer():
106
121
  """Create a completer for template types."""
122
+
107
123
  def completer(ctx: typer.Context, incomplete: str) -> List[str]:
108
124
  return complete_template_type(incomplete)
125
+
109
126
  return completer
110
-
127
+
111
128
  @staticmethod
112
129
  def create_artefact_name_completer():
113
130
  """Create a completer for artefact names based on classifier context."""
131
+
114
132
  def completer(ctx: typer.Context, incomplete: str) -> List[str]:
115
133
  # Try to get classifier from context
116
- if hasattr(ctx, 'params') and 'classifier' in ctx.params:
117
- classifier = ctx.params['classifier']
118
- if hasattr(classifier, 'value'):
134
+ if hasattr(ctx, "params") and "classifier" in ctx.params:
135
+ classifier = ctx.params["classifier"]
136
+ if hasattr(classifier, "value"):
119
137
  classifier = classifier.value
120
- artefacts = complete_artefact_name(classifier)
121
- return [a for a in artefacts if a.startswith(incomplete)]
138
+ return complete_artefact_name(classifier, incomplete)
122
139
  return []
140
+
123
141
  return completer
124
-
142
+
125
143
  @staticmethod
126
144
  def create_parent_name_completer():
127
145
  """Create a completer for parent artefact names based on parent classifier context."""
146
+
128
147
  def completer(ctx: typer.Context, incomplete: str) -> List[str]:
129
148
  # Try to get parent_classifier from context
130
- if hasattr(ctx, 'params') and 'parent_classifier' in ctx.params:
131
- parent_classifier = ctx.params['parent_classifier']
132
- if hasattr(parent_classifier, 'value'):
149
+ if hasattr(ctx, "params") and "parent_classifier" in ctx.params:
150
+ parent_classifier = ctx.params["parent_classifier"]
151
+ if hasattr(parent_classifier, "value"):
133
152
  parent_classifier = parent_classifier.value
134
- artefacts = complete_artefact_name(parent_classifier)
135
- return [a for a in artefacts if a.startswith(incomplete)]
153
+ return complete_artefact_name(parent_classifier, incomplete)
136
154
  return []
155
+
137
156
  return completer
138
-
157
+
139
158
  @staticmethod
140
159
  def create_chat_file_completer():
141
160
  """Create a completer for chat files."""
161
+
142
162
  def completer(ctx: typer.Context, incomplete: str) -> List[str]:
143
163
  return complete_chat_files(incomplete)
164
+
165
+ return completer
166
+
167
+ @staticmethod
168
+ def create_convert_source_artefact_name_completer():
169
+ """Create a completer for convert command source artefact names based on old_classifier context."""
170
+
171
+ def completer(ctx: typer.Context, incomplete: str) -> List[str]:
172
+ # Try to get old_classifier from context
173
+ if hasattr(ctx, "params") and "old_classifier" in ctx.params:
174
+ old_classifier = ctx.params["old_classifier"]
175
+ if hasattr(old_classifier, "value"):
176
+ old_classifier = old_classifier.value
177
+ return complete_artefact_name(old_classifier, incomplete)
178
+ return []
179
+
144
180
  return completer
ara_cli/constants.py ADDED
@@ -0,0 +1,2 @@
1
+
2
+ VALID_ASPECTS = ["technology", "concept", "persona", "customer", "step"]
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import sys
2
3
  from os.path import join, exists, isdir, dirname, basename
3
4
  # from ara_cli.directory_searcher import DirectorySearcher
4
5
 
@@ -23,7 +24,8 @@ class DirectoryNavigator:
23
24
  return original_directory
24
25
 
25
26
  current_directory = original_directory
26
- while current_directory != dirname(current_directory): # Ensure loop breaks at root
27
+ # Ensure loop breaks at root
28
+ while current_directory != dirname(current_directory):
27
29
  potential_path = join(current_directory, self.target_directory)
28
30
  if self.exists(potential_path):
29
31
  os.chdir(potential_path)
@@ -31,7 +33,8 @@ class DirectoryNavigator:
31
33
  current_directory = dirname(current_directory)
32
34
 
33
35
  # If the loop completes, the target directory was not found
34
- user_input = input(f"Unable to locate the '{self.target_directory}' directory. Do you want to create an 'ara' folder in the working directory? (y/N): ").strip().lower()
36
+ user_input = input(
37
+ f"Unable to locate the '{self.target_directory}' directory. Do you want to create an 'ara' folder in the working directory? (y/N): ").strip().lower()
35
38
 
36
39
  if user_input == '' or user_input == 'y':
37
40
  ara_folder_path = join(original_directory, 'ara')
@@ -40,7 +43,8 @@ class DirectoryNavigator:
40
43
  os.chdir(ara_folder_path)
41
44
  return original_directory
42
45
  else:
43
- print(f"Unable to locate the '{self.target_directory}' directory and user declined to create 'ara' folder.")
46
+ print(
47
+ f"Unable to locate the '{self.target_directory}' directory and user declined to create 'ara' folder.")
44
48
  sys.exit(0)
45
49
 
46
50
  def navigate_to_relative(self, relative_path):
@@ -56,7 +60,36 @@ class DirectoryNavigator:
56
60
  if self.exists(path):
57
61
  os.chdir(path)
58
62
  else:
59
- raise Exception(f"Unable to navigate to '{relative_path}' relative to the target directory.")
63
+ raise Exception(
64
+ f"Unable to navigate to '{relative_path}' relative to the target directory.")
65
+
66
+ @staticmethod
67
+ def find_ara_directory_root():
68
+ """Find the root ara directory by traversing up the directory tree."""
69
+ current_dir = os.getcwd()
70
+
71
+ # Check if we're already inside an ara directory structure
72
+ path_parts = current_dir.split(os.sep)
73
+
74
+ # Look for 'ara' in the path parts
75
+ if 'ara' in path_parts:
76
+ ara_index = path_parts.index('ara')
77
+ # Reconstruct path up to and including 'ara'
78
+ ara_root_parts = path_parts[:ara_index + 1]
79
+ potential_ara_root = os.sep.join(ara_root_parts)
80
+ if os.path.exists(potential_ara_root) and os.path.isdir(potential_ara_root):
81
+ return potential_ara_root
82
+
83
+ # If not inside ara directory, check current directory and parents
84
+ check_dir = current_dir
85
+ # Stop at filesystem root
86
+ while check_dir != os.path.dirname(check_dir):
87
+ ara_path = os.path.join(check_dir, 'ara')
88
+ if os.path.exists(ara_path) and os.path.isdir(ara_path):
89
+ return ara_path
90
+ check_dir = os.path.dirname(check_dir)
91
+
92
+ return None
60
93
 
61
94
  # debug version
62
95
  # def get_ara_directory(self):
ara_cli/llm_utils.py ADDED
@@ -0,0 +1,58 @@
1
+ from ara_cli.ara_config import ConfigManager
2
+ from pydantic_ai import Agent
3
+
4
+ FALLBACK_MODEL = "anthropic:claude-4-sonnet-20250514"
5
+
6
+
7
+ def get_configured_conversion_llm_model() -> str:
8
+ """
9
+ Retrieves the configured conversion LLM model string, adapted for pydantic_ai.
10
+ Falls back to a default model if configuration is missing or invalid.
11
+ """
12
+ model_name = FALLBACK_MODEL
13
+ try:
14
+ config = ConfigManager.get_config()
15
+ conversion_llm_key = config.conversion_llm
16
+
17
+ if conversion_llm_key and conversion_llm_key in config.llm_config:
18
+ llm_config_item = config.llm_config[conversion_llm_key]
19
+ raw_model_name = llm_config_item.model
20
+
21
+ # Adapt LiteLLM model string to PydanticAI format
22
+ # LiteLLM: provider/model-name (e.g. openai/gpt-4o)
23
+ # PydanticAI: provider:model-name (e.g. openai:gpt-4o)
24
+ if "/" in raw_model_name and ":" not in raw_model_name:
25
+ parts = raw_model_name.split("/", 1)
26
+ if len(parts) == 2:
27
+ model_name = f"{parts[0]}:{parts[1]}"
28
+ else:
29
+ model_name = raw_model_name
30
+ else:
31
+ model_name = raw_model_name
32
+ else:
33
+ print(
34
+ f"Warning: Conversion LLM configuration issue. Using fallback model: {FALLBACK_MODEL}"
35
+ )
36
+ except Exception as e:
37
+ print(
38
+ f"Warning: Error resolving LLM config ({e}). Using fallback model: {FALLBACK_MODEL}"
39
+ )
40
+ model_name = FALLBACK_MODEL
41
+
42
+ return model_name
43
+
44
+
45
+ def create_pydantic_ai_agent(
46
+ output_type, model_name: str = None, instrument: bool = True
47
+ ) -> Agent:
48
+ """
49
+ Creates a pydantic_ai Agent with the specified or configured model.
50
+ """
51
+ if not model_name:
52
+ model_name = get_configured_conversion_llm_model()
53
+
54
+ return Agent(
55
+ model=model_name,
56
+ output_type=output_type,
57
+ instrument=instrument,
58
+ )
ara_cli/prompt_chat.py CHANGED
@@ -6,9 +6,18 @@ from ara_cli.update_config_prompt import update_artefact_config_prompt_files
6
6
  from ara_cli.output_suppressor import suppress_stdout
7
7
 
8
8
 
9
- def initialize_prompt_chat_mode(classifier, param, chat_name, reset=None, output_mode=False, append_strings=[], restricted=False):
9
+ def initialize_prompt_chat_mode(
10
+ classifier,
11
+ param,
12
+ chat_name,
13
+ reset=None,
14
+ output_mode=False,
15
+ append_strings=[],
16
+ restricted=False,
17
+ ):
10
18
  sub_directory = Classifier.get_sub_directory(classifier)
11
- artefact_data_path = os.path.join("ara", sub_directory, f"{param}.data") # f"ara/{sub_directory}/{parameter}.data"
19
+ # f"ara/{sub_directory}/{parameter}.data"
20
+ artefact_data_path = os.path.join("ara", sub_directory, f"{param}.data")
12
21
 
13
22
  if chat_name is None:
14
23
  chat_name = classifier
@@ -17,11 +26,18 @@ def initialize_prompt_chat_mode(classifier, param, chat_name, reset=None, output
17
26
  update_artefact_config_prompt_files(classifier, param, automatic_update=True)
18
27
 
19
28
  classifier_chat_file = os.path.join(artefact_data_path, f"{chat_name}")
20
- start_chat_session(classifier_chat_file, reset, output_mode, append_strings, restricted)
29
+ start_chat_session(
30
+ classifier_chat_file, reset, output_mode, append_strings, restricted
31
+ )
32
+
21
33
 
22
34
  def start_chat_session(chat_file, reset, output_mode, append_strings, restricted):
23
35
  with suppress_stdout(suppress=output_mode):
24
- chat = Chat(chat_file, reset=reset) if not restricted else Chat(chat_file, reset=reset, enable_commands=whitelisted_commands)
36
+ chat = (
37
+ Chat(chat_file, reset=reset)
38
+ if not restricted
39
+ else Chat(chat_file, reset=reset, enable_commands=whitelisted_commands)
40
+ )
25
41
  if append_strings:
26
42
  chat.append_strings(append_strings)
27
43
  if output_mode: