Add drag-reorder, validation, and flow-to-tool sync
List View:
- Enable drag-drop reordering of steps
- Show broken dependency warnings when reordering
- Steps with missing variable refs shown in red with tooltip
Validation:
- Parse {variable} references from prompts and code
- Track available variables at each step position
- Warn when steps reference undefined variables
Flow View Sync:
- Deleting nodes in flow view updates the tool model
- steps_deleted signal propagates changes to builder
- Both views stay in sync
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8ab0fba67c
commit
b8760eb208
|
|
@ -163,6 +163,10 @@ class ToolBuilderPage(QWidget):
|
||||||
|
|
||||||
self.steps_list = QListWidget()
|
self.steps_list = QListWidget()
|
||||||
self.steps_list.itemDoubleClicked.connect(self._edit_step)
|
self.steps_list.itemDoubleClicked.connect(self._edit_step)
|
||||||
|
# Enable drag-drop reordering
|
||||||
|
self.steps_list.setDragDropMode(QListWidget.InternalMove)
|
||||||
|
self.steps_list.setDefaultDropAction(Qt.MoveAction)
|
||||||
|
self.steps_list.model().rowsMoved.connect(self._on_steps_reordered)
|
||||||
list_layout.addWidget(self.steps_list)
|
list_layout.addWidget(self.steps_list)
|
||||||
|
|
||||||
self.steps_stack.addWidget(list_container)
|
self.steps_stack.addWidget(list_container)
|
||||||
|
|
@ -236,6 +240,7 @@ class ToolBuilderPage(QWidget):
|
||||||
from ..widgets.flow_graph import FlowGraphWidget
|
from ..widgets.flow_graph import FlowGraphWidget
|
||||||
self._flow_widget = FlowGraphWidget()
|
self._flow_widget = FlowGraphWidget()
|
||||||
self._flow_widget.node_double_clicked.connect(self._on_flow_node_double_clicked)
|
self._flow_widget.node_double_clicked.connect(self._on_flow_node_double_clicked)
|
||||||
|
self._flow_widget.steps_deleted.connect(self._on_flow_steps_deleted)
|
||||||
|
|
||||||
# Replace placeholder
|
# Replace placeholder
|
||||||
old_widget = self.steps_stack.widget(1)
|
old_widget = self.steps_stack.widget(1)
|
||||||
|
|
@ -264,6 +269,19 @@ class ToolBuilderPage(QWidget):
|
||||||
step = self._tool.steps[step_index]
|
step = self._tool.steps[step_index]
|
||||||
self._edit_step_at_index(step_index, step)
|
self._edit_step_at_index(step_index, step)
|
||||||
|
|
||||||
|
def _on_flow_steps_deleted(self, indices: list):
|
||||||
|
"""Handle step deletion from flow view."""
|
||||||
|
if not self._tool:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Delete steps in reverse order (indices are already sorted reverse)
|
||||||
|
for idx in indices:
|
||||||
|
if 0 <= idx < len(self._tool.steps):
|
||||||
|
del self._tool.steps[idx]
|
||||||
|
|
||||||
|
# Refresh the list view (flow view will be refreshed by set_tool)
|
||||||
|
self._refresh_steps()
|
||||||
|
|
||||||
def _load_tool(self, name: str):
|
def _load_tool(self, name: str):
|
||||||
"""Load an existing tool for editing."""
|
"""Load an existing tool for editing."""
|
||||||
tool = load_tool(name)
|
tool = load_tool(name)
|
||||||
|
|
@ -311,6 +329,13 @@ class ToolBuilderPage(QWidget):
|
||||||
prompt_count = 0
|
prompt_count = 0
|
||||||
code_count = 0
|
code_count = 0
|
||||||
|
|
||||||
|
# Track available variables for dependency checking
|
||||||
|
available_vars = {"input"}
|
||||||
|
if self._tool.arguments:
|
||||||
|
for arg in self._tool.arguments:
|
||||||
|
if arg.variable:
|
||||||
|
available_vars.add(arg.variable)
|
||||||
|
|
||||||
for i, step in enumerate(self._tool.steps, 1):
|
for i, step in enumerate(self._tool.steps, 1):
|
||||||
if isinstance(step, PromptStep):
|
if isinstance(step, PromptStep):
|
||||||
prompt_count += 1
|
prompt_count += 1
|
||||||
|
|
@ -323,10 +348,25 @@ class ToolBuilderPage(QWidget):
|
||||||
text = f"{i}. {step_name} [python] → ${step.output_var}"
|
text = f"{i}. {step_name} [python] → ${step.output_var}"
|
||||||
else:
|
else:
|
||||||
text = f"{i}. Unknown step"
|
text = f"{i}. Unknown step"
|
||||||
|
|
||||||
item = QListWidgetItem(text)
|
item = QListWidgetItem(text)
|
||||||
item.setData(Qt.UserRole, step)
|
item.setData(Qt.UserRole, step)
|
||||||
|
|
||||||
|
# Check for broken dependencies
|
||||||
|
refs = self._get_step_variable_refs(step)
|
||||||
|
broken_refs = [r for r in refs if r not in available_vars]
|
||||||
|
if broken_refs:
|
||||||
|
# Mark item with warning color and tooltip
|
||||||
|
item.setForeground(Qt.red)
|
||||||
|
item.setToolTip(f"Missing variables: {', '.join(broken_refs)}")
|
||||||
|
|
||||||
self.steps_list.addItem(item)
|
self.steps_list.addItem(item)
|
||||||
|
|
||||||
|
# Add this step's output to available vars for next iteration
|
||||||
|
if hasattr(step, 'output_var') and step.output_var:
|
||||||
|
for var in step.output_var.split(','):
|
||||||
|
available_vars.add(var.strip())
|
||||||
|
|
||||||
# Update flow widget if initialized
|
# Update flow widget if initialized
|
||||||
if self._flow_widget:
|
if self._flow_widget:
|
||||||
self._flow_widget.set_tool(self._tool)
|
self._flow_widget.set_tool(self._tool)
|
||||||
|
|
@ -454,6 +494,91 @@ class ToolBuilderPage(QWidget):
|
||||||
del self._tool.steps[idx]
|
del self._tool.steps[idx]
|
||||||
self._refresh_steps()
|
self._refresh_steps()
|
||||||
|
|
||||||
|
def _on_steps_reordered(self, parent, start, end, dest, row):
|
||||||
|
"""Handle step reordering via drag-drop."""
|
||||||
|
if not self._tool or not self._tool.steps:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Rebuild steps list from current list widget order
|
||||||
|
new_steps = []
|
||||||
|
for i in range(self.steps_list.count()):
|
||||||
|
item = self.steps_list.item(i)
|
||||||
|
step = item.data(Qt.UserRole)
|
||||||
|
if step:
|
||||||
|
new_steps.append(step)
|
||||||
|
|
||||||
|
# Validate the new order
|
||||||
|
warnings = self._validate_step_order(new_steps)
|
||||||
|
|
||||||
|
if warnings:
|
||||||
|
# Show warning but allow the reorder
|
||||||
|
warning_msg = "Variable dependency warnings:\n\n" + "\n".join(warnings)
|
||||||
|
warning_msg += "\n\nThe reorder has been applied. You may need to fix these issues."
|
||||||
|
QMessageBox.warning(self, "Dependency Warning", warning_msg)
|
||||||
|
|
||||||
|
# Apply the new order
|
||||||
|
self._tool.steps = new_steps
|
||||||
|
|
||||||
|
# Refresh to update numbering and flow view
|
||||||
|
self._refresh_steps()
|
||||||
|
|
||||||
|
def _validate_step_order(self, steps: list) -> list:
|
||||||
|
"""Validate step order for variable dependencies.
|
||||||
|
|
||||||
|
Returns a list of warning messages for broken dependencies.
|
||||||
|
"""
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
# Track available variables at each position
|
||||||
|
available = {"input"}
|
||||||
|
|
||||||
|
# Add argument variables
|
||||||
|
if self._tool and self._tool.arguments:
|
||||||
|
for arg in self._tool.arguments:
|
||||||
|
if arg.variable:
|
||||||
|
available.add(arg.variable)
|
||||||
|
|
||||||
|
for i, step in enumerate(steps):
|
||||||
|
# Get variables this step references
|
||||||
|
refs = self._get_step_variable_refs(step)
|
||||||
|
|
||||||
|
# Check for undefined references
|
||||||
|
for ref in refs:
|
||||||
|
if ref not in available:
|
||||||
|
step_name = step.name if step.name else f"Step {i+1}"
|
||||||
|
warnings.append(f"{step_name}: references '{{{ref}}}' which is not defined yet")
|
||||||
|
|
||||||
|
# Add this step's output to available vars
|
||||||
|
if hasattr(step, 'output_var') and step.output_var:
|
||||||
|
for var in step.output_var.split(','):
|
||||||
|
available.add(var.strip())
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
def _get_step_variable_refs(self, step) -> set:
|
||||||
|
"""Extract variable references from a step.
|
||||||
|
|
||||||
|
Parses {variable} patterns from prompt templates and code.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
refs = set()
|
||||||
|
|
||||||
|
# Pattern to match {variable} but not {{escaped}}
|
||||||
|
pattern = r'\{(\w+)\}'
|
||||||
|
|
||||||
|
if isinstance(step, PromptStep):
|
||||||
|
# Parse prompt template
|
||||||
|
matches = re.findall(pattern, step.prompt or "")
|
||||||
|
refs.update(matches)
|
||||||
|
elif isinstance(step, CodeStep):
|
||||||
|
# Parse code for variable references
|
||||||
|
# In code, variables are accessed directly, but we also check for {var} patterns
|
||||||
|
# since the code might use string formatting
|
||||||
|
matches = re.findall(pattern, step.code or "")
|
||||||
|
refs.update(matches)
|
||||||
|
|
||||||
|
return refs
|
||||||
|
|
||||||
def _save(self):
|
def _save(self):
|
||||||
"""Save the tool."""
|
"""Save the tool."""
|
||||||
name = self.name_input.text().strip()
|
name = self.name_input.text().strip()
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@ class FlowGraphWidget(QWidget):
|
||||||
# Emitted when the flow structure changes
|
# Emitted when the flow structure changes
|
||||||
flow_changed = Signal()
|
flow_changed = Signal()
|
||||||
|
|
||||||
|
# Emitted when steps are deleted from flow view (list of step indices)
|
||||||
|
steps_deleted = Signal(list)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._tool: Optional[Tool] = None
|
self._tool: Optional[Tool] = None
|
||||||
|
|
@ -149,6 +152,7 @@ class FlowGraphWidget(QWidget):
|
||||||
|
|
||||||
# Connect signals
|
# Connect signals
|
||||||
self._graph.node_double_clicked.connect(self._on_node_double_clicked)
|
self._graph.node_double_clicked.connect(self._on_node_double_clicked)
|
||||||
|
self._graph.nodes_deleted.connect(self._on_nodes_deleted)
|
||||||
|
|
||||||
# Add graph widget
|
# Add graph widget
|
||||||
layout.addWidget(self._graph.widget, 1)
|
layout.addWidget(self._graph.widget, 1)
|
||||||
|
|
@ -409,6 +413,20 @@ class FlowGraphWidget(QWidget):
|
||||||
step_type = 'prompt' if isinstance(node, PromptNode) else 'code'
|
step_type = 'prompt' if isinstance(node, PromptNode) else 'code'
|
||||||
self.node_double_clicked.emit(node._step_index, step_type)
|
self.node_double_clicked.emit(node._step_index, step_type)
|
||||||
|
|
||||||
|
def _on_nodes_deleted(self, nodes):
|
||||||
|
"""Handle node deletion from the graph."""
|
||||||
|
# Collect step indices of deleted step nodes (not Input/Output nodes)
|
||||||
|
deleted_indices = []
|
||||||
|
for node in nodes:
|
||||||
|
if hasattr(node, '_step_index') and node._step_index >= 0:
|
||||||
|
deleted_indices.append(node._step_index)
|
||||||
|
|
||||||
|
if deleted_indices:
|
||||||
|
# Sort in reverse order so we delete from end first
|
||||||
|
deleted_indices.sort(reverse=True)
|
||||||
|
self.steps_deleted.emit(deleted_indices)
|
||||||
|
self.flow_changed.emit()
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
"""Refresh the graph from current tool data."""
|
"""Refresh the graph from current tool data."""
|
||||||
self._rebuild_graph()
|
self._rebuild_graph()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue