ara-cli 0.1.10.0__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 (140) hide show
  1. ara_cli/__init__.py +51 -6
  2. ara_cli/__main__.py +270 -103
  3. ara_cli/ara_command_action.py +106 -63
  4. ara_cli/ara_config.py +187 -128
  5. ara_cli/ara_subcommands/__init__.py +0 -0
  6. ara_cli/ara_subcommands/autofix.py +26 -0
  7. ara_cli/ara_subcommands/chat.py +27 -0
  8. ara_cli/ara_subcommands/classifier_directory.py +16 -0
  9. ara_cli/ara_subcommands/common.py +100 -0
  10. ara_cli/ara_subcommands/config.py +221 -0
  11. ara_cli/ara_subcommands/convert.py +43 -0
  12. ara_cli/ara_subcommands/create.py +75 -0
  13. ara_cli/ara_subcommands/delete.py +22 -0
  14. ara_cli/ara_subcommands/extract.py +22 -0
  15. ara_cli/ara_subcommands/fetch.py +41 -0
  16. ara_cli/ara_subcommands/fetch_agents.py +22 -0
  17. ara_cli/ara_subcommands/fetch_scripts.py +19 -0
  18. ara_cli/ara_subcommands/fetch_templates.py +19 -0
  19. ara_cli/ara_subcommands/list.py +139 -0
  20. ara_cli/ara_subcommands/list_tags.py +25 -0
  21. ara_cli/ara_subcommands/load.py +48 -0
  22. ara_cli/ara_subcommands/prompt.py +136 -0
  23. ara_cli/ara_subcommands/read.py +47 -0
  24. ara_cli/ara_subcommands/read_status.py +20 -0
  25. ara_cli/ara_subcommands/read_user.py +20 -0
  26. ara_cli/ara_subcommands/reconnect.py +27 -0
  27. ara_cli/ara_subcommands/rename.py +22 -0
  28. ara_cli/ara_subcommands/scan.py +14 -0
  29. ara_cli/ara_subcommands/set_status.py +22 -0
  30. ara_cli/ara_subcommands/set_user.py +22 -0
  31. ara_cli/ara_subcommands/template.py +16 -0
  32. ara_cli/artefact_autofix.py +154 -63
  33. ara_cli/artefact_converter.py +256 -0
  34. ara_cli/artefact_models/artefact_model.py +106 -25
  35. ara_cli/artefact_models/artefact_templates.py +20 -10
  36. ara_cli/artefact_models/epic_artefact_model.py +11 -2
  37. ara_cli/artefact_models/feature_artefact_model.py +31 -1
  38. ara_cli/artefact_models/userstory_artefact_model.py +15 -3
  39. ara_cli/artefact_scan.py +2 -2
  40. ara_cli/chat.py +283 -80
  41. ara_cli/chat_agent/__init__.py +0 -0
  42. ara_cli/chat_agent/agent_process_manager.py +155 -0
  43. ara_cli/chat_script_runner/__init__.py +0 -0
  44. ara_cli/chat_script_runner/script_completer.py +23 -0
  45. ara_cli/chat_script_runner/script_finder.py +41 -0
  46. ara_cli/chat_script_runner/script_lister.py +36 -0
  47. ara_cli/chat_script_runner/script_runner.py +36 -0
  48. ara_cli/chat_web_search/__init__.py +0 -0
  49. ara_cli/chat_web_search/web_search.py +263 -0
  50. ara_cli/commands/agent_run_command.py +98 -0
  51. ara_cli/commands/fetch_agents_command.py +106 -0
  52. ara_cli/commands/fetch_scripts_command.py +43 -0
  53. ara_cli/commands/fetch_templates_command.py +39 -0
  54. ara_cli/commands/fetch_templates_commands.py +39 -0
  55. ara_cli/commands/list_agents_command.py +39 -0
  56. ara_cli/commands/read_command.py +17 -4
  57. ara_cli/completers.py +180 -0
  58. ara_cli/constants.py +2 -0
  59. ara_cli/directory_navigator.py +37 -4
  60. ara_cli/file_loaders/text_file_loader.py +2 -2
  61. ara_cli/global_file_lister.py +5 -15
  62. ara_cli/llm_utils.py +58 -0
  63. ara_cli/prompt_chat.py +20 -4
  64. ara_cli/prompt_extractor.py +199 -76
  65. ara_cli/prompt_handler.py +160 -59
  66. ara_cli/tag_extractor.py +38 -18
  67. ara_cli/template_loader.py +3 -2
  68. ara_cli/template_manager.py +52 -21
  69. ara_cli/templates/global-scripts/hello_global.py +1 -0
  70. ara_cli/templates/prompt-modules/commands/add_scenarios_for_new_behaviour.feature_creation_agent.commands.md +1 -0
  71. ara_cli/templates/prompt-modules/commands/align_feature_with_implementation_changes.interview_agent.commands.md +1 -0
  72. ara_cli/templates/prompt-modules/commands/analyze_codebase_and_plan_tasks.interview_agent.commands.md +1 -0
  73. ara_cli/templates/prompt-modules/commands/choose_best_parent_artefact.interview_agent.commands.md +1 -0
  74. ara_cli/templates/prompt-modules/commands/create_tasks_from_artefact_content.interview_agent.commands.md +1 -0
  75. ara_cli/templates/prompt-modules/commands/create_tests_for_uncovered_modules.test_generation_agent.commands.md +1 -0
  76. ara_cli/templates/prompt-modules/commands/derive_features_from_video_description.feature_creation_agent.commands.md +1 -0
  77. ara_cli/templates/prompt-modules/commands/describe_agent_capabilities.agent.commands.md +1 -0
  78. ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
  79. ara_cli/templates/prompt-modules/commands/execute_scoped_todos_in_task.interview_agent.commands.md +1 -0
  80. ara_cli/templates/prompt-modules/commands/explain_single_file_purpose.interview_agent.commands.md +1 -0
  81. ara_cli/templates/prompt-modules/commands/extract_file_information_bullets.interview_agent.commands.md +1 -0
  82. ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
  83. ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
  84. ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
  85. ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
  86. ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
  87. ara_cli/templates/prompt-modules/commands/fix_failing_behave_step_definitions.interview_agent.commands.md +1 -0
  88. ara_cli/templates/prompt-modules/commands/fix_failing_pytest_tests.interview_agent.commands.md +1 -0
  89. ara_cli/templates/prompt-modules/commands/general_instruction_policy.commands.md +47 -0
  90. ara_cli/templates/prompt-modules/commands/generate_and_fix_pytest_tests.test_generation_agent.commands.md +1 -0
  91. ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
  92. ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
  93. ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
  94. ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
  95. ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
  96. ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
  97. ara_cli/templates/prompt-modules/commands/suggest_next_story_child_tasks.interview_agent.commands.md +1 -0
  98. ara_cli/templates/prompt-modules/commands/summarize_or_transcribe_media.interview_agent.commands.md +1 -0
  99. ara_cli/templates/prompt-modules/commands/update_feature_to_match_implementation.feature_creation_agent.commands.md +1 -0
  100. ara_cli/templates/prompt-modules/commands/update_user_story_with_requirements.interview_agent.commands.md +1 -0
  101. ara_cli/version.py +1 -1
  102. {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/METADATA +34 -1
  103. {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/RECORD +123 -54
  104. tests/test_ara_command_action.py +31 -19
  105. tests/test_ara_config.py +177 -90
  106. tests/test_artefact_autofix.py +170 -97
  107. tests/test_artefact_autofix_integration.py +495 -0
  108. tests/test_artefact_converter.py +357 -0
  109. tests/test_artefact_extraction.py +564 -0
  110. tests/test_artefact_scan.py +1 -1
  111. tests/test_chat.py +162 -126
  112. tests/test_chat_givens_images.py +603 -0
  113. tests/test_chat_script_runner.py +454 -0
  114. tests/test_global_file_lister.py +1 -1
  115. tests/test_llm_utils.py +164 -0
  116. tests/test_prompt_chat.py +343 -0
  117. tests/test_prompt_extractor.py +683 -0
  118. tests/test_prompt_handler.py +12 -4
  119. tests/test_tag_extractor.py +19 -13
  120. tests/test_web_search.py +467 -0
  121. ara_cli/ara_command_parser.py +0 -605
  122. ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
  123. ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
  124. ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
  125. ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
  126. ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
  127. ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
  128. ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
  129. ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
  130. ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
  131. ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
  132. ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
  133. ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
  134. ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
  135. ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
  136. ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
  137. ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
  138. {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/WHEEL +0 -0
  139. {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/entry_points.txt +0 -0
  140. {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/top_level.txt +0 -0
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:
@@ -9,90 +9,208 @@ from ara_cli.directory_navigator import DirectoryNavigator
9
9
  from ara_cli.artefact_models.artefact_mapping import title_prefix_to_artefact_class
10
10
 
11
11
 
12
- def extract_code_blocks_md(markdown_text):
13
- md = MarkdownIt()
14
- tokens = md.parse(markdown_text)
15
- code_blocks = [token.content for token in tokens if token.type == 'fence']
16
- return code_blocks
12
+ def _find_extract_token(tokens):
13
+ """Find the first token that needs to be processed."""
14
+ for token in tokens:
15
+ if token.type == 'fence' and token.content.strip().startswith("# [x] extract"):
16
+ return token
17
+ return None
18
+
19
+
20
+ def _extract_file_path(content_lines):
21
+ """Extract file path from content lines."""
22
+ if not content_lines:
23
+ return None
24
+ file_path_search = re.search(r"# filename: (.+)", content_lines[0])
25
+ return file_path_search.group(1).strip() if file_path_search else None
26
+
27
+
28
+ def _find_artefact_class(content_lines):
29
+ """Find the appropriate artefact class from content lines."""
30
+ for line in content_lines[:2]:
31
+ words = line.strip().split(' ')
32
+ if not words:
33
+ continue
34
+ first_word = words[0]
35
+ if first_word in title_prefix_to_artefact_class:
36
+ return title_prefix_to_artefact_class[first_word]
37
+ return None
17
38
 
18
39
 
19
- def extract_responses(document_path, relative_to_ara_root=False, force=False, write=False):
20
- print(f"Starting extraction from '{document_path}'")
21
- block_extraction_counter = 0
40
+ def _process_file_extraction(file_path, code_content, force, write):
41
+ """Process file extraction logic."""
42
+ print(f"Filename extracted: {file_path}")
43
+ handle_existing_file(file_path, code_content, force, write)
22
44
 
23
- with open(document_path, 'r', encoding='utf-8', errors='replace') as file:
24
- content = file.read()
25
45
 
26
- cwd = os.getcwd()
27
- if relative_to_ara_root:
28
- navigator = DirectoryNavigator()
29
- navigator.navigate_to_target()
30
- os.chdir('..')
46
+ def _process_artefact_extraction(artefact_class, content_lines, force, write):
47
+ """Process artefact extraction logic."""
48
+ artefact = artefact_class.deserialize('\n'.join(content_lines))
49
+ serialized_artefact = artefact.serialize()
31
50
 
32
- code_blocks_found = extract_code_blocks_md(content)
33
- updated_content = content
51
+ original_directory = os.getcwd()
52
+ directory_navigator = DirectoryNavigator()
53
+ directory_navigator.navigate_to_target()
34
54
 
35
- for block in code_blocks_found:
36
- block_lines = block.split('\n')
55
+ artefact_path = artefact.file_path
56
+ directory = os.path.dirname(artefact_path)
57
+ os.makedirs(directory, exist_ok=True)
58
+ handle_existing_file(artefact_path, serialized_artefact, force, write)
37
59
 
38
- if "# [x] extract" not in block_lines[0]:
39
- continue
40
- print("Block found and processed.")
41
-
42
- block_lines = block_lines[1:]
60
+ os.chdir(original_directory)
43
61
 
44
- file_path_search = re.search(r"# filename: (.+)", block_lines[0])
45
62
 
46
- if file_path_search:
47
- file_path = file_path_search.group(1).strip()
48
- print(f"Filename extracted: {file_path}")
63
+ def _perform_extraction_for_block(source_lines, block_start, block_end, force, write):
64
+ """Helper function to process a single, identified block."""
65
+ original_block_text = '\n'.join(source_lines[block_start:block_end + 1])
66
+ block_content_lines = source_lines[block_start + 1:block_end]
67
+ block_content = '\n'.join(block_content_lines)
49
68
 
50
- block_lines = block_lines[1:] # Remove first line again after removing filename line
51
- block = '\n'.join(block_lines)
69
+ block_lines = block_content.split('\n')
70
+ content_lines_after_extract = block_lines[1:]
52
71
 
53
- handle_existing_file(file_path, block, force, write)
54
- block_extraction_counter += 1
72
+ file_path = _extract_file_path(content_lines_after_extract)
55
73
 
56
- # Update the markdown content
57
- updated_content = update_markdown(content, block, file_path)
74
+ if file_path:
75
+ code_content = '\n'.join(content_lines_after_extract[1:])
76
+ _process_file_extraction(file_path, code_content, force, write)
77
+ else:
78
+ artefact_class = _find_artefact_class(content_lines_after_extract)
79
+ if artefact_class:
80
+ _process_artefact_extraction(
81
+ artefact_class, content_lines_after_extract, force, write)
58
82
  else:
59
- # Extract artefact
60
- artefact_class = None
61
- for line in block_lines[:2]:
62
- words = line.strip().split(' ')
63
- if not words:
64
- continue
65
- first_word = words[0]
66
- if first_word not in title_prefix_to_artefact_class:
67
- continue
68
- artefact_class = title_prefix_to_artefact_class[first_word]
69
- if not artefact_class:
70
- print("No filename found, skipping this block.")
83
+ print(
84
+ "No filename or valid artefact found, skipping processing for this block.")
85
+ return None, None
86
+
87
+ modified_block_text = original_block_text.replace(
88
+ "# [x] extract", "# [v] extract", 1)
89
+ return original_block_text, modified_block_text
90
+
91
+
92
+ class FenceDetector:
93
+ """Helper class to detect and match fence blocks."""
94
+
95
+ def __init__(self, source_lines):
96
+ self.source_lines = source_lines
97
+
98
+ def is_extract_fence(self, line_num):
99
+ """Check if line is a fence with extract marker."""
100
+ line = self.source_lines[line_num]
101
+ stripped_line = line.strip()
102
+
103
+ is_fence = stripped_line.startswith(
104
+ '```') or stripped_line.startswith('~~~')
105
+ if not is_fence:
106
+ return False
107
+
108
+ if not (line_num + 1 < len(self.source_lines)):
109
+ return False
110
+
111
+ return self.source_lines[line_num + 1].strip().startswith("# [x] extract")
112
+
113
+ def find_matching_fence_end(self, start_line):
114
+ """Find the matching end fence for a given start fence."""
115
+ fence_line = self.source_lines[start_line]
116
+ indentation = len(fence_line) - len(fence_line.lstrip())
117
+ stripped_fence_line = fence_line.strip()
118
+ fence_char = stripped_fence_line[0]
119
+ fence_length = len(stripped_fence_line) - \
120
+ len(stripped_fence_line.lstrip(fence_char))
121
+
122
+ for i in range(start_line + 1, len(self.source_lines)):
123
+ scan_line = self.source_lines[i]
124
+ stripped_scan_line = scan_line.strip()
125
+
126
+ if not stripped_scan_line or stripped_scan_line[0] != fence_char:
71
127
  continue
72
- artefact = artefact_class.deserialize('\n'.join(block_lines))
73
- serialized_artefact = artefact.serialize()
74
128
 
75
- original_directory = os.getcwd()
76
- directory_navigator = DirectoryNavigator()
77
- directory_navigator.navigate_to_target()
129
+ if not all(c == fence_char for c in stripped_scan_line):
130
+ continue
78
131
 
79
- artefact_path = artefact.file_path
80
- directory = os.path.dirname(artefact_path)
81
- os.makedirs(directory, exist_ok=True)
82
- handle_existing_file(artefact_path, serialized_artefact, force, write)
132
+ candidate_indentation = len(scan_line) - len(scan_line.lstrip())
133
+ candidate_length = len(stripped_scan_line)
134
+
135
+ if candidate_length == fence_length and candidate_indentation == indentation:
136
+ return i
137
+
138
+ return -1
139
+
140
+
141
+ def _process_document_blocks(source_lines, force, write):
142
+ """Process all extract blocks in the document."""
143
+ fence_detector = FenceDetector(source_lines)
144
+ replacements = []
145
+ line_num = 0
146
+
147
+ while line_num < len(source_lines):
148
+ if not fence_detector.is_extract_fence(line_num):
149
+ line_num += 1
150
+ continue
151
+
152
+ block_start_line = line_num
153
+ block_end_line = fence_detector.find_matching_fence_end(
154
+ block_start_line)
155
+
156
+ if block_end_line != -1:
157
+ print(
158
+ f"Block found and processed starting on line {block_start_line + 1}.")
159
+ original, modified = _perform_extraction_for_block(
160
+ source_lines, block_start_line, block_end_line, force, write
161
+ )
162
+ if original and modified:
163
+ replacements.append((original, modified))
164
+ line_num = block_end_line + 1
165
+ else:
166
+ line_num += 1
167
+
168
+ return replacements
83
169
 
84
- os.chdir(original_directory)
85
170
 
86
- # TODO: make update_markdown work block by block instead of updating the whole document at once
87
- block_extraction_counter += 1
88
- updated_content = update_markdown(content, block, None)
171
+ def _apply_replacements(content, replacements):
172
+ """Apply all replacements to the content."""
173
+ updated_content = content
174
+ for original, modified in replacements:
175
+ updated_content = updated_content.replace(original, modified, 1)
176
+ return updated_content
177
+
178
+
179
+ def _setup_working_directory(relative_to_ara_root):
180
+ """Setup working directory and return original cwd."""
181
+ cwd = os.getcwd()
182
+ if relative_to_ara_root:
183
+ navigator = DirectoryNavigator()
184
+ navigator.navigate_to_target()
185
+ os.chdir('..')
186
+ return cwd
187
+
188
+
189
+ def extract_responses(document_path, relative_to_ara_root=False, force=False, write=False):
190
+ print(f"Starting extraction from '{document_path}'", flush=True)
191
+
192
+ try:
193
+ with open(document_path, 'r', encoding='utf-8', errors='replace') as file:
194
+ content = file.read()
195
+ except FileNotFoundError:
196
+ print(
197
+ f"Error: File not found at '{document_path}'. Skipping extraction.")
198
+ return
199
+
200
+ cwd = _setup_working_directory(relative_to_ara_root)
201
+
202
+ source_lines = content.split('\n')
203
+ replacements = _process_document_blocks(source_lines, force, write)
204
+
205
+ updated_content = _apply_replacements(content, replacements)
89
206
 
90
207
  os.chdir(cwd)
91
- # Save the updated markdown content
92
208
  with open(document_path, 'w', encoding='utf-8') as file:
93
209
  file.write(updated_content)
94
210
 
95
- print(f"End of extraction. Found {block_extraction_counter} blocks.")
211
+ if replacements:
212
+ print(
213
+ f"End of extraction. Found and processed {len(replacements)} blocks in '{os.path.basename(document_path)}'.")
96
214
 
97
215
 
98
216
  def modify_and_save_file(response, file_path):
@@ -107,7 +225,8 @@ def modify_and_save_file(response, file_path):
107
225
  """)
108
226
 
109
227
  if filename_from_response != file_path:
110
- user_decision = prompt_user_decision("Filename does not match, overwrite? (y/n): ")
228
+ user_decision = prompt_user_decision(
229
+ "Filename does not match, overwrite? (y/n): ")
111
230
  if user_decision.lower() not in ['y', 'yes']:
112
231
  print("Debug: User chose not to overwrite")
113
232
  print("Skipping block.")
@@ -126,8 +245,11 @@ def prompt_user_decision(prompt):
126
245
 
127
246
  def determine_should_create(skip_query=False):
128
247
  if skip_query:
248
+ print("[DEBUG] skip_query is True, allowing creation.", flush=True)
129
249
  return True
130
- user_decision = prompt_user_decision("File does not exist. Create? (y/n): ")
250
+ print(f"[DEBUG] About to prompt for file creation: File does not exist. Create? (y/n): ", flush=True)
251
+ user_decision = prompt_user_decision(
252
+ "File does not exist. Create? (y/n): ")
131
253
  if user_decision.lower() in ['y', 'yes']:
132
254
  return True
133
255
  return False
@@ -138,7 +260,9 @@ def create_file_if_not_exist(filename, content, skip_query=False):
138
260
  if not os.path.exists(filename):
139
261
  if determine_should_create(skip_query):
140
262
  # Ensure the directory exists
141
- os.makedirs(os.path.dirname(filename), exist_ok=True)
263
+ dir_name = os.path.dirname(filename)
264
+ if dir_name:
265
+ os.makedirs(dir_name, exist_ok=True)
142
266
 
143
267
  with open(filename, 'w', encoding='utf-8') as file:
144
268
  file.write(content)
@@ -156,8 +280,8 @@ def create_prompt_for_file_modification(content_str, filename):
156
280
  print(f"WARNING: {filename} for merge prompt creation does not exist.")
157
281
  return
158
282
 
159
- content_of_existing_file = json.dumps(get_file_content(filename))
160
- content = json.dumps(content_str)
283
+ content_of_existing_file = get_file_content(filename)
284
+ content = content_str
161
285
 
162
286
  prompt_text = f"""
163
287
  * given this new_content:
@@ -185,9 +309,15 @@ def create_prompt_for_file_modification(content_str, filename):
185
309
  def handle_existing_file(filename, block_content, skip_query=False, write=False):
186
310
  if not os.path.isfile(filename):
187
311
  print(f"File {filename} does not exist, attempting to create")
312
+ # Ensure directory exists before writing
313
+ directory = os.path.dirname(filename)
314
+ if directory:
315
+ os.makedirs(directory, exist_ok=True)
188
316
  create_file_if_not_exist(filename, block_content, skip_query)
317
+
189
318
  elif write:
190
- print(f"File {filename} exists. Overwriting without LLM merge as requested.")
319
+ print(
320
+ f"File {filename} exists. Overwriting without LLM merge as requested.")
191
321
  try:
192
322
  directory = os.path.dirname(filename)
193
323
  if directory:
@@ -200,7 +330,8 @@ def handle_existing_file(filename, block_content, skip_query=False, write=False)
200
330
  print(f"Failed to overwrite file {filename} due to an OS error")
201
331
  else:
202
332
  print(f"File {filename} exists, creating modification prompt")
203
- prompt_text = create_prompt_for_file_modification(block_content, filename)
333
+ prompt_text = create_prompt_for_file_modification(
334
+ block_content, filename)
204
335
  if prompt_text is None:
205
336
  return
206
337
 
@@ -220,11 +351,3 @@ def extract_and_save_prompt_results(classifier, param, write=False):
220
351
  print(f"Extract marked sections from: {prompt_log_file}")
221
352
 
222
353
  extract_responses(prompt_log_file, write=write)
223
-
224
-
225
- def update_markdown(original_content, block_content, filename):
226
- """
227
- Update the markdown content by changing the extract block from "# [x] extract" to "# [v] extract"
228
- """
229
- updated_content = original_content.replace("# [x] extract", "# [v] extract")
230
- return updated_content