pdd-cli 0.0.24__py3-none-any.whl → 0.0.26__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 pdd-cli might be problematic. Click here for more details.

Files changed (49) hide show
  1. pdd/__init__.py +14 -1
  2. pdd/bug_main.py +5 -1
  3. pdd/bug_to_unit_test.py +16 -5
  4. pdd/change.py +2 -1
  5. pdd/change_main.py +407 -189
  6. pdd/cli.py +853 -301
  7. pdd/code_generator.py +2 -1
  8. pdd/conflicts_in_prompts.py +2 -1
  9. pdd/construct_paths.py +377 -222
  10. pdd/context_generator.py +2 -1
  11. pdd/continue_generation.py +5 -2
  12. pdd/crash_main.py +55 -20
  13. pdd/data/llm_model.csv +18 -17
  14. pdd/detect_change.py +2 -1
  15. pdd/fix_code_loop.py +465 -160
  16. pdd/fix_code_module_errors.py +7 -4
  17. pdd/fix_error_loop.py +9 -9
  18. pdd/fix_errors_from_unit_tests.py +207 -365
  19. pdd/fix_main.py +32 -4
  20. pdd/fix_verification_errors.py +148 -77
  21. pdd/fix_verification_errors_loop.py +842 -768
  22. pdd/fix_verification_main.py +412 -0
  23. pdd/generate_output_paths.py +427 -189
  24. pdd/generate_test.py +3 -2
  25. pdd/increase_tests.py +2 -2
  26. pdd/llm_invoke.py +1167 -343
  27. pdd/preprocess.py +3 -3
  28. pdd/process_csv_change.py +466 -154
  29. pdd/prompts/bug_to_unit_test_LLM.prompt +11 -11
  30. pdd/prompts/extract_prompt_update_LLM.prompt +11 -5
  31. pdd/prompts/extract_unit_code_fix_LLM.prompt +2 -2
  32. pdd/prompts/find_verification_errors_LLM.prompt +11 -9
  33. pdd/prompts/fix_code_module_errors_LLM.prompt +29 -0
  34. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +5 -5
  35. pdd/prompts/fix_verification_errors_LLM.prompt +8 -1
  36. pdd/prompts/generate_test_LLM.prompt +9 -3
  37. pdd/prompts/trim_results_start_LLM.prompt +1 -1
  38. pdd/prompts/update_prompt_LLM.prompt +3 -3
  39. pdd/split.py +6 -5
  40. pdd/split_main.py +13 -4
  41. pdd/trace_main.py +7 -0
  42. pdd/update_model_costs.py +446 -0
  43. pdd/xml_tagger.py +2 -1
  44. {pdd_cli-0.0.24.dist-info → pdd_cli-0.0.26.dist-info}/METADATA +8 -16
  45. {pdd_cli-0.0.24.dist-info → pdd_cli-0.0.26.dist-info}/RECORD +49 -47
  46. {pdd_cli-0.0.24.dist-info → pdd_cli-0.0.26.dist-info}/WHEEL +1 -1
  47. {pdd_cli-0.0.24.dist-info → pdd_cli-0.0.26.dist-info}/entry_points.txt +0 -0
  48. {pdd_cli-0.0.24.dist-info → pdd_cli-0.0.26.dist-info}/licenses/LICENSE +0 -0
  49. {pdd_cli-0.0.24.dist-info → pdd_cli-0.0.26.dist-info}/top_level.txt +0 -0
pdd/process_csv_change.py CHANGED
@@ -1,182 +1,494 @@
1
- # process_csv_change.py
2
-
3
- from typing import List, Dict, Tuple
4
- import os
5
1
  import csv
6
- from pathlib import Path
7
- import logging
2
+ import os
3
+ from typing import List, Dict, Tuple, Optional
8
4
 
9
5
  from rich.console import Console
10
- from rich.pretty import Pretty
11
- from rich.panel import Panel
12
6
 
13
- from .change import change # Relative import for the internal change function
7
+ # Use relative imports for internal modules within the package
8
+ from .change import change
9
+ from .get_extension import get_extension
10
+ # Assuming EXTRACTION_STRENGTH and DEFAULT_STRENGTH might be needed later,
11
+ # or just acknowledging their existence as per the prompt.
12
+ # from .. import EXTRACTION_STRENGTH, DEFAULT_STRENGTH
13
+
14
+ # No changes needed in the code_under_test based on these specific errors.
14
15
 
15
16
  console = Console()
16
17
 
