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
+50 -4
View File
@@ -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:
print(f"Warning: Missing dependencies: {', '.join(missing)}", file=sys.stderr)
print(f"Install with: cmdforge install {' '.join(missing)}", file=sys.stderr)
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