Add meta-tools, collections, and attribution features
Meta-tools: - Add ToolStep dataclass for tools calling other tools - Implement execute_tool_step() with recursion depth limiting - Add check_dependencies() for validating tool dependencies - Add 'cmdforge check' command for dependency verification Collections: - Add collections API endpoints and database schema - Create collections web UI (list and detail pages) - Add collections to navigation header and homepage - Document collections in REGISTRY.md Attribution: - Add source attribution fields to tool detail page - Document source types (original, adapted, imported) - Add license field documentation Documentation updates across README, DESIGN.md, REGISTRY.md, and new META_TOOLS.md design document. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
+160
-3
@@ -5,11 +5,63 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from .tool import Tool, PromptStep, CodeStep
|
||||
from .tool import Tool, PromptStep, CodeStep, ToolStep
|
||||
from .providers import call_provider, mock_provider
|
||||
from .resolver import resolve_tool, ToolNotFoundError, ToolSpec
|
||||
from .manifest import load_manifest
|
||||
|
||||
# Maximum recursion depth for nested tool calls
|
||||
MAX_TOOL_DEPTH = 10
|
||||
|
||||
|
||||
def check_dependencies(tool: Tool, checked: set = None) -> list[str]:
|
||||
"""
|
||||
Check if all dependencies for a tool are available.
|
||||
|
||||
Args:
|
||||
tool: Tool to check dependencies for
|
||||
checked: Set of already checked tools (prevents infinite loops)
|
||||
|
||||
Returns:
|
||||
List of missing dependency tool references
|
||||
"""
|
||||
if checked is None:
|
||||
checked = set()
|
||||
|
||||
missing = []
|
||||
|
||||
# Check explicit dependencies
|
||||
for dep in tool.dependencies:
|
||||
if dep in checked:
|
||||
continue
|
||||
checked.add(dep)
|
||||
|
||||
try:
|
||||
resolved = resolve_tool(dep)
|
||||
# Recursively check nested dependencies
|
||||
nested_missing = check_dependencies(resolved.tool, checked)
|
||||
missing.extend(nested_missing)
|
||||
except ToolNotFoundError:
|
||||
missing.append(dep)
|
||||
|
||||
# Also check tool steps for implicit dependencies
|
||||
for step in tool.steps:
|
||||
if isinstance(step, ToolStep):
|
||||
tool_ref = step.tool
|
||||
if tool_ref in checked:
|
||||
continue
|
||||
checked.add(tool_ref)
|
||||
|
||||
try:
|
||||
resolved = resolve_tool(tool_ref)
|
||||
# Recursively check nested dependencies
|
||||
nested_missing = check_dependencies(resolved.tool, checked)
|
||||
missing.extend(nested_missing)
|
||||
except ToolNotFoundError:
|
||||
missing.append(tool_ref)
|
||||
|
||||
return missing
|
||||
|
||||
|
||||
def substitute_variables(template: str, variables: dict) -> str:
|
||||
"""
|
||||
@@ -111,6 +163,75 @@ def execute_code_step(step: CodeStep, variables: dict) -> tuple[dict, bool]:
|
||||
return {}, False
|
||||
|
||||
|
||||
def execute_tool_step(
|
||||
step: ToolStep,
|
||||
variables: dict,
|
||||
depth: int = 0,
|
||||
provider_override: Optional[str] = None,
|
||||
dry_run: bool = False,
|
||||
verbose: bool = False
|
||||
) -> tuple[str, bool]:
|
||||
"""
|
||||
Execute a tool step by calling another tool.
|
||||
|
||||
Args:
|
||||
step: The tool step to execute
|
||||
variables: Current variable values
|
||||
depth: Current recursion depth
|
||||
provider_override: Override provider for nested calls
|
||||
dry_run: Just show what would happen
|
||||
verbose: Show debug info
|
||||
|
||||
Returns:
|
||||
Tuple of (output_value, success)
|
||||
"""
|
||||
if depth >= MAX_TOOL_DEPTH:
|
||||
print(f"Error: Maximum tool nesting depth ({MAX_TOOL_DEPTH}) exceeded", file=sys.stderr)
|
||||
return "", False
|
||||
|
||||
# Resolve the tool reference
|
||||
try:
|
||||
resolved = resolve_tool(step.tool)
|
||||
nested_tool = resolved.tool
|
||||
except ToolNotFoundError as e:
|
||||
print(f"Error: Tool '{step.tool}' not found", file=sys.stderr)
|
||||
print(f"Hint: Install it with: cmdforge install {step.tool}", file=sys.stderr)
|
||||
return "", False
|
||||
|
||||
# Prepare input by substituting variables
|
||||
input_text = substitute_variables(step.input_template, variables)
|
||||
|
||||
# Prepare arguments by substituting variables in arg values
|
||||
custom_args = {}
|
||||
for key, value in step.args.items():
|
||||
custom_args[key] = substitute_variables(str(value), variables)
|
||||
|
||||
# Determine effective provider (step override > parent override)
|
||||
effective_provider = step.provider or provider_override
|
||||
|
||||
if verbose:
|
||||
print(f"[verbose] Tool step: calling {step.tool}", file=sys.stderr)
|
||||
print(f"[verbose] Input length: {len(input_text)} chars", file=sys.stderr)
|
||||
print(f"[verbose] Args: {list(custom_args.keys())}", file=sys.stderr)
|
||||
|
||||
if dry_run:
|
||||
return f"[DRY RUN - would call tool {step.tool}]", True
|
||||
|
||||
# Run the nested tool
|
||||
output, exit_code = run_tool(
|
||||
tool=nested_tool,
|
||||
input_text=input_text,
|
||||
custom_args=custom_args,
|
||||
provider_override=effective_provider,
|
||||
dry_run=dry_run,
|
||||
show_prompt=False,
|
||||
verbose=verbose,
|
||||
_depth=depth + 1 # Track nesting depth
|
||||
)
|
||||
|
||||
return output, exit_code == 0
|
||||
|
||||
|
||||
def run_tool(
|
||||
tool: Tool,
|
||||
input_text: str,
|
||||
@@ -118,7 +239,8 @@ def run_tool(
|
||||
provider_override: Optional[str] = None,
|
||||
dry_run: bool = False,
|
||||
show_prompt: bool = False,
|
||||
verbose: bool = False
|
||||
verbose: bool = False,
|
||||
_depth: int = 0 # Internal: tracks nesting depth for tool steps
|
||||
) -> tuple[str, int]:
|
||||
"""
|
||||
Execute a tool.
|
||||
@@ -131,10 +253,19 @@ def run_tool(
|
||||
dry_run: Just show what would happen
|
||||
show_prompt: Show prompts in addition to output
|
||||
verbose: Show debug info
|
||||
_depth: Internal recursion depth tracker
|
||||
|
||||
Returns:
|
||||
Tuple of (output_text, exit_code)
|
||||
"""
|
||||
# Check dependencies on first level only (not for nested calls)
|
||||
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)
|
||||
# Continue anyway - the actual step execution will fail with a better error
|
||||
|
||||
# Initialize variables with input and arguments
|
||||
variables = {"input": input_text}
|
||||
|
||||
@@ -156,7 +287,14 @@ def run_tool(
|
||||
# Execute each step
|
||||
for i, step in enumerate(tool.steps):
|
||||
if verbose:
|
||||
step_type = "PROMPT" if isinstance(step, PromptStep) else "CODE"
|
||||
if isinstance(step, PromptStep):
|
||||
step_type = "PROMPT"
|
||||
elif isinstance(step, CodeStep):
|
||||
step_type = "CODE"
|
||||
elif isinstance(step, ToolStep):
|
||||
step_type = f"TOOL({step.tool})"
|
||||
else:
|
||||
step_type = "UNKNOWN"
|
||||
print(f"[verbose] Step {i+1}: {step_type} -> {{{step.output_var}}}", file=sys.stderr)
|
||||
|
||||
if isinstance(step, PromptStep):
|
||||
@@ -192,6 +330,25 @@ def run_tool(
|
||||
# Merge all output vars into variables
|
||||
variables.update(outputs)
|
||||
|
||||
elif isinstance(step, ToolStep):
|
||||
if verbose or dry_run:
|
||||
print(f"=== TOOL (Step {i+1}) -> {{{step.output_var}}} ===", file=sys.stderr)
|
||||
print(f" Calling: {step.tool}", file=sys.stderr)
|
||||
print(f" Args: {step.args}", file=sys.stderr)
|
||||
print("=== END TOOL ===", file=sys.stderr)
|
||||
|
||||
output, success = execute_tool_step(
|
||||
step,
|
||||
variables,
|
||||
depth=_depth,
|
||||
provider_override=provider_override,
|
||||
dry_run=dry_run,
|
||||
verbose=verbose
|
||||
)
|
||||
if not success:
|
||||
return "", 3
|
||||
variables[step.output_var] = output
|
||||
|
||||
# Generate final output
|
||||
output = substitute_variables(tool.output, variables)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user