diff --git a/src/cmdforge/gui/dialogs/step_dialog.py b/src/cmdforge/gui/dialogs/step_dialog.py index 6fcab36..89120e7 100644 --- a/src/cmdforge/gui/dialogs/step_dialog.py +++ b/src/cmdforge/gui/dialogs/step_dialog.py @@ -3,12 +3,12 @@ from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QFormLayout, QLineEdit, QComboBox, QPushButton, QHBoxLayout, QLabel, - QPlainTextEdit + QPlainTextEdit, QSplitter, QGroupBox, QTextEdit ) -from PySide6.QtCore import Qt +from PySide6.QtCore import Qt, QThread, Signal from ...tool import PromptStep, CodeStep -from ...providers import load_providers +from ...providers import load_providers, call_provider from ...profiles import list_profiles @@ -136,63 +136,142 @@ class PromptStepDialog(QDialog): ) -class CodeStepDialog(QDialog): - """Dialog for editing code steps.""" +class AIGenerateWorker(QThread): + """Background worker for AI code generation.""" + finished = Signal(str) + error = Signal(str) - def __init__(self, parent, step: CodeStep = None): + def __init__(self, provider: str, prompt: str): + super().__init__() + self.provider = provider + self.prompt = prompt + + def run(self): + try: + result = call_provider(self.provider, self.prompt) + if result.success: + self.finished.emit(result.text) + else: + self.error.emit(result.error or "Unknown error") + except Exception as e: + self.error.emit(str(e)) + + +class CodeStepDialog(QDialog): + """Dialog for editing code steps with AI assist.""" + + def __init__(self, parent, step: CodeStep = None, available_vars: list = None): super().__init__(parent) self.setWindowTitle("Edit Code Step" if step else "Add Code Step") - self.setMinimumSize(600, 500) + self.setMinimumSize(900, 600) self._step = step + self._available_vars = available_vars or ["input"] + self._worker = None self._setup_ui() if step: self._load_step(step) def _setup_ui(self): - """Set up the UI.""" + """Set up the UI with code editor and AI assist panel.""" layout = QVBoxLayout(self) - layout.setSpacing(16) + layout.setSpacing(12) - # Form + # Top: Output variable form = QFormLayout() - form.setSpacing(12) - - # Output variable + form.setSpacing(8) self.output_input = QLineEdit() self.output_input.setPlaceholderText("result") self.output_input.setText("result") form.addRow("Output variable:", self.output_input) - layout.addLayout(form) - # Code editor - code_label = QLabel("Code:") - layout.addWidget(code_label) + # Available variables display + vars_text = ", ".join(self._available_vars) + vars_label = QLabel(f"Available variables: {vars_text}") + vars_label.setStyleSheet("color: #718096; font-size: 11px;") + layout.addWidget(vars_label) + + # Main splitter: Code editor | AI Assist + splitter = QSplitter(Qt.Horizontal) + + # Left: Code editor + code_group = QGroupBox("Code") + code_layout = QVBoxLayout(code_group) self.code_input = QPlainTextEdit() self.code_input.setPlaceholderText( "# Python code here\n" - "# Access input with: input_text\n" - "# Access args with their variable names\n" - "# Access previous step outputs with their variable names\n\n" - "result = input_text.upper()" + "# Access input with: input\n" + "# Access args with their variable names\n\n" + "result = input.upper()" ) font = self.code_input.font() - font.setFamily("Courier New, Consolas, monospace") + font.setFamily("Consolas, Monaco, monospace") self.code_input.setFont(font) - layout.addWidget(self.code_input, 1) + code_layout.addWidget(self.code_input) - # Help text - help_text = QLabel( - "The code will be executed with variables in scope. " - "The last expression or the output variable will be captured." + splitter.addWidget(code_group) + + # Right: AI Assist panel + ai_group = QGroupBox("AI Assisted Code Generation") + ai_layout = QVBoxLayout(ai_group) + ai_layout.setSpacing(8) + + # Provider selector + provider_layout = QHBoxLayout() + provider_layout.addWidget(QLabel("Provider:")) + self.ai_provider_combo = QComboBox() + providers = load_providers() + for provider in sorted(providers, key=lambda p: p.name): + self.ai_provider_combo.addItem(provider.name) + # Add common defaults if not present + for default in ["claude", "gpt", "mock"]: + if self.ai_provider_combo.findText(default) < 0: + self.ai_provider_combo.addItem(default) + self.ai_provider_combo.setMinimumWidth(150) + provider_layout.addWidget(self.ai_provider_combo) + provider_layout.addStretch() + ai_layout.addLayout(provider_layout) + + # Prompt editor + prompt_label = QLabel("Describe what you want the code to do:") + ai_layout.addWidget(prompt_label) + + self.ai_prompt_input = QPlainTextEdit() + self.ai_prompt_input.setPlaceholderText( + "Example: Parse the input as JSON and extract the 'name' field" ) - help_text.setStyleSheet("color: #718096; font-size: 11px;") - help_text.setWordWrap(True) - layout.addWidget(help_text) + # Set default prompt template + self._set_default_prompt() + ai_layout.addWidget(self.ai_prompt_input, 2) - # Buttons + # Generate button + btn_layout = QHBoxLayout() + self.btn_generate = QPushButton("Generate Code") + self.btn_generate.clicked.connect(self._generate_code) + self.btn_generate.setMinimumHeight(32) + btn_layout.addStretch() + btn_layout.addWidget(self.btn_generate) + btn_layout.addStretch() + ai_layout.addLayout(btn_layout) + + # Output/feedback area + feedback_label = QLabel("AI Response:") + ai_layout.addWidget(feedback_label) + + self.ai_output = QTextEdit() + self.ai_output.setReadOnly(True) + self.ai_output.setPlaceholderText("AI response will appear here...") + self.ai_output.setMaximumHeight(100) + ai_layout.addWidget(self.ai_output, 1) + + splitter.addWidget(ai_group) + splitter.setSizes([450, 450]) + + layout.addWidget(splitter, 1) + + # Bottom: OK/Cancel buttons buttons = QHBoxLayout() buttons.addStretch() @@ -207,6 +286,81 @@ class CodeStepDialog(QDialog): layout.addLayout(buttons) + def _set_default_prompt(self): + """Set the default AI prompt template.""" + vars_formatted = ', '.join(f'"{{{v}}}"' for v in self._available_vars) + default_prompt = f"""Write inline Python code (NOT a function definition) according to my instruction. + +The code runs directly with variable substitution. Assign any available variables to a local variable first using triple quotes for multi-line content safety. + +Example: +my_var = \"\"\"{{input}}\"\"\" +result = my_var.upper() + +INSTRUCTION: [Describe what you want the code to do] + +CURRENT CODE: +{{code}} + +AVAILABLE VARIABLES: {vars_formatted} + +IMPORTANT: Return ONLY executable inline Python code. No function definitions, no markdown fencing, no explanations - just the code.""" + self.ai_prompt_input.setPlainText(default_prompt) + + def _generate_code(self): + """Generate code using AI.""" + prompt_template = self.ai_prompt_input.toPlainText().strip() + if not prompt_template: + self.ai_output.setHtml("Please enter a prompt") + return + + # Replace {code} placeholder with current code + current_code = self.code_input.toPlainText().strip() or "# No code yet" + prompt = prompt_template.replace("{code}", current_code) + + provider = self.ai_provider_combo.currentText() + + # Disable button and show loading state + self.btn_generate.setEnabled(False) + self.btn_generate.setText("Generating...") + self.ai_output.setHtml(f"Calling {provider}...") + + # Start worker thread + self._worker = AIGenerateWorker(provider, prompt) + self._worker.finished.connect(self._on_generate_finished) + self._worker.error.connect(self._on_generate_error) + self._worker.start() + + def _on_generate_finished(self, result: str): + """Handle successful AI generation.""" + self.btn_generate.setEnabled(True) + self.btn_generate.setText("Generate Code") + + # Clean up the result - strip markdown code fences if present + code = result.strip() + if code.startswith("```python"): + code = code[9:] + elif code.startswith("```"): + code = code[3:] + if code.endswith("```"): + code = code[:-3] + code = code.strip() + + # Update code editor + self.code_input.setPlainText(code) + + # Show success message + self.ai_output.setHtml( + f"Code generated successfully!
" + f"Response length: {len(result)} chars" + ) + + def _on_generate_error(self, error: str): + """Handle AI generation error.""" + self.btn_generate.setEnabled(True) + self.btn_generate.setText("Generate Code") + self.ai_output.setHtml(f"Error: {error}") + def _load_step(self, step: CodeStep): """Load step data into form.""" self.output_input.setText(step.output_var) diff --git a/src/cmdforge/gui/pages/tool_builder_page.py b/src/cmdforge/gui/pages/tool_builder_page.py index bfed676..d05be40 100644 --- a/src/cmdforge/gui/pages/tool_builder_page.py +++ b/src/cmdforge/gui/pages/tool_builder_page.py @@ -261,6 +261,34 @@ class ToolBuilderPage(QWidget): del self._tool.arguments[idx] self._refresh_arguments() + def _get_available_vars(self, up_to_step: int = -1) -> list: + """Get available variables for a step. + + Args: + up_to_step: Include variables from steps before this index. + -1 means include all existing steps (for new steps). + """ + vars_list = ["input"] + + # Add argument variables + if self._tool and self._tool.arguments: + for arg in self._tool.arguments: + if arg.variable and arg.variable not in vars_list: + vars_list.append(arg.variable) + + # Add output variables from previous steps + if self._tool and self._tool.steps: + steps_to_check = self._tool.steps if up_to_step < 0 else self._tool.steps[:up_to_step] + for step in steps_to_check: + if hasattr(step, 'output_var') and step.output_var: + # Handle comma-separated output vars + for var in step.output_var.split(','): + var = var.strip() + if var and var not in vars_list: + vars_list.append(var) + + return vars_list + def _add_prompt_step(self): """Add a prompt step.""" from ..dialogs.step_dialog import PromptStepDialog @@ -275,7 +303,8 @@ class ToolBuilderPage(QWidget): def _add_code_step(self): """Add a code step.""" from ..dialogs.step_dialog import CodeStepDialog - dialog = CodeStepDialog(self) + available_vars = self._get_available_vars() + dialog = CodeStepDialog(self, available_vars=available_vars) if dialog.exec(): step = dialog.get_step() if not self._tool: @@ -297,7 +326,8 @@ class ToolBuilderPage(QWidget): dialog = PromptStepDialog(self, step) elif isinstance(step, CodeStep): from ..dialogs.step_dialog import CodeStepDialog - dialog = CodeStepDialog(self, step) + available_vars = self._get_available_vars(up_to_step=idx) + dialog = CodeStepDialog(self, step, available_vars=available_vars) else: return