17
- # Set up logging
18
- logging.basicConfig(level=logging.WARNING)
19
- logger = logging.getLogger(__name__)
18
+ def resolve_prompt_path(prompt_name: str, csv_file: str, code_directory: str) -> Optional[str]:
19
+ """
20
+ Attempts to find a prompt file by trying several possible locations.
21
+
22
+ Args:
23
+ prompt_name: The name or path of the prompt file from the CSV
24
+ csv_file: Path to the CSV file (for relative path resolution)
25
+ code_directory: Path to the code directory (as another potential source)
26
+
27
+ Returns:
28
+ Resolved path to the prompt file if found, None otherwise
29
+ """
30
+ # Ensure paths are absolute for reliable checking
31
+ abs_code_directory = os.path.abspath(code_directory)
32
+ abs_csv_dir = os.path.abspath(os.path.dirname(csv_file))
33
+ abs_cwd = os.path.abspath(os.getcwd())
34
+
35
+ # List of locations to try, in order of priority
36
+ possible_locations = [
37
+ prompt_name, # Try exactly as specified first (could be absolute or relative to CWD)
38
+ os.path.join(abs_cwd, os.path.basename(prompt_name)), # Try in current directory (basename only)
39
+ os.path.join(abs_csv_dir, os.path.basename(prompt_name)), # Try relative to CSV (basename only)
40
+ os.path.join(abs_code_directory, os.path.basename(prompt_name)), # Try in code directory (basename only)
41
+ os.path.join(abs_csv_dir, prompt_name), # Try relative to CSV (full prompt_name path)
42
+ os.path.join(abs_code_directory, prompt_name) # Try relative to code_dir (full prompt_name path)
43
+ ]
44
+
45
+ # Try each location, normalizing and checking existence/type
46
+ checked_locations = set()
47
+ for location in possible_locations:
48
+ try:
49
+ # Normalize path to handle relative parts like '.' or '..' and make absolute
50
+ normalized_location = os.path.abspath(location)
51
+ if normalized_location in checked_locations:
52
+ continue
53
+ checked_locations.add(normalized_location)
54
+
55
+ # Check if it exists and is a file
56
+ if os.path.exists(normalized_location) and os.path.isfile(normalized_location):
57
+ return normalized_location
58
+ except Exception:
59
+ # Ignore errors during path resolution (e.g., invalid characters)
60
+ continue
61
+
62
+ # If we get here, file was not found
63
+ return None
20
64
 
21
65
  def process_csv_change(
22
66
  csv_file: str,
23
67
  strength: float,
24
68
  temperature: float,
25
69
  code_directory: str,
26
- language: str,
27
- extension: str,
70
+ language: str, # Default language if not specified in prompt filename
71
+ extension: str, # Default extension (unused if language suffix found)
28
72
  budget: float
29
- ) -> Tuple[bool, List[Dict[str, str]], float, str]:
73
+ ) -> Tuple[bool, List[Dict[str, str]], float, Optional[str]]:
30
74
  """
31
- Processes a CSV file to apply changes to code prompts using an LLM model.
75
+ Reads a CSV file, processes each row to modify associated code files using an LLM,
76
+ and returns the results.
32
77
 
33
78
  Args:
34
- csv_file (str): Path to the CSV file containing 'prompt_name' and 'change_instructions' columns.
35
- strength (float): Strength parameter for the LLM model (0.0 to 1.0).
36
- temperature (float): Temperature parameter for the LLM model.
37
- code_directory (str): Path to the directory where code files are stored.
38
- language (str): Programming language of the code files.
39
- extension (str): File extension of the code files.
40
- budget (float): Maximum allowed total cost for the change process.
79
+ csv_file: Path to the input CSV file. Must contain 'prompt_name' and
80
+ 'change_instructions' columns.
81
+ strength: Strength parameter for the LLM model (0.0 to 1.0).
82
+ temperature: Temperature parameter for the LLM model (0.0 to 1.0).
83
+ code_directory: Path to the directory containing the code files.
84
+ language: Default programming language if the prompt filename doesn't
85
+ specify one (e.g., '_python').
86
+ extension: Default file extension (including '.') if language cannot be inferred.
87
+ Note: This is less likely to be used if `get_extension` covers the default language.
88
+ budget: Maximum allowed cost for all LLM operations. Must be non-negative.
41
89
 
42
90
  Returns:
43
- Tuple[bool, List[Dict[str, str]], float, str]:
44
- - success (bool): Indicates if changes were successfully made within the budget and without errors.
45
- - list_of_jsons (List[Dict[str, str]]): List of dictionaries with 'file_name' and 'modified_prompt'.
46
- - total_cost (float): Total accumulated cost of all change attempts.
47
- - model_name (str): Name of the LLM model used.
91
+ A tuple containing:
92
+ - success (bool): True if all rows attempted were processed without errors
93
+ (even if skipped due to missing data) and budget was not exceeded.
94
+ False otherwise (including partial success due to budget or errors).
95
+ - list_of_jsons (List[Dict[str, str]]): A list of dictionaries, where each
96
+ dictionary contains 'file_name' (original prompt name from CSV) and 'modified_prompt'.
97
+ - total_cost (float): The total cost incurred for the LLM operations.
98
+ - model_name (Optional[str]): The name of the LLM model used for the first successful change.
99
+ Returns "N/A" if no changes were successfully processed.
100
+ Returns None if an input validation error occurred before processing.
48
101
  """
