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:
rob
2026-01-17 05:27:54 -04:00
co-authored by Claude Opus 4.5
parent 05a2fae94c
commit 3f259449d4
5 changed files with 435 additions and 11 deletions
+62 -3
View File
@@ -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):