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:
@@ -10,10 +10,10 @@ from PySide6.QtWidgets import (
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
from ...tool import (
|
||||
Tool, ToolArgument, PromptStep, CodeStep,
|
||||
Tool, ToolArgument, PromptStep, CodeStep, ToolStep,
|
||||
load_tool, save_tool, validate_tool_name, DEFAULT_CATEGORIES
|
||||
)
|
||||
from ..widgets.icons import get_prompt_icon, get_code_icon
|
||||
from ..widgets.icons import get_prompt_icon, get_code_icon, get_tool_icon
|
||||
|
||||
|
||||
class ToolBuilderPage(QWidget):
|
||||
@@ -190,6 +190,10 @@ class ToolBuilderPage(QWidget):
|
||||
self.btn_add_code.clicked.connect(self._add_code_step)
|
||||
steps_btns.addWidget(self.btn_add_code)
|
||||
|
||||
self.btn_add_tool = QPushButton("Add Tool")
|
||||
self.btn_add_tool.clicked.connect(self._add_tool_step)
|
||||
steps_btns.addWidget(self.btn_add_tool)
|
||||
|
||||
self.btn_edit_step = QPushButton("Edit")
|
||||
self.btn_edit_step.setObjectName("secondary")
|
||||
self.btn_edit_step.clicked.connect(self._edit_step)
|
||||
@@ -379,6 +383,9 @@ class ToolBuilderPage(QWidget):
|
||||
if arg.variable:
|
||||
available_vars.add(arg.variable)
|
||||
|
||||
# Count for default naming
|
||||
tool_count = 0
|
||||
|
||||
for i, step in enumerate(self._tool.steps, 1):
|
||||
if isinstance(step, PromptStep):
|
||||
prompt_count += 1
|
||||
@@ -391,6 +398,11 @@ class ToolBuilderPage(QWidget):
|
||||
step_name = step.name if step.name else f"Code {code_count}"
|
||||
text = f"{step_name} [python] → ${step.output_var}"
|
||||
icon = get_code_icon(20)
|
||||
elif isinstance(step, ToolStep):
|
||||
tool_count += 1
|
||||
step_name = step.name if step.name else f"Tool {tool_count}"
|
||||
text = f"{step_name} [{step.tool}] → ${step.output_var}"
|
||||
icon = get_tool_icon(20)
|
||||
else:
|
||||
text = f"Unknown step"
|
||||
icon = None
|
||||
@@ -430,6 +442,7 @@ class ToolBuilderPage(QWidget):
|
||||
# Count steps by type for default naming
|
||||
prompt_count = 0
|
||||
code_count = 0
|
||||
tool_count = 0
|
||||
|
||||
# Track available variables for dependency checking
|
||||
available_vars = {"input"}
|
||||
@@ -449,6 +462,11 @@ class ToolBuilderPage(QWidget):
|
||||
step_name = step.name if step.name else f"Code {code_count}"
|
||||
text = f"{step_name} [python] → ${step.output_var}"
|
||||
icon = get_code_icon(20)
|
||||
elif isinstance(step, ToolStep):
|
||||
tool_count += 1
|
||||
step_name = step.name if step.name else f"Tool {tool_count}"
|
||||
text = f"{step_name} [{step.tool}] → ${step.output_var}"
|
||||
icon = get_tool_icon(20)
|
||||
else:
|
||||
text = f"Unknown step"
|
||||
icon = None
|
||||
@@ -508,6 +526,23 @@ class ToolBuilderPage(QWidget):
|
||||
del self._tool.arguments[idx]
|
||||
self._refresh_arguments()
|
||||
|
||||
def _add_tool_dependency(self, tool_ref: str):
|
||||
"""Add a tool reference to the dependencies list if not already present.
|
||||
|
||||
Args:
|
||||
tool_ref: Tool reference (e.g., 'my-tool' or 'owner/tool-name')
|
||||
"""
|
||||
if not self._tool or not tool_ref:
|
||||
return
|
||||
|
||||
# Initialize dependencies list if needed
|
||||
if not hasattr(self._tool, 'dependencies') or self._tool.dependencies is None:
|
||||
self._tool.dependencies = []
|
||||
|
||||
# Add if not already present
|
||||
if tool_ref not in self._tool.dependencies:
|
||||
self._tool.dependencies.append(tool_ref)
|
||||
|
||||
def _get_available_vars(self, up_to_step: int = -1) -> list:
|
||||
"""Get available variables for a step.
|
||||
|
||||
@@ -559,6 +594,21 @@ class ToolBuilderPage(QWidget):
|
||||
self._tool.steps.append(step)
|
||||
self._refresh_steps()
|
||||
|
||||
def _add_tool_step(self):
|
||||
"""Add a tool step (call another tool)."""
|
||||
from ..dialogs.step_dialog import ToolStepDialog
|
||||
available_vars = self._get_available_vars()
|
||||
current_name = self._tool.name if self._tool else None
|
||||
dialog = ToolStepDialog(self, available_vars=available_vars, current_tool_name=current_name)
|
||||
if dialog.exec():
|
||||
step = dialog.get_step()
|
||||
if not self._tool:
|
||||
self._tool = Tool(name="", description="", arguments=[], steps=[], output="{response}")
|
||||
self._tool.steps.append(step)
|
||||
# Auto-add to dependencies if not already present
|
||||
self._add_tool_dependency(step.tool)
|
||||
self._refresh_steps()
|
||||
|
||||
def _edit_step(self):
|
||||
"""Edit selected step from list view."""
|
||||
items = self.steps_list.selectedItems()
|
||||
@@ -578,11 +628,20 @@ class ToolBuilderPage(QWidget):
|
||||
from ..dialogs.step_dialog import CodeStepDialog
|
||||
available_vars = self._get_available_vars(up_to_step=idx)
|
||||
dialog = CodeStepDialog(self, step, available_vars=available_vars)
|
||||
elif isinstance(step, ToolStep):
|
||||
from ..dialogs.step_dialog import ToolStepDialog
|
||||
available_vars = self._get_available_vars(up_to_step=idx)
|
||||
current_name = self._tool.name if self._tool else None
|
||||
dialog = ToolStepDialog(self, step, available_vars=available_vars, current_tool_name=current_name)
|
||||
else:
|
||||
return
|
||||
|
||||
if dialog.exec():
|
||||
self._tool.steps[idx] = dialog.get_step()
|
||||
new_step = dialog.get_step()
|
||||
self._tool.steps[idx] = new_step
|
||||
# Auto-add to dependencies if it's a ToolStep
|
||||
if isinstance(new_step, ToolStep):
|
||||
self._add_tool_dependency(new_step.tool)
|
||||
self._refresh_steps()
|
||||
|
||||
def _delete_step(self):
|
||||
|
||||
Reference in New Issue
Block a user