49
102
  list_of_jsons: List[Dict[str, str]] = []
50
103
  total_cost: float = 0.0
51
- model_name: str = ""
52
- success: bool = False
53
- any_failures: bool = False # Track if any failures occur
54
-
55
- # Validate inputs
56
- if not os.path.isfile(csv_file):
57
- console.print(f"[bold red]Error:[/bold red] CSV file '{csv_file}' does not exist.")
58
- return success, list_of_jsons, total_cost, model_name
59
-
60
- if not (0.0 <= strength <= 1.0):
61
- console.print(f"[bold red]Error:[/bold red] 'strength' must be between 0 and 1. Given: {strength}")
62
- return success, list_of_jsons, total_cost, model_name
63
-
64
- if not (0.0 <= temperature <= 1.0):
65
- console.print(f"[bold red]Error:[/bold red] 'temperature' must be between 0 and 1. Given: {temperature}")
66
- return success, list_of_jsons, total_cost, model_name
67
-
68
- code_dir_path = Path(code_directory)
69
- if not code_dir_path.is_dir():
70
- console.print(f"[bold red]Error:[/bold red] Code directory '{code_directory}' does not exist or is not a directory.")
71
- return success, list_of_jsons, total_cost, model_name
104
+ model_name: Optional[str] = None
105
+ overall_success: bool = True # Assume success until an error occurs or budget exceeded
106
+
107
+ # --- Input Validation ---
108
+ if not os.path.exists(csv_file) or not os.path.isfile(csv_file): # Check it's a file too
109
+ console.print(f"[bold red]Error:[/bold red] CSV file not found or is not a file: '{csv_file}'")
110
+ return False, [], 0.0, None # Return None for model_name on early exit
111
+ if not os.path.isdir(code_directory):
112
+ console.print(f"[bold red]Error:[/bold red] Code directory not found or is not a directory: '{code_directory}'")
113
+ return False, [], 0.0, None # Return None for model_name on early exit
114
+ if not 0.0 <= strength <= 1.0:
115
+ console.print(f"[bold red]Error:[/bold red] 'strength' must be between 0.0 and 1.0. Given: {strength}")
116
+ return False, [], 0.0, None # Return None for model_name on early exit
117
+ if not 0.0 <= temperature <= 1.0: # Added temperature validation (assuming 0-1 range)
118
+ console.print(f"[bold red]Error:[/bold red] 'temperature' must be between 0.0 and 1.0. Given: {temperature}")
119
+ return False, [], 0.0, None # Return None for model_name on early exit
120
+ if budget < 0.0:
121
+ console.print(f"[bold red]Error:[/bold red] 'budget' must be non-negative. Given: {budget}")
122
+ return False, [], 0.0, None # Return None for model_name on early exit
123
+ # --- End Input Validation ---
124
+
125
+ console.print(f"[cyan]Starting CSV processing:[/cyan] '{os.path.abspath(csv_file)}'")
126
+ console.print(f"[cyan]Code directory:[/cyan] '{os.path.abspath(code_directory)}'")
127
+ console.print(f"[cyan]Budget:[/cyan] ${budget:.2f}")
128
+
129
+ processed_rows = 0
130
+ successful_changes = 0
72
131
 
73
132
  try:
