Add tool composition UI with auto-dependency management
- Add ToolStepDialog for configuring tool steps (tool selection, input mapping, args) - Add "Add Tool" button in Tool Builder alongside Add Prompt/Add Code - Add ToolNode in flow graph visualization (purple puzzle piece icon) - Auto-populate dependencies when adding ToolStep in GUI - Add --auto-install flag for automatic runtime dependency installation - Add check_dependencies() and auto_install_dependencies() functions - Update runner to pass auto_install parameter through execution chain Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -9,7 +9,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
from PySide6.QtCore import Qt, QThread, Signal
|
||||
|
||||
from ...tool import PromptStep, CodeStep
|
||||
from ...tool import PromptStep, CodeStep, ToolStep, list_tools, load_tool
|
||||
from ...providers import load_providers, call_provider
|
||||
from ...profiles import list_profiles
|
||||
|
||||
@@ -424,3 +424,218 @@ IMPORTANT: Return ONLY executable inline Python code. No function definitions, n
|
||||
output_var=self.output_input.text().strip(),
|
||||
name=name
|
||||
)
|
||||
|
||||
|
||||
class ToolStepDialog(QDialog):
|
||||
"""Dialog for adding/editing tool steps (calling another tool)."""
|
||||
|
||||
def __init__(self, parent, step: ToolStep = None, available_vars: list = None, current_tool_name: str = None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Edit Tool Step" if step else "Add Tool Step")
|
||||
self.setMinimumSize(550, 500)
|
||||
self._step = step
|
||||
self._available_vars = available_vars or ["input"]
|
||||
self._current_tool_name = current_tool_name # To prevent self-referencing
|
||||
self._tool_args = {} # Cache of tool arguments
|
||||
self._setup_ui()
|
||||
|
||||
if step:
|
||||
self._load_step(step)
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Set up the UI."""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# Form
|
||||
form = QFormLayout()
|
||||
form.setSpacing(12)
|
||||
|
||||
# Step name (optional)
|
||||
self.name_input = QLineEdit()
|
||||
self.name_input.setPlaceholderText("Optional display name")
|
||||
form.addRow("Step name:", self.name_input)
|
||||
|
||||
# Tool selection
|
||||
self.tool_combo = QComboBox()
|
||||
self._populate_tools()
|
||||
self.tool_combo.currentTextChanged.connect(self._on_tool_changed)
|
||||
form.addRow("Tool:", self.tool_combo)
|
||||
|
||||
# Tool description (read-only)
|
||||
self.tool_desc = QLabel("")
|
||||
self.tool_desc.setStyleSheet("color: #718096; font-size: 11px;")
|
||||
self.tool_desc.setWordWrap(True)
|
||||
form.addRow("", self.tool_desc)
|
||||
|
||||
# Output variable
|
||||
self.output_input = QLineEdit()
|
||||
self.output_input.setPlaceholderText("tool_output")
|
||||
self.output_input.setText("tool_output")
|
||||
form.addRow("Output variable:", self.output_input)
|
||||
|
||||
# Provider override (optional)
|
||||
self.provider_combo = QComboBox()
|
||||
self.provider_combo.addItem("(use tool's default)")
|
||||
providers = load_providers()
|
||||
for provider in sorted(providers, key=lambda p: p.name):
|
||||
self.provider_combo.addItem(provider.name)
|
||||
form.addRow("Provider override:", self.provider_combo)
|
||||
|
||||
layout.addLayout(form)
|
||||
|
||||
# Input template
|
||||
input_group = QGroupBox("Input")
|
||||
input_layout = QVBoxLayout(input_group)
|
||||
|
||||
vars_text = ", ".join(f"{{{v}}}" for v in self._available_vars)
|
||||
input_help = QLabel(f"Available variables: {vars_text}")
|
||||
input_help.setStyleSheet("color: #718096; font-size: 11px;")
|
||||
input_layout.addWidget(input_help)
|
||||
|
||||
self.input_template = QPlainTextEdit()
|
||||
self.input_template.setPlaceholderText("{input}")
|
||||
self.input_template.setPlainText("{input}")
|
||||
self.input_template.setMaximumHeight(80)
|
||||
input_layout.addWidget(self.input_template)
|
||||
|
||||
layout.addWidget(input_group)
|
||||
|
||||
# Tool arguments
|
||||
self.args_group = QGroupBox("Tool Arguments")
|
||||
self.args_layout = QFormLayout(self.args_group)
|
||||
self.args_layout.setSpacing(8)
|
||||
self._arg_inputs = {} # variable -> QLineEdit
|
||||
layout.addWidget(self.args_group)
|
||||
|
||||
# Buttons
|
||||
buttons = QHBoxLayout()
|
||||
buttons.addStretch()
|
||||
|
||||
self.btn_cancel = QPushButton("Cancel")
|
||||
self.btn_cancel.setObjectName("secondary")
|
||||
self.btn_cancel.clicked.connect(self.reject)
|
||||
buttons.addWidget(self.btn_cancel)
|
||||
|
||||
self.btn_ok = QPushButton("OK")
|
||||
self.btn_ok.clicked.connect(self._validate_and_accept)
|
||||
buttons.addWidget(self.btn_ok)
|
||||
|
||||
layout.addLayout(buttons)
|
||||
|
||||
# Trigger initial tool selection
|
||||
if self.tool_combo.count() > 0:
|
||||
self._on_tool_changed(self.tool_combo.currentText())
|
||||
|
||||
def _populate_tools(self):
|
||||
"""Populate the tool dropdown."""
|
||||
tools = list_tools()
|
||||
for tool_name in sorted(tools):
|
||||
# Skip the current tool to prevent self-referencing
|
||||
if tool_name != self._current_tool_name:
|
||||
self.tool_combo.addItem(tool_name)
|
||||
|
||||
def _on_tool_changed(self, tool_name: str):
|
||||
"""Handle tool selection change."""
|
||||
# Clear existing arg inputs
|
||||
for widget in self._arg_inputs.values():
|
||||
self.args_layout.removeRow(widget)
|
||||
self._arg_inputs.clear()
|
||||
|
||||
if not tool_name:
|
||||
self.tool_desc.setText("")
|
||||
return
|
||||
|
||||
tool = load_tool(tool_name)
|
||||
if not tool:
|
||||
self.tool_desc.setText("(Tool not found)")
|
||||
return
|
||||
|
||||
# Update description
|
||||
self.tool_desc.setText(tool.description or "(No description)")
|
||||
|
||||
# Cache arguments
|
||||
self._tool_args[tool_name] = tool.arguments
|
||||
|
||||
# Create input fields for each argument
|
||||
vars_text = ", ".join(f"{{{v}}}" for v in self._available_vars)
|
||||
for arg in tool.arguments:
|
||||
line = QLineEdit()
|
||||
line.setPlaceholderText(f"Default: {arg.default}" if arg.default else f"Variable: {vars_text}")
|
||||
if arg.default:
|
||||
line.setText(arg.default)
|
||||
self._arg_inputs[arg.variable] = line
|
||||
|
||||
label = f"{arg.flag}:"
|
||||
if arg.description:
|
||||
label = f"{arg.flag} ({arg.description}):"
|
||||
self.args_layout.addRow(label, line)
|
||||
|
||||
def _load_step(self, step: ToolStep):
|
||||
"""Load step data into form."""
|
||||
# Load name
|
||||
if step.name:
|
||||
self.name_input.setText(step.name)
|
||||
|
||||
# Select tool
|
||||
idx = self.tool_combo.findText(step.tool)
|
||||
if idx >= 0:
|
||||
self.tool_combo.setCurrentIndex(idx)
|
||||
else:
|
||||
# Tool might be qualified name - try to add it
|
||||
self.tool_combo.addItem(step.tool)
|
||||
self.tool_combo.setCurrentText(step.tool)
|
||||
|
||||
self.output_input.setText(step.output_var)
|
||||
self.input_template.setPlainText(step.input_template)
|
||||
|
||||
# Set provider override
|
||||
if step.provider:
|
||||
idx = self.provider_combo.findText(step.provider)
|
||||
if idx >= 0:
|
||||
self.provider_combo.setCurrentIndex(idx)
|
||||
|
||||
# Set argument values (after tool is selected and args are loaded)
|
||||
for var, value in step.args.items():
|
||||
if var in self._arg_inputs:
|
||||
self._arg_inputs[var].setText(str(value))
|
||||
|
||||
def _validate_and_accept(self):
|
||||
"""Validate and accept."""
|
||||
tool = self.tool_combo.currentText().strip()
|
||||
if not tool:
|
||||
self.tool_combo.setFocus()
|
||||
return
|
||||
|
||||
output = self.output_input.text().strip()
|
||||
if not output:
|
||||
self.output_input.setFocus()
|
||||
return
|
||||
|
||||
self.accept()
|
||||
|
||||
def get_step(self) -> ToolStep:
|
||||
"""Get the step from form data."""
|
||||
# Collect arguments
|
||||
args = {}
|
||||
for var, line in self._arg_inputs.items():
|
||||
value = line.text().strip()
|
||||
if value:
|
||||
args[var] = value
|
||||
|
||||
# Get provider override
|
||||
provider = self.provider_combo.currentText()
|
||||
if provider == "(use tool's default)":
|
||||
provider = None
|
||||
|
||||
# Get name, use None if empty
|
||||
name = self.name_input.text().strip() or None
|
||||
|
||||
return ToolStep(
|
||||
tool=self.tool_combo.currentText(),
|
||||
output_var=self.output_input.text().strip(),
|
||||
input_template=self.input_template.toPlainText() or "{input}",
|
||||
args=args,
|
||||
provider=provider,
|
||||
name=name
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user