from typing import Dict, List, Set, Optional from dataclasses import dataclass from enum import Enum import openai from contextlib import contextmanager import re import json @dataclass class UIComponent: name: str type: str config: Dict @dataclass class PromptConfig: name: str inputs: List[str] outputs: List[str] ui_components: Dict[str, UIComponent] step: Optional[str] = None class ChangeType(Enum): ADD_PROMPT = "add_prompt" REMOVE_PROMPT = "remove_prompt" RENAME_PROMPT = "rename_prompt" ADD_INPUT = "add_input" REMOVE_INPUT = "remove_input" RENAME_INPUT = "rename_input" ADD_OUTPUT = "add_output" REMOVE_OUTPUT = "remove_output" RENAME_OUTPUT = "rename_output" ADD_UI = "add_ui" REMOVE_UI = "remove_ui" RENAME_UI = "rename_ui" CHANGE_STEP = "change_step" @dataclass class ConfigChange: type: ChangeType prompt_name: str old_value: Optional[str] = None new_value: Optional[str] = None details: Optional[Dict] = None @dataclass class AnalysisResult: changes: List[ConfigChange] affected_files: Set[str] required_updates: Dict[str, List[Dict]] @contextmanager def openai_session(): """Context manager to properly handle OpenAI API sessions""" try: client = openai.OpenAI() yield client finally: if hasattr(client, 'close'): client.close() def call_o1_mini(prompt: str) -> str: """Call the o1-mini model with the given prompt""" with openai_session() as client: try: response = client.chat.completions.create( model="o1-mini", messages=[{"role": "user", "content": prompt}] ) return response.choices[0].message.content except Exception as e: return f"Error generating output: {str(e)}" def clean_ai_response(response: str) -> str: """Clean the AI response to ensure valid JSON""" try: # Remove any potential markdown code block markers response = re.sub(r'```json\s*', '', response) response = re.sub(r'```\s*$', '', response) # Remove any leading/trailing whitespace response = response.strip() # Attempt to parse JSON to validate json.loads(response) return response except json.JSONDecodeError as e: print(f"Error cleaning response: {str(e)}") print("Raw response:", response) return "{}" def analyze_config_structure(config_content: str, project_content: str, current_handlers: str) -> Dict: """First AI: Analyzes the current config structure and determines required changes""" analysis_prompt = f""" You are an expert software engineer performing a comprehensive analysis.You must analyze the current state of the code WITHOUT relying on any previous context or memory. IMPORTANT RULES: 1. ONLY analyze the code provided in this prompt 2. DO NOT reference or recall any previous analyses 3. DO NOT suggest changes based on previous versions 4. ONLY compare against the current files provided 5. IGNORE any remembered patterns or previous suggestions 6. The config file (page_prompts_config.py) is the source of truth for naming conventions 7. Component names MUST match their corresponding config names EXACTLY Your task is to analyze and focus on the configuration structure and validate the relationships between components. 1. OUTPUT AND INPUT VALIDATION RULES: - Not all outputs/inputs require UI component mappings - Special outputs like 'quotation_cost' can be internal values used as inputs and outputs in other steps - Internal values can be passed between steps without UI components , so just DO NOT suggest any changes for them. - Examples of internal values: * quotation_cost * project_detail * gathered_project_input * reviewed_project_input * client_follow_up_questions * follow_up_questions * client_followup_question_sample_answers 2. OUTPUT TO UI COMPONENT MAPPING RULES: - Most outputs in config map to specific UI components - Standard Output Pattern: * Base output name MUST map to TWO components: - base_name_text: For textbox display - base_name_markdown: For markdown display - Special Case - Mandays Pattern: * Mandays outputs MUST map to ONE component: - base_name_dataframe: For dataframe display 3. INPUT TO UI COMPONENT MAPPING RULES: - Only UI-interactive inputs need component mappings - When needed, inputs MUST reference either the _text or _markdown variant 4. NAMING CONVENTION (Source of Truth: Config File) MUST CHECK: - Prompt names - Component names - Input/Output references WHERE TO CHECK: - event_handlers.py: Function names, component references - Project.py: Method names, mappings - all_components dictionary: Component keys 5. HANDLER VALIDATION RULES: - Each step handler must correctly map: * Inputs to component _text or _markdown * Outputs to both _text and _markdown (or _dataframe for mandays) - all_components dictionary keys must exactly match config naming - Function parameters should align with Project.py signatures 6. PROMPTCONFIG CHANGES: A. REMOVED PROMPTCONFIGS: - When a PromptConfig is removed from config: * ALL references to that prompt must be removed from Project.py * ALL UI components associated with that prompt must be removed from event_handlers.py * ALL method signatures and return values must be updated accordingly * ALL dependent code that referenced the removed components must be updated - Check for: * Orphaned UI components * Unused function parameters * Outdated return values * Broken event handler mappings B. NEW PROMPTCONFIGS: - When a new PromptConfig is added to config: * Add corresponding execute_prompt() call in Project.py * Add UI component references in event_handlers.py * Add new step button handler if step is specified * Add necessary input/output mappings - Check for: * Required method signatures * Input/output component creation * Event handler connections * Step button integration * State management updates Context Files: 1. Current Config: {config_content} 2. Project.py: {project_content} 3. event_handlers.py: {current_handlers} CRITICAL ANALYSIS RULES: 1. RENAMED PROMPTS: - First check if a component missing from config exists with a similar name - Compare component names using similarity matching (e.g., generate_Tech_SOW vs generate_Technical_SOW) - If a similar name exists in config, treat it as a rename operation - Add it to the 'renamed_prompts' section with old and new names - Do NOT mark it as a removed prompt 2. REMOVED PROMPTS: - Only mark a prompt as removed if NO similar name exists in config - If unsure, prefer rename over removal - For confirmed removals: * List ALL code locations that need cleanup * Include exact file paths and line numbers if possible * Specify what code needs to be removed 3. NAMING UPDATES: - When a component name changes in config: * Update ALL references to match exactly * Maintain consistent casing and formatting * Update both component names and method references Return a JSON analysis with this EXACT structure as shown as an example below: {{ "component_mappings": {{ "outputs": [ {{ "config_name": "base_output_name", "required_components": ["component1", "component2"], "current_components": ["existing1", "existing2"], "is_valid": false, "issues": ["detailed_issue_description"] }} ], "inputs": [ {{ "config_name": "input_name", "required_component": "required_component_name", "current_component": "current_component_name", "is_valid": false, "issues": ["detailed_issue_description"] }} ], "buttons": [ {{ "button_id": "button_name", "type": "step|action|recalculate|upload", "handler": "handler_function_name", "inputs": ["input1", "input2"], "outputs": ["output1", "output2"], "state_updates": ["state1", "state2"], "is_valid": false, "issues": ["detailed_issue_description"] }} ] }}, "event_handlers": {{ "handlers": [ {{ "button_id": "button_name", "type": "step|recalculate|upload", "input_mappings": {{ "is_valid": false, "issues": ["detailed_issue_description"] }}, "output_mappings": {{ "is_valid": false, "issues": ["detailed_issue_description"] }}, "state_mappings": {{ "is_valid": false, "issues": ["detailed_issue_description"] }} }} ] }}, "prompt_changes": {{ "removed_prompts": [ {{ "prompt_name": "name_of_removed_prompt", "affected_components": ["component1", "component2"], "affected_handlers": ["handler1", "handler2"], "cleanup_required": {{ "Project.py": [ {{ "type": "method_removal|signature_update", "location": "exact_location", "code_to_remove": "code_snippet", "reason": "detailed_explanation" }} ], "event_handlers.py": [ {{ "type": "component_removal|handler_update", "location": "exact_location", "code_to_remove": "code_snippet", "reason": "detailed_explanation" }} ] }} }} ], "renamed_prompts": [ {{ "old_name": "old_prompt_name", "new_name": "new_prompt_name", "affected_files": {{ "Project.py": [ {{ "type": "method_rename", "location": "method references", "old_code": "old_code_snippet", "new_code": "new_code_snippet", "reason": "Prompt name updated in config" }} ], "event_handlers.py": [ {{ "type": "component_rename", "location": "component references", "old_code": "old_code_snippet", "new_code": "new_code_snippet", "reason": "Prompt name updated in config" }} ] }} }} ] "new_prompts": [ {{ "prompt_name": "name_of_new_prompt", "required_components": ["component1", "component2"], "required_handlers": ["handler1", "handler2"], "additions_required": {{ "Project.py": [ {{ "type": "method_addition|signature_update", "location": "exact_location", "code_to_add": "code_snippet", "reason": "detailed_explanation" }} ], "event_handlers.py": [ {{ "type": "component_addition|handler_creation", "location": "exact_location", "code_to_add": "code_snippet", "reason": "detailed_explanation" }} ] }} }} ] }}, "required_updates": {{ "Project.py": [ {{ "type": "method|mapping", "location": "exact_location", "reason": "detailed_explanation" }} ], "event_handlers.py": [ {{ "button_id": "button_name", "type": "step|recalculate|upload", "current": "current_code", "required": "required_code", "reason": "detailed_explanation" }} ] }} }} IMPORTANT: 1. Validate EVERY output has correct component mappings 2. Check EVERY input references correct component variant 3. Verify ALL component names match exactly 4. Flag ANY case mismatches or naming inconsistencies 5. Return ONLY the JSON object, no additional text """ try: result = call_o1_mini(analysis_prompt) cleaned_result = clean_ai_response(result) return json.loads(cleaned_result) except Exception as e: print(f"Error in analysis: {str(e)}") print("Raw response:", result) return { "component_mappings": {"outputs": [], "inputs": []}, "event_handlers": {"steps": []}, "required_updates": {} } def generate_code_updates(analysis_result: Dict) -> Dict: """Second AI: Generates specific code updates based on the analysis""" code_generation_prompt = f""" You are an expert code generator. Based on this analysis, generate EXACT code updates. Analysis Result: {json.dumps(analysis_result, indent=2)} CRITICAL RULES: 1. REMOVED PROMPTS: When a prompt is removed from config: - REMOVE all related component references - REMOVE all related handler code - DO NOT suggest adding new components - DO NOT suggest new mappings 2. NEW PROMPTS: When a new prompt is added to config: - ADD corresponding execute_prompt() method in Project.py - ADD UI component references in event_handlers.py - ADD new step button handler if step is specified - ADD necessary input/output mappings - FOLLOW exact naming from config - ENSURE all required components are created 3. Processing Order: a) Process removals FIRST (prompt_changes.removed_prompts) b) Then process additions (prompt_changes.new_prompts) c) Finally process other updates Generate a JSON response with this EXACT structure: {{ "Project.py": {{ "updates": [ {{ "type": "removal|addition|update", # Must specify type "location": "exact_location", "explanation": "Removing/Adding/Updating for prompt 'prompt_name'", "old_code": "existing code to remove or update", "new_code": "new code to add or update with", "template": {{ # Only for additions "method_signature": "def method_name(self, ...)", "docstring": "\"\"\"Method documentation\"\"\"", "implementation": "method implementation", "return_statement": "return statement" }} }} ] }}, "event_handlers.py": {{ "updates": [ {{ "type": "removal|addition|update", "location": "event_handlers.py - Component References", "explanation": "Adding/Removing components for prompt 'prompt_name'", "old_code": "code to remove or update", "new_code": "new code or empty string for removals", "components": [ # Only for additions {{ "name": "component_name", "type": "text|markdown|dataframe", "initial_value": "initial value" }} ], "handler": {{ # Only for additions requiring handlers "name": "handler_name", "inputs": ["input1", "input2"], "outputs": ["output1", "output2"], "implementation": "handler implementation" }} }} ] }} }} Requirements: 1. Process changes in correct order (removals -> additions -> updates) 2. Each update MUST specify correct type (removal|addition|update) 3. For removals, new_code should be empty string 4. For additions: - Include complete method templates - Include all required components - Include handler implementations if needed - Follow exact naming from config 5. Maintain exact function signatures 6. Preserve existing code structure 7. Include clear comments explaining changes 8. Each file must have an 'updates' array, even if empty IMPORTANT: Return ONLY the JSON object, without any markdown formatting or explanation. """ try: result = call_o1_mini(code_generation_prompt) cleaned_result = clean_ai_response(result) parsed_result = json.loads(cleaned_result) # Ensure correct structure and validate updates for file_name in ["Project.py", "event_handlers.py"]: if file_name not in parsed_result: parsed_result[file_name] = {"updates": []} # Ensure updates array exists if "updates" not in parsed_result[file_name]: parsed_result[file_name]["updates"] = [] # Validate each update for update in parsed_result[file_name]["updates"]: if "type" not in update: update["type"] = "update" # Default type if missing if update["type"] == "removal" and update.get("new_code"): update["new_code"] = "" # Ensure removals have empty new_code if update["type"] == "addition": # Ensure template exists for Project.py additions if file_name == "Project.py" and "template" not in update: update["template"] = { "method_signature": "", "docstring": "", "implementation": "", "return_statement": "" } # Ensure components and handler exist for event_handlers.py additions if file_name == "event_handlers.py": if "components" not in update: update["components"] = [] if "handler" not in update: update["handler"] = { "name": "", "inputs": [], "outputs": [], "implementation": "" } return parsed_result except Exception as e: print(f"Error generating code updates: {str(e)}") print("Raw response:", result) return { "Project.py": {"updates": []}, "event_handlers.py": {"updates": []} } def apply_updates_with_ai(file_path: str, updates: List[Dict]) -> bool: """Third AI: Intelligently applies code updates while preserving structure""" try: with open(file_path, 'r') as f: current_code = f.read() # Store original empty lines and their positions original_lines = current_code.splitlines(keepends=True) empty_line_indices = [i for i, line in enumerate(original_lines) if not line.strip()] trailing_spaces = [len(line) - len(line.rstrip()) for line in original_lines] update_prompt = f""" You are an expert code updater. Your task is to apply updates to the code while perfectly preserving its structure and formatting. CRITICAL RULES: 1. DO NOT modify any line breaks or indentation 2. DO NOT add or remove any lines 3. DO NOT change any code structure or formatting 4. ONLY replace the exact old_code with new_code 5. Return the COMPLETE file with ALL original formatting preserved 6. Keep ALL whitespace, commas, and brackets exactly as they are 7. Return ONLY the code, no explanations or markdown 8. Preserve ALL empty lines in their exact positions 9. Keep ALL trailing spaces at the end of each line Current Code: {current_code} Required Updates: {json.dumps(updates, indent=2)} IMPORTANT: - Empty lines are at these positions: {empty_line_indices} - Each line must maintain its exact trailing spaces - Return the complete file with perfect structure preservation """ # Get AI-generated update result = call_o1_mini(update_prompt) updated_code = clean_code_response(result) # Split into lines while preserving endings updated_lines = updated_code.splitlines(keepends=True) # Handle line structure preservation updated_code = preserve_line_structure(original_lines, updated_lines, empty_line_indices, trailing_spaces) # Verify updates were applied using flexible pattern matching for update in updates: old_code = update.get('old_code', '').strip() new_code = update.get('new_code', '').strip() # Skip empty updates if not old_code and not new_code: continue # For renames, extract the core identifiers if update.get('type') == 'rename': old_identifier = extract_identifier(old_code) new_identifier = extract_identifier(new_code) if old_identifier in updated_code: print(f"Error: Old identifier '{old_identifier}' still present") return False if new_identifier not in updated_code: print(f"Error: New identifier '{new_identifier}' not found") return False else: # For other updates, check if the change was applied if old_code and old_code in updated_code: print(f"Error: Old code still present") return False if new_code and new_code not in updated_code: print(f"Error: New code not found") return False # Write the updated code with open(file_path, 'w') as f: f.write(updated_code) return True except Exception as e: print(f"Error applying AI updates: {str(e)}") return False def clean_code_response(result: str) -> str: """Clean up the AI response code""" if '```python' in result: result = result.split('```python', 1)[1] if '```' in result: result = result.split('```', 1)[0] return result.strip() def preserve_line_structure(original_lines: List[str], updated_lines: List[str], empty_indices: List[int], trailing_spaces: List[int]) -> str: """Preserve the original code structure""" if len(original_lines) != len(updated_lines): fixed_lines = [] for i in range(len(original_lines)): if i in empty_indices: fixed_lines.append('\n') else: line = updated_lines[i].rstrip() if i < len(updated_lines) else '' line = line + ' ' * trailing_spaces[i] fixed_lines.append(line + '\n') return ''.join(fixed_lines) return ''.join(updated_lines) def extract_identifier(code: str) -> str: """Extract the core identifier from a code snippet""" # Remove common code patterns code = code.replace('self.', '') code = code.replace('execute_prompt(', '') code = code.replace('generated_', '') code = code.replace('all_components[', '') code = code.replace(']', '') code = code.replace('"', '') code = code.replace("'", '') # Return the cleaned identifier return code.strip() def extract_config_without_prompts() -> str: """Extract config file content without prompts""" try: with open('page_prompts_config.py', 'r') as f: config_lines = [] in_prompt_block = False in_multiline_string = False prompt_indent = 0 for line in f: stripped_line = line.strip() # Skip empty lines if not stripped_line: continue # Detect start of multi-line string if '"""' in line: if not in_multiline_string: in_multiline_string = True in_prompt_block = True continue else: in_multiline_string = False in_prompt_block = False continue # Skip lines while in a prompt block if in_prompt_block or in_multiline_string: continue # Detect single-line prompt assignments if 'prompt=' in line and not in_multiline_string: continue # Keep all other lines config_lines.append(line) return ''.join(config_lines) except Exception as e: return f"Error reading config file: {str(e)}" def main(): """Main function to handle config changes""" try: # Read current files config_content = extract_config_without_prompts() if config_content.startswith("Error"): print(config_content) return with open('Project.py', 'r') as f: project_content = f.read() with open('event_handlers.py', 'r') as f: current_handlers = f.read() # First AI: Analyze current structure print("\nAnalyzing configuration structure...") analysis_result = analyze_config_structure(config_content, project_content, current_handlers) if not analysis_result.get('component_mappings'): print("Error: Invalid analysis result format") return # Display analysis results print("\nConfiguration Analysis:") print("1. Output Mappings:", len(analysis_result['component_mappings']['outputs'])) print("2. Input Mappings:", len(analysis_result['component_mappings']['inputs'])) print("3. Event Handlers:", len(analysis_result['event_handlers']['handlers'])) if analysis_result.get('required_updates'): print("\nRequired Updates:") for file, updates in analysis_result['required_updates'].items(): print(f"\n{file}:") for update in updates: # Safely access update information with fallbacks update_type = update.get('type', 'Unknown') explanation = update.get('reason', update.get('explanation', 'No explanation provided')) print(f"- {update_type}: {explanation}") if analysis_result.get('validation_issues'): print("\nValidation Issues:") for issue in analysis_result['validation_issues']: severity = issue.get('severity', 'unknown') description = issue.get('description', 'No description provided') issue_type = issue.get('type', 'Unknown') print(f"- {issue_type} ({severity}): {description}") # Get user confirmation confirm = input("\nProceed with generating code updates? (y/n): ") if confirm.lower() != 'y': print("Analysis complete. Update cancelled.") return # Second AI: Generate code updates print("\nGenerating code updates...") code_updates = generate_code_updates(analysis_result) # Display proposed changes print("\nProposed Code Updates:") for file, updates in code_updates.items(): print(f"\n{file}:") for update in updates['updates']: print(f"- Location: {update.get('location', 'Unknown location')}") print(f" Explanation: {update.get('explanation', 'No explanation provided')}") print(" Old code:") print(f"```\n{update.get('old_code', 'No old code provided')}\n```") print(" New code:") print(f"```\n{update.get('new_code', 'No new code provided')}\n```") # Final confirmation confirm = input("\nApply these code updates? (y/n): ") if confirm.lower() != 'y': print("Updates cancelled.") return # Apply updates success = True if 'Project.py' in code_updates: success &= apply_updates_with_ai('Project.py' ,code_updates['Project.py']['updates']) if 'event_handlers.py' in code_updates: success &= apply_updates_with_ai('event_handlers.py' , code_updates['event_handlers.py']['updates']) if success: print("Successfully updated all files") else: print("Some updates failed - please check the files manually") except Exception as e: print(f"Error in update process: {str(e)}") import traceback print("Traceback:", traceback.format_exc()) if __name__ == "__main__": main()