74
- with open(csv_file, mode='r', newline='', encoding='utf-8') as csvfile:
75
- reader = csv.DictReader(csvfile)
76
- if 'prompt_name' not in reader.fieldnames or 'change_instructions' not in reader.fieldnames:
77
- console.print("[bold red]Error:[/bold red] CSV file must contain 'prompt_name' and 'change_instructions' columns.")
78
- return success, list_of_jsons, total_cost, model_name
79
-
80
- for row_number, row in enumerate(reader, start=1):
81
- prompt_name = row.get('prompt_name', '').strip()
82
- change_instructions = row.get('change_instructions', '').strip()
83
-
84
- if not prompt_name:
85
- console.print(f"[yellow]Warning:[/yellow] Missing 'prompt_name' in row {row_number}. Skipping.")
86
- any_failures = True
87
- continue
88
-
89
- if not change_instructions:
90
- console.print(f"[yellow]Warning:[/yellow] Missing 'change_instructions' in row {row_number}. Skipping.")
91
- any_failures = True
92
- continue
93
-
94
- # Parse the prompt_name to get the input_code filename
95
- try:
96
- prompt_path = Path(prompt_name)
97
- base_name = prompt_path.stem # Removes suffix
98
- # Remove the '_<language>' part if present
99
- if '_' in base_name:
100
- base_name = base_name.rsplit('_', 1)[0]
101
- input_code_name = f"{base_name}{extension}"
102
- input_code_path = code_dir_path / input_code_name
103
-
104
- if not input_code_path.is_file():
105
- console.print(f"[yellow]Warning:[/yellow] Input code file '{input_code_path}' does not exist. Skipping row {row_number}.")
106
- logger.warning(f"Input code file '{input_code_path}' does not exist for row {row_number}")
107
- any_failures = True
133
+ header_valid = True
134
+ with open(csv_file, mode='r', newline='', encoding='utf-8') as file:
135
+ header_valid = True # Flag to track header status
136
+ reader = None # Initialize reader to None
137
+
138
+ # Read the header line manually
139
+ header_line = file.readline()
140
+ if not header_line:
141
+ # Handle empty file
142
+ console.print("[yellow]Warning:[/yellow] CSV file is empty.")
143
+ header_valid = False # Treat as invalid header for flow control
144
+ # No rows will be processed, overall_success remains True initially
145
+ else:
146
+ # Parse the header line
147
+ actual_fieldnames = [col.strip() for col in header_line.strip().split(',')]
148
+
149
+ # Check if required columns are present
150
+ required_cols = {'prompt_name', 'change_instructions'}
151
+ missing_cols = required_cols - set(actual_fieldnames)
152
+ if missing_cols:
153
+ console.print(f"[bold red]Error:[/bold red] CSV file must contain 'prompt_name' and 'change_instructions' columns. Missing: {missing_cols}")
154
+ overall_success = False # Mark overall failure
155
+ header_valid = False # Mark header as invalid
156
+ else:
157
+ # Header is valid and has required columns, initialize DictReader for remaining lines
158
+ reader = csv.DictReader(file, fieldnames=actual_fieldnames)
159
+
160
+ # Only loop if the header was valid and reader was initialized
161
+ if header_valid and reader:
162
+ for i, row in enumerate(reader):
163
+ row_num = i + 1
164
+ processed_rows += 1
165
+ console.print(f"\n[cyan]Processing row {row_num}...[/cyan]")
166
+
167
+ prompt_name_from_csv = row.get('prompt_name', '').strip()
168
+ change_instructions = row.get('change_instructions', '').strip()
169
+
170
+ if not prompt_name_from_csv:
171
+ console.print(f"[bold yellow]Warning:[/bold yellow] Missing 'prompt_name' in row {row_num}. Skipping.")
172
+ overall_success = False # Mark as not fully successful if skips occur
108
173
  continue
109
-
110
- # Check if prompt file exists
111
- if not prompt_path.is_file():
112
- console.print(f"[yellow]Warning:[/yellow] Prompt file '{prompt_name}' does not exist. Skipping row {row_number}.")
113
- logger.warning(f"Prompt file '{prompt_name}' does not exist for row {row_number}")
114
- any_failures = True
174
+ if not change_instructions:
175
+ console.print(f"[bold yellow]Warning:[/bold yellow] Missing 'change_instructions' in row {row_num}. Skipping.")
176
+ overall_success = False # Mark as not fully successful if skips occur
177
+ continue
178
+
179
+ # Try to resolve the prompt file path
180
+ resolved_prompt_path = resolve_prompt_path(prompt_name_from_csv, csv_file, code_directory)
181
+ if not resolved_prompt_path:
182
+ console.print(f"[bold red]Error:[/bold red] Prompt file for '{prompt_name_from_csv}' not found in any location (row {row_num}).")
183
+ console.print(f" [dim]Searched: as is, CWD, CSV dir, code dir (using basename and full name)[/dim]")
184
+ overall_success = False
115
185
  continue
116
186
 
