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 <noreply@anthropic.com>
This commit is contained in:
parent
05a2fae94c
commit
3f259449d4
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ from PySide6.QtGui import QKeyEvent, QAction
|
|||
|
||||
from NodeGraphQt import NodeGraph, BaseNode
|
||||
|
||||
from ...tool import Tool, PromptStep, CodeStep
|
||||
from ...tool import Tool, PromptStep, CodeStep, ToolStep
|
||||
from .icons import (
|
||||
get_prompt_icon_path, get_code_icon_path,
|
||||
get_input_icon_path, get_output_icon_path
|
||||
get_input_icon_path, get_output_icon_path, get_tool_icon_path
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -105,6 +105,33 @@ class CodeNode(CmdForgeBaseNode):
|
|||
self.output_ports()[0]._name = step.output_var or 'result'
|
||||
|
||||
|
||||
class ToolNode(CmdForgeBaseNode):
|
||||
"""Node representing a tool step (calling another tool)."""
|
||||
|
||||
NODE_NAME = 'Tool'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.set_color(159, 122, 234) # Purple
|
||||
self.set_icon(get_tool_icon_path(16))
|
||||
self.add_input('in', color=(220, 180, 250))
|
||||
self.add_output('out', color=(250, 180, 220))
|
||||
|
||||
# Add properties
|
||||
self.add_text_input('tool_ref', 'Tool', text='')
|
||||
self.add_text_input('output_var', 'Output', text='tool_output')
|
||||
|
||||
def set_step(self, step: ToolStep, index: int):
|
||||
"""Configure node from a ToolStep."""
|
||||
self._step_data = step
|
||||
self._step_index = index
|
||||
self.set_property('tool_ref', step.tool or '')
|
||||
self.set_property('output_var', step.output_var or 'tool_output')
|
||||
# Update output port name
|
||||
if self.output_ports():
|
||||
self.output_ports()[0]._name = step.output_var or 'tool_output'
|
||||
|
||||
|
||||
class OutputNode(CmdForgeBaseNode):
|
||||
"""Node representing tool output."""
|
||||
|
||||
|
|
@ -168,6 +195,7 @@ class FlowGraphWidget(QWidget):
|
|||
self._graph.register_node(InputNode)
|
||||
self._graph.register_node(PromptNode)
|
||||
self._graph.register_node(CodeNode)
|
||||
self._graph.register_node(ToolNode)
|
||||
self._graph.register_node(OutputNode)
|
||||
|
||||
# Connect signals
|
||||
|
|
@ -265,6 +293,7 @@ class FlowGraphWidget(QWidget):
|
|||
# Count steps by type for default naming
|
||||
prompt_count = 0
|
||||
code_count = 0
|
||||
tool_count = 0
|
||||
|
||||
for i, step in enumerate(self._tool.steps or []):
|
||||
if isinstance(step, PromptStep):
|
||||
|
|
@ -287,6 +316,16 @@ class FlowGraphWidget(QWidget):
|
|||
pos=[x_pos, 0]
|
||||
)
|
||||
node.set_step(step, i)
|
||||
elif isinstance(step, ToolStep):
|
||||
tool_count += 1
|
||||
# Use custom name if set, otherwise default to "Tool N" (per type)
|
||||
node_name = step.name if step.name else f'Tool {tool_count}'
|
||||
node = self._graph.create_node(
|
||||
'cmdforge.ToolNode',
|
||||
name=node_name,
|
||||
pos=[x_pos, 0]
|
||||
)
|
||||
node.set_step(step, i)
|
||||
else:
|
||||
continue
|
||||
|
||||
|
|
@ -443,7 +482,12 @@ class FlowGraphWidget(QWidget):
|
|||
def _on_node_double_clicked(self, node):
|
||||
"""Handle node double-click."""
|
||||
if hasattr(node, '_step_index') and node._step_index >= 0:
|
||||
step_type = 'prompt' if isinstance(node, PromptNode) else 'code'
|
||||
if isinstance(node, PromptNode):
|
||||
step_type = 'prompt'
|
||||
elif isinstance(node, ToolNode):
|
||||
step_type = 'tool'
|
||||
else:
|
||||
step_type = 'code'
|
||||
self.node_double_clicked.emit(node._step_index, step_type)
|
||||
|
||||
def _on_nodes_deleted(self, nodes):
|
||||
|
|
|
|||
|
|
@ -273,3 +273,63 @@ def get_output_icon(size: int = 24) -> QIcon:
|
|||
if key not in _icon_cache:
|
||||
_icon_cache[key] = create_output_icon(size)
|
||||
return _icon_cache[key]
|
||||
|
||||
|
||||
def create_tool_icon(size: int = 24, color: QColor = None) -> QIcon:
|
||||
"""Create a puzzle piece icon for tool steps (calling another tool).
|
||||
|
||||
Args:
|
||||
size: Icon size in pixels
|
||||
color: Icon color (defaults to purple #9f7aea)
|
||||
"""
|
||||
if color is None:
|
||||
color = QColor(159, 122, 234) # Purple
|
||||
|
||||
pixmap = QPixmap(size, size)
|
||||
pixmap.fill(Qt.transparent)
|
||||
|
||||
painter = QPainter(pixmap)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
|
||||
pen = QPen(color, 1.5)
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(QBrush(color))
|
||||
|
||||
# Draw a simple puzzle piece shape
|
||||
margin = size * 0.12
|
||||
w = size - 2 * margin
|
||||
h = size - 2 * margin
|
||||
|
||||
path = QPainterPath()
|
||||
# Start from top-left, going clockwise
|
||||
path.moveTo(margin, margin + h * 0.3)
|
||||
# Top edge with bump
|
||||
path.lineTo(margin + w * 0.35, margin + h * 0.3)
|
||||
path.arcTo(QRectF(margin + w * 0.35 - w * 0.1, margin, w * 0.3, h * 0.3), 180, -180)
|
||||
path.lineTo(margin + w, margin + h * 0.3)
|
||||
# Right edge
|
||||
path.lineTo(margin + w, margin + h * 0.7)
|
||||
# Bottom edge with notch
|
||||
path.lineTo(margin + w * 0.65, margin + h * 0.7)
|
||||
path.arcTo(QRectF(margin + w * 0.35, margin + h * 0.7, w * 0.3, h * 0.3), 0, 180)
|
||||
path.lineTo(margin, margin + h * 0.7)
|
||||
# Left edge back to start
|
||||
path.closeSubpath()
|
||||
|
||||
painter.drawPath(path)
|
||||
painter.end()
|
||||
|
||||
return QIcon(pixmap)
|
||||
|
||||
|
||||
def get_tool_icon(size: int = 24) -> QIcon:
|
||||
"""Get cached tool icon."""
|
||||
key = ('tool', size)
|
||||
if key not in _icon_cache:
|
||||
_icon_cache[key] = create_tool_icon(size)
|
||||
return _icon_cache[key]
|
||||
|
||||
|
||||
def get_tool_icon_path(size: int = 16) -> str:
|
||||
"""Get file path to tool icon (for NodeGraphQt)."""
|
||||
return _get_icon_path('tool', size, create_tool_icon)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from typing import Optional
|
|||
|
||||
from .tool import Tool, PromptStep, CodeStep, ToolStep
|
||||
from .providers import call_provider, mock_provider
|
||||
from .resolver import resolve_tool, ToolNotFoundError, ToolSpec
|
||||
from .resolver import resolve_tool, ToolNotFoundError, ToolSpec, install_from_registry
|
||||
from .manifest import load_manifest
|
||||
from .profiles import load_profile
|
||||
|
||||
|
|
@ -15,6 +15,36 @@ from .profiles import load_profile
|
|||
MAX_TOOL_DEPTH = 10
|
||||
|
||||
|
||||
def auto_install_dependencies(tool: Tool, verbose: bool = False) -> list[str]:
|
||||
"""
|
||||
Automatically install missing dependencies for a tool.
|
||||
|
||||
Args:
|
||||
tool: Tool to check and install dependencies for
|
||||
verbose: Show installation progress
|
||||
|
||||
Returns:
|
||||
List of successfully installed tool references
|
||||
"""
|
||||
missing = check_dependencies(tool)
|
||||
if not missing:
|
||||
return []
|
||||
|
||||
installed = []
|
||||
for dep in missing:
|
||||
try:
|
||||
if verbose:
|
||||
print(f"[auto-install] Installing {dep}...", file=sys.stderr)
|
||||
install_from_registry(dep)
|
||||
installed.append(dep)
|
||||
if verbose:
|
||||
print(f"[auto-install] Installed {dep}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to auto-install {dep}: {e}", file=sys.stderr)
|
||||
|
||||
return installed
|
||||
|
||||
|
||||
def check_dependencies(tool: Tool, checked: set = None) -> list[str]:
|
||||
"""
|
||||
Check if all dependencies for a tool are available.
|
||||
|
|
@ -293,6 +323,7 @@ def run_tool(
|
|||
dry_run: bool = False,
|
||||
show_prompt: bool = False,
|
||||
verbose: bool = False,
|
||||
auto_install: bool = False,
|
||||
_depth: int = 0,
|
||||
_call_stack: Optional[list] = None
|
||||
) -> tuple[str, int]:
|
||||
|
|
@ -307,6 +338,7 @@ def run_tool(
|
|||
dry_run: Just show what would happen
|
||||
show_prompt: Show prompts in addition to output
|
||||
verbose: Show debug info
|
||||
auto_install: Automatically install missing dependencies
|
||||
_depth: Internal recursion depth tracker
|
||||
_call_stack: Internal call stack for error reporting
|
||||
|
||||
|
|
@ -322,8 +354,19 @@ def run_tool(
|
|||
if _depth == 0 and (tool.dependencies or any(isinstance(s, ToolStep) for s in tool.steps)):
|
||||
missing = check_dependencies(tool)
|
||||
if missing:
|
||||
if auto_install:
|
||||
# Try to auto-install missing dependencies
|
||||
installed = auto_install_dependencies(tool, verbose=verbose)
|
||||
# Re-check after installation
|
||||
still_missing = check_dependencies(tool)
|
||||
if still_missing:
|
||||
print(f"Warning: Could not install all dependencies. Missing: {', '.join(still_missing)}", file=sys.stderr)
|
||||
elif installed:
|
||||
print(f"Auto-installed dependencies: {', '.join(installed)}", file=sys.stderr)
|
||||
else:
|
||||
print(f"Warning: Missing dependencies: {', '.join(missing)}", file=sys.stderr)
|
||||
print(f"Install with: cmdforge install {' '.join(missing)}", file=sys.stderr)
|
||||
print(f"Or use --auto-install to install automatically", file=sys.stderr)
|
||||
# Continue anyway - the actual step execution will fail with a better error
|
||||
|
||||
# Initialize variables with input and arguments
|
||||
|
|
@ -448,6 +491,8 @@ def create_argument_parser(tool: Tool) -> argparse.ArgumentParser:
|
|||
help="Override provider (e.g., --provider mock)")
|
||||
parser.add_argument("-v", "--verbose", action="store_true",
|
||||
help="Show debug information")
|
||||
parser.add_argument("--auto-install", action="store_true",
|
||||
help="Automatically install missing tool dependencies")
|
||||
|
||||
# Tool-specific flags from arguments
|
||||
for arg in tool.arguments:
|
||||
|
|
@ -527,7 +572,8 @@ def main():
|
|||
provider_override=effective_provider,
|
||||
dry_run=args.dry_run,
|
||||
show_prompt=args.show_prompt,
|
||||
verbose=args.verbose
|
||||
verbose=args.verbose,
|
||||
auto_install=args.auto_install
|
||||
)
|
||||
|
||||
# Write output
|
||||
|
|
|
|||
Loading…
Reference in New Issue