117
- # Read the input_code from the file
118
- with open(input_code_path, 'r', encoding='utf-8') as code_file:
119
- input_code = code_file.read()
120
-
121
- # Read the input_prompt from the prompt file
122
- with open(prompt_path, 'r', encoding='utf-8') as prompt_file:
123
- input_prompt = prompt_file.read()
124
-
125
- # Call the change function
126
- modified_prompt, cost, current_model_name = change(
127
- input_prompt=input_prompt,
128
- input_code=input_code,
129
- change_prompt=change_instructions,
130
- strength=strength,
131
- temperature=temperature
132
- )
133
-
134
- # Accumulate the total cost
135
- total_cost += cost
136
-
137
- # Check if budget is exceeded
138
- if total_cost > budget:
139
- console.print(f"[bold red]Budget exceeded after row {row_number}. Stopping further processing.[/bold red]")
140
- any_failures = True
141
- break
142
-
143
- # Set the model_name (assumes all calls use the same model)
144
- if not model_name:
145
- model_name = current_model_name
146
- elif model_name != current_model_name:
147
- console.print(f"[yellow]Warning:[/yellow] Model name changed from '{model_name}' to '{current_model_name}' in row {row_number}.")
148
-
149
- # Add to the list_of_jsons
150
- list_of_jsons.append({
151
- "file_name": prompt_name,
152
- "modified_prompt": modified_prompt
153
- })
154
-
155
- console.print(Panel(f"[green]Row {row_number} processed successfully.[/green]"))
156
-
157
- except Exception as e:
158
- console.print(f"[red]Error:[/red] Failed to process 'prompt_name' in row {row_number}: {str(e)}")
159
- logger.exception(f"Failed to process row {row_number}")
160
- any_failures = True
161
- continue
162
-
163
- # Determine success based on whether total_cost is within budget and no failures occurred
164
- success = (total_cost <= budget) and not any_failures
165
-
166
- # Pretty print the results
167
- console.print(Panel(f"[bold]Processing Complete[/bold]\n"
168
- f"Success: {'Yes' if success else 'No'}\n"
169
- f"Total Cost: ${total_cost:.6f}\n"
170
- f"Model Used: {model_name if model_name else 'N/A'}"))
171
-
172
- # Optionally, pretty print the list of modified prompts
173
- if list_of_jsons:
174
- console.print(Panel("[bold]List of Modified Prompts[/bold]"))
175
- console.print(Pretty(list_of_jsons))
176
-
177
- return success, list_of_jsons, total_cost, model_name
178
-
187
+ console.print(f" [dim]Prompt name from CSV:[/dim] {prompt_name_from_csv}")
188
+ console.print(f" [dim]Resolved prompt path:[/dim] {resolved_prompt_path}")
189
+
190
+ # --- Step 2a: Initialize variables ---
191
+ input_prompt: Optional[str] = None
192
+ input_code: Optional[str] = None
193
+ input_code_path: Optional[str] = None
194
+
195
+ # Read the input prompt from the resolved path
196
+ try:
197
+ with open(resolved_prompt_path, 'r', encoding='utf-8') as f:
198
+ input_prompt = f.read()
199
+ except IOError as e:
200
+ console.print(f"[bold red]Error:[/bold red] Could not read prompt file '{resolved_prompt_path}' (row {row_num}): {e}")
201
+ overall_success = False
202
+ continue # Skip to next row
203
+
204
+ # Parse prompt_name to determine input_code_name
205
+ try:
206
+ # i. remove the path and suffix _language.prompt from the prompt_name
207
+ prompt_filename = os.path.basename(resolved_prompt_path) # Use basename of resolved path
208
+ base_name, ext = os.path.splitext(prompt_filename) # Removes .prompt (or other ext)
209
+
210
+ # Ensure it actually ends with .prompt before stripping language
211
+ if ext.lower() != '.prompt':
212
+ console.print(f"[bold yellow]Warning:[/bold yellow] Prompt file '{prompt_filename}' does not end with '.prompt'. Attempting to parse language anyway (row {row_num}).")
213
+ # Keep base_name as is, don't assume .prompt was the only extension part
214
+
215
+ file_stem = base_name
216
+ actual_language = language # Default language
217
+ language_from_suffix = False # Track if language came from suffix
218
+
219
+ # Check for _language suffix
220
+ if '_' in base_name:
221
+ parts = base_name.rsplit('_', 1)
222
+ # Check if the suffix looks like a language identifier (simple check: alpha only)
223
+ if len(parts) == 2 and parts[1].isalpha():
224
+ file_stem = parts[0]
225
+ # Use capitalize for consistency, matching get_extension examples
226
+ actual_language = parts[1].capitalize()
227
+ language_from_suffix = True # Set flag
228
+ console.print(f" [dim]Inferred language from filename:[/dim] {actual_language}")
229
+ else:
230
+ console.print(f" [dim]Suffix '_{parts[1]}' not recognized as language, using default:[/dim] {language}")
231
+ else:
232
+ console.print(f" [dim]Using default language:[/dim] {language}")
233
+
234
+
235
+ # ii. use get_extension to infer the extension
236
+ try:
237
+ # print(f"DEBUG: Trying get_extension for language: '{actual_language}'") # Keep commented
238
+ # Use the capitalized version for lookup
239
+ code_extension = get_extension(actual_language.capitalize())
240
+ console.print(f" [dim]Inferred extension for {actual_language}:[/dim] '{code_extension}'")
241
+ except ValueError: # Handle case where get_extension doesn't know the language
242
+ # print(f"DEBUG: get_extension failed. Falling back to default extension parameter: '{extension}'") # Keep commented
243
+ if language_from_suffix:
244
+ # Suffix was present but get_extension failed for it! Error out for this row.
245
+ console.print(f"[bold red]Error:[/bold red] Language '{actual_language}' found in prompt suffix, but its extension is unknown (row {row_num}). Skipping.")
246
+ overall_success = False # Mark failure
247
+ continue # Skip to next row
248
+ else:
249
+ # No suffix, and get_extension failed for the default language.
250
+ # Fallback to the 'extension' parameter as a last resort (current behavior).
251
+ console.print(f"[bold yellow]Warning:[/bold yellow] Could not determine extension for default language '{actual_language}'. Using default extension parameter '{extension}' (row {row_num}).")
252
+ code_extension = extension # Fallback to the provided default extension parameter
253
+ # Do not mark overall_success as False for this warning, it's a fallback mechanism
254
+ # print(f"DEBUG: Determined code extension: '{code_extension}'") # Keep commented
255
+
256
+ # iii. add the suffix extension to the prompt_name (stem)
257
+ input_code_filename = file_stem + code_extension
258
+
259
+ # iv. Construct code file path: place it directly in code_directory.
260
+ input_code_path = os.path.join(code_directory, input_code_filename)
261
+ console.print(f" [dim]Derived target code path:[/dim] {input_code_path}")
262
+ print(f"DEBUG: Attempting to access code file path: '{input_code_path}'") # Added log
263
+
264
+
265
+ # Read the input code from the input_code_path
266
+ if not os.path.exists(input_code_path) or not os.path.isfile(input_code_path):
267
+ console.print(f"[bold red]Error:[/bold red] Derived code file not found or is not a file: '{input_code_path}' (row {row_num})")
268
+ overall_success = False
269
+ continue # Skip to next row
270
+ with open(input_code_path, 'r', encoding='utf-8') as f:
271
+ input_code = f.read()
272
+
273
+ except Exception as e:
274
+ console.print(f"[bold red]Error:[/bold red] Failed to parse filenames or read code file for row {row_num}: {e}")
275
+ overall_success = False
276
+ continue # Skip to next row
277
+
278
+ # Ensure we have all necessary components before calling change
279
+ # (Should be guaranteed by checks above, but added defensively)
280
+ if input_prompt is None or input_code is None or change_instructions is None:
281
+ console.print(f"[bold red]Internal Error:[/bold red] Missing required data (prompt, code, or instructions) for row {row_num}. Skipping.")
282
+ overall_success = False
283
+ continue
284
+
285
+ # --- Step 2b: Call the change function ---
286
+ try:
287
+ # Check budget *before* making the potentially expensive call
288
+ if total_cost >= budget:
289
+ console.print(f"[bold yellow]Warning:[/bold yellow] Budget (${budget:.2f}) already met or exceeded before processing row {row_num}. Stopping.")
290
+ overall_success = False # Mark as incomplete due to budget
291
+ break # Exit the loop
292
+
293
+ console.print(f" [dim]Calling LLM for change... (Budget remaining: ${budget - total_cost:.2f})[/dim]")
294
+ modified_prompt, cost, current_model_name = change(
295
+ input_prompt=input_prompt,
296
+ input_code=input_code,
297
+ change_prompt=change_instructions,
298
+ strength=strength,
299
+ temperature=temperature
300
+ )
301
+ console.print(f" [dim]Change cost:[/dim] ${cost:.6f}")
302
+ console.print(f" [dim]Model used:[/dim] {current_model_name}")
303
+
304
+ # --- Step 2c: Add cost ---
305
+ new_total_cost = total_cost + cost
306
+
307
+ # --- Step 2d: Check budget *after* call ---
308
+ console.print(f" [dim]Cumulative cost:[/dim] ${new_total_cost:.6f} / ${budget:.2f}")
309
+ if new_total_cost > budget:
310
+ console.print(f"[bold yellow]Warning:[/bold yellow] Budget exceeded (${budget:.2f}) after processing row {row_num}. Change from this row NOT saved. Stopping.")
311
+ total_cost = new_total_cost # Record the cost even if result isn't saved
312
+ overall_success = False # Mark as incomplete due to budget
313
+ break # Exit the loop
314
+ else:
315
+ total_cost = new_total_cost # Update cost only if within budget
316
+
317
+ # --- Step 2e: Add successful result ---
318
+ # Capture model name on first successful call within budget
319
+ if model_name is None and current_model_name:
320
+ model_name = current_model_name
321
+ # Warn if model name changes on subsequent calls
322
+ elif current_model_name and model_name != current_model_name:
323
+ console.print(f"[bold yellow]Warning:[/bold yellow] Model name changed from '{model_name}' to '{current_model_name}' in row {row_num}.")
324
+ # Keep the first model_name
325
+
326
+ list_of_jsons.append({
327
+ "file_name": prompt_name_from_csv, # Use original prompt name from CSV as key
328
+ "modified_prompt": modified_prompt
329
+ })
330
+ successful_changes += 1
331
+ console.print(f" [green]Successfully processed change for:[/green] {prompt_name_from_csv}")
332
+
333
+
334
+ except Exception as e:
335
+ console.print(f"[bold red]Error:[/bold red] Failed during 'change' call for '{prompt_name_from_csv}' (row {row_num}): {e}")
336
+ overall_success = False
337
+ # Continue to the next row even if one fails
338
+
339
+ except FileNotFoundError:
340
+ # This case should be caught by the initial validation, but included for robustness
341
+ console.print(f"[bold red]Error:[/bold red] CSV file not found at '{csv_file}'")
342
+ return False, [], 0.0, None
343
+ except IOError as e:
344
+ console.print(f"[bold red]Error:[/bold red] Could not read CSV file '{csv_file}': {e}")
345
+ return False, [], 0.0, None
179
346
  except Exception as e:
180
- console.print(f"[bold red]Unexpected Error:[/bold red] {str(e)}")
181
- logger.exception("Unexpected error occurred")
182
- return success, list_of_jsons, total_cost, model_name
347
+ console.print(f"[bold red]An unexpected error occurred during CSV processing:[/bold red] {e}")
348
+ # Return potentially partial results, but mark as failure
349
+ return False, list_of_jsons, total_cost, model_name if model_name else "N/A"
350
+
351
+ # --- Step 3: Return results ---
352
+ console.print("\n[bold cyan]=== Processing Summary ===[/bold cyan]")
353
+ if processed_rows == 0 and overall_success:
354
+ # This case is handled by the empty file check earlier, but keep for clarity
355
+ console.print("[yellow]No rows found in CSV file.[/yellow]")
356
+ elif not overall_success:
357
+ console.print("[yellow]Processing finished with errors, skips, or budget exceeded.[/yellow]")
358
+ else:
359
+ console.print("[green]CSV processing finished successfully.[/green]")
360
+
361
+ console.print(f"[bold]Total Rows Processed:[/bold] {processed_rows}")
362
+ console.print(f"[bold]Successful Changes:[/bold] {successful_changes}")
363
+ console.print(f"[bold]Total Cost:[/bold] ${total_cost:.6f}")
364
+ console.print(f"[bold]Model Used (first success):[/bold] {model_name if model_name else 'N/A'}")
365
+ console.print(f"[bold]Overall Success Status:[/bold] {overall_success}")
366
+
367
+
368
+ # --- Summary printing block should be right above here ---
369
+
370
+ final_model_name = model_name if model_name else "N/A"
371
+ # If overall_success is False AND header_valid is False, it means we failed on header validation.
372
+ if not overall_success and not header_valid:
373
+ final_model_name = None
374
+
375
+ return overall_success, list_of_jsons, total_cost, final_model_name
376
+
377
+
378
+ # Example usage (assuming this file is part of a package structure)
379
+ # Keep the example usage block as is for basic testing/demonstration
380
+ if __name__ == '__main__':
381
+ # This block is for demonstration/testing purposes.
382
+ # In a real package, you'd import and call process_csv_change from another module.
383
+
384
+ # Create dummy files and directories for testing
385
+ if not os.path.exists("temp_code_dir"):
386
+ os.makedirs("temp_code_dir")
387
+ if not os.path.exists("temp_prompt_dir"):
388
+ os.makedirs("temp_prompt_dir")
389
+
390
+ # Dummy CSV
391
+ csv_content = """prompt_name,change_instructions
392
+ temp_prompt_dir/func1_python.prompt,"Add error handling for negative numbers"
393
+ temp_prompt_dir/script2_javascript.prompt,"Convert to async/await"
394
+ temp_prompt_dir/invalid_file.prompt,"This will fail code file lookup"
395
+ temp_prompt_dir/config_yaml.prompt,"Increase timeout value"
396
+ temp_prompt_dir/missing_instr.prompt,
397
+ missing_prompt.prompt,"This prompt file won't be found"
398
+ temp_prompt_dir/budget_breaker_python.prompt,"This might break the budget"
399
+ """
400
+ with open("temp_changes.csv", "w") as f:
401
+ f.write(csv_content)
402
+
403
+ # Dummy prompt files
404
+ with open("temp_prompt_dir/func1_python.prompt", "w") as f:
405
+ f.write("Create a Python function for factorial.")
406
+ with open("temp_prompt_dir/script2_javascript.prompt", "w") as f:
407
+ f.write("Write a JS script using callbacks.")
408
+ with open("temp_prompt_dir/invalid_file.prompt", "w") as f: # Code file missing
409
+ f.write("Some prompt.")
410
+ with open("temp_prompt_dir/config_yaml.prompt", "w") as f:
411
+ f.write("Describe the YAML config.")
412
+ with open("temp_prompt_dir/missing_instr.prompt", "w") as f: # Instructions missing in CSV
413
+ f.write("Prompt with missing instructions.")
414
+ # missing_prompt.prompt does not exist
415
+ with open("temp_prompt_dir/budget_breaker_python.prompt", "w") as f:
416
+ f.write("Prompt for budget breaker.")
417
+
418
+
419
+ # Dummy code files
420
+ with open("temp_code_dir/func1.py", "w") as f:
421
+ f.write("def factorial(n):\n if n == 0: return 1\n return n * factorial(n-1)\n")
422
+ with open("temp_code_dir/script2.js", "w") as f:
423
+ f.write("function fetchData(url, callback) { /* ... */ }")
424
+ # No code file for invalid_file
425
+ with open("temp_code_dir/config.yaml", "w") as f:
426
+ f.write("timeout: 10s\nretries: 3\n")
427
+ with open("temp_code_dir/budget_breaker.py", "w") as f:
428
+ f.write("print('Hello')")
429
+
430
+
431
+ # Dummy internal modules (replace with actual imports if running within package)
432
+ # Mocking the internal functions for standalone execution
433
+ def mock_change(input_prompt, input_code, change_prompt, strength, temperature):
434
+ # Simulate cost and model name
435
+ cost = 0.01 + (0.0001 * len(change_prompt))
436
+ model = "mock-gpt-4"
437
+ # Simulate success or failure based on input
438
+ if "invalid" in input_prompt.lower():
439
+ raise ValueError("Simulated model error for invalid input.")
440
+ modified_prompt = f"MODIFIED: {input_prompt[:30]}... based on '{change_prompt[:30]}...'"
441
+ return modified_prompt, cost, model
442
+
443
+ def mock_get_extension(language_name):
444
+ lang_map = {
445
+ "Python": ".py",
446
+ "Javascript": ".js", # Note: Case difference from prompt example
447
+ "Yaml": ".yaml",
448
+ "Makefile": "" # Example from prompt
449
+ }
450
+ # Match behavior of raising ValueError if unknown, case-sensitive
451
+ if language_name in lang_map:
452
+ return lang_map[language_name]
453
+ else:
454
+ raise ValueError(f"Unknown language: {language_name}")
455
+
456
+ # Replace the actual imports with mocks for the example
457
+ change_original = change
458
+ get_extension_original = get_extension
459
+ change = mock_change
460
+ get_extension = mock_get_extension
461
+
462
+ console.print("\n[bold magenta]=== Running Example Usage ===[/bold magenta]")
463
+
464
+ # Call the function
465
+ success_status, results, final_cost, final_model = process_csv_change(
466
+ csv_file="temp_changes.csv",
467
+ strength=0.6,
468
+ temperature=0.1,
469
+ code_directory="temp_code_dir",
470
+ language="UnknownLang", # Default language
471
+ extension=".txt", # Default extension (used if get_extension fails)
472
+ budget=0.05 # Set a budget likely to be exceeded
473
+ )
474
+
475
+ console.print("\n[bold magenta]=== Example Usage Results ===[/bold magenta]")
476
+ # console.print(f"Overall Success: {success_status}") # Printed in summary now
477
+ # console.print(f"Total Cost: ${final_cost:.6f}") # Printed in summary now
478
+ # console.print(f"Model Name: {final_model}") # Printed in summary now
479
+ console.print("Modified Prompts JSON (results list):")
480
+ console.print(results)
481
+
482
+ # Restore original functions if needed elsewhere
483
+ change = change_original
484
+ get_extension = get_extension_original
485
+
486
+ # Cleanup dummy files (optional)
487
+ # import shutil
488
+ # try:
489
+ # os.remove("temp_changes.csv")
490
+ # shutil.rmtree("temp_code_dir")
491
+ # shutil.rmtree("temp_prompt_dir")
492
+ # console.print("\n[bold magenta]=== Cleaned up temporary files ===[/bold magenta]")
493
+ # except OSError as e:
494
+ # console.print(f"\n[yellow]Warning: Could not clean up all temp files: {e}[/yellow]")