Add comprehensive documentation and 28 example tools
- Updated README with full feature overview and examples - Added docs/INSTALL.md with detailed setup instructions - Added docs/PROVIDERS.md with 12 profiled providers - Added docs/EXAMPLES.md with all 28 tool configurations - Added examples/install.py for one-command tool installation - Updated pyproject.toml with better metadata - Added urwid TUI with provider descriptions in dropdown - Profiled all providers for speed/accuracy/cost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
This commit is contained in:
+633
-148
@@ -2,9 +2,34 @@
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Optional, Tuple
|
||||
import tempfile
|
||||
from typing import Optional, Tuple, List
|
||||
|
||||
from .tool import Tool, ToolInput, list_tools, load_tool, save_tool, delete_tool, tool_exists
|
||||
from .tool import (
|
||||
Tool, ToolArgument, PromptStep, CodeStep, Step,
|
||||
list_tools, load_tool, save_tool, delete_tool, tool_exists
|
||||
)
|
||||
from .providers import Provider, load_providers, add_provider, delete_provider, get_provider
|
||||
|
||||
|
||||
def _check_urwid() -> bool:
|
||||
"""Check if urwid is available (preferred - has mouse support)."""
|
||||
try:
|
||||
import urwid
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
def _check_snack() -> bool:
|
||||
"""Check if snack (python3-newt) is available."""
|
||||
try:
|
||||
if '/usr/lib/python3/dist-packages' not in sys.path:
|
||||
sys.path.insert(0, '/usr/lib/python3/dist-packages')
|
||||
import snack
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
def check_dialog() -> str:
|
||||
@@ -19,35 +44,30 @@ def check_dialog() -> str:
|
||||
|
||||
|
||||
def run_dialog(args: list[str], dialog_prog: str = "dialog") -> Tuple[int, str]:
|
||||
"""
|
||||
Run a dialog command and return (exit_code, output).
|
||||
|
||||
Args:
|
||||
args: Dialog arguments
|
||||
dialog_prog: Dialog program to use
|
||||
|
||||
Returns:
|
||||
Tuple of (exit_code, captured_output)
|
||||
"""
|
||||
cmd = [dialog_prog] + args
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
# dialog outputs to stderr
|
||||
return result.returncode, result.stderr.strip()
|
||||
"""Run a dialog command and return (exit_code, output)."""
|
||||
try:
|
||||
if dialog_prog == "whiptail":
|
||||
result = subprocess.run(
|
||||
[dialog_prog] + args,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
return result.returncode, result.stderr.strip()
|
||||
else:
|
||||
cmd_with_stdout = [dialog_prog, "--stdout"] + args
|
||||
result = subprocess.run(
|
||||
cmd_with_stdout,
|
||||
stdout=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
return result.returncode, result.stdout.strip()
|
||||
except Exception as e:
|
||||
return 1, ""
|
||||
|
||||
|
||||
def show_menu(title: str, choices: list[tuple[str, str]], dialog_prog: str) -> Optional[str]:
|
||||
"""
|
||||
Show a menu and return the selected item.
|
||||
|
||||
Args:
|
||||
title: Menu title
|
||||
choices: List of (tag, description) tuples
|
||||
dialog_prog: Dialog program to use
|
||||
|
||||
Returns:
|
||||
Selected tag or None if cancelled
|
||||
"""
|
||||
args = ["--title", title, "--menu", "", "15", "60", str(len(choices))]
|
||||
"""Show a menu and return the selected item."""
|
||||
args = ["--title", title, "--menu", "Choose an option:", "20", "75", str(len(choices))]
|
||||
for tag, desc in choices:
|
||||
args.extend([tag, desc])
|
||||
|
||||
@@ -56,61 +76,29 @@ def show_menu(title: str, choices: list[tuple[str, str]], dialog_prog: str) -> O
|
||||
|
||||
|
||||
def show_input(title: str, prompt: str, initial: str = "", dialog_prog: str = "dialog") -> Optional[str]:
|
||||
"""
|
||||
Show an input box and return the entered text.
|
||||
|
||||
Args:
|
||||
title: Dialog title
|
||||
prompt: Prompt text
|
||||
initial: Initial value
|
||||
dialog_prog: Dialog program to use
|
||||
|
||||
Returns:
|
||||
Entered text or None if cancelled
|
||||
"""
|
||||
"""Show an input box and return the entered text."""
|
||||
args = ["--title", title, "--inputbox", prompt, "10", "60", initial]
|
||||
code, output = run_dialog(args, dialog_prog)
|
||||
return output if code == 0 else None
|
||||
|
||||
|
||||
def show_textbox(title: str, text: str, dialog_prog: str = "dialog") -> Optional[str]:
|
||||
"""
|
||||
Show a text editor for multi-line input.
|
||||
|
||||
Args:
|
||||
title: Dialog title
|
||||
text: Initial text
|
||||
dialog_prog: Dialog program to use
|
||||
|
||||
Returns:
|
||||
Edited text or None if cancelled
|
||||
"""
|
||||
import tempfile
|
||||
"""Show a text editor for multi-line input."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
||||
f.write(text)
|
||||
temp_path = f.name
|
||||
|
||||
args = ["--title", title, "--editbox", temp_path, "20", "70"]
|
||||
code, output = run_dialog(args, dialog_prog)
|
||||
|
||||
import os
|
||||
os.unlink(temp_path)
|
||||
|
||||
return output if code == 0 else None
|
||||
try:
|
||||
args = ["--title", title, "--editbox", temp_path, "20", "75"]
|
||||
code, output = run_dialog(args, dialog_prog)
|
||||
return output if code == 0 else None
|
||||
finally:
|
||||
import os
|
||||
os.unlink(temp_path)
|
||||
|
||||
|
||||
def show_yesno(title: str, text: str, dialog_prog: str = "dialog") -> bool:
|
||||
"""
|
||||
Show a yes/no dialog.
|
||||
|
||||
Args:
|
||||
title: Dialog title
|
||||
text: Question text
|
||||
dialog_prog: Dialog program to use
|
||||
|
||||
Returns:
|
||||
True if yes, False if no
|
||||
"""
|
||||
"""Show a yes/no dialog."""
|
||||
args = ["--title", title, "--yesno", text, "10", "60"]
|
||||
code, _ = run_dialog(args, dialog_prog)
|
||||
return code == 0
|
||||
@@ -118,84 +106,525 @@ def show_yesno(title: str, text: str, dialog_prog: str = "dialog") -> bool:
|
||||
|
||||
def show_message(title: str, text: str, dialog_prog: str = "dialog"):
|
||||
"""Show a message box."""
|
||||
args = ["--title", title, "--msgbox", text, "10", "60"]
|
||||
args = ["--title", title, "--msgbox", text, "15", "70"]
|
||||
run_dialog(args, dialog_prog)
|
||||
|
||||
|
||||
def create_tool_form(dialog_prog: str, existing: Optional[Tool] = None) -> Optional[Tool]:
|
||||
"""
|
||||
Show form for creating/editing a tool.
|
||||
def show_mixed_form(title: str, fields: dict, dialog_prog: str, height: int = 20) -> Optional[dict]:
|
||||
"""Show a form with multiple fields using --mixedform."""
|
||||
args = ["--title", title, "--mixedform",
|
||||
"Tab: next field | Enter: submit | Esc: cancel",
|
||||
str(height), "75", "0"]
|
||||
|
||||
Args:
|
||||
dialog_prog: Dialog program to use
|
||||
existing: Existing tool to edit (None for new)
|
||||
field_names = list(fields.keys())
|
||||
y = 1
|
||||
for name in field_names:
|
||||
label, initial, field_type = fields[name]
|
||||
args.extend([
|
||||
label, str(y), "1",
|
||||
initial, str(y), "18",
|
||||
"52", "256", str(field_type)
|
||||
])
|
||||
y += 1
|
||||
|
||||
code, output = run_dialog(args, dialog_prog)
|
||||
|
||||
if code != 0:
|
||||
return None
|
||||
|
||||
values = output.split('\n')
|
||||
result = {}
|
||||
for i, name in enumerate(field_names):
|
||||
result[name] = values[i] if i < len(values) else ""
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ============ Provider Management ============
|
||||
|
||||
def select_provider(dialog_prog: str) -> Optional[str]:
|
||||
"""Show provider selection menu with option to create new."""
|
||||
providers = load_providers()
|
||||
|
||||
choices = [(p.name, f"{p.description} ({p.command})") for p in providers]
|
||||
choices.append(("__new__", "[ + Add New Provider ]"))
|
||||
|
||||
selected = show_menu("Select Provider", choices, dialog_prog)
|
||||
|
||||
if selected == "__new__":
|
||||
provider = create_provider_form(dialog_prog)
|
||||
if provider:
|
||||
add_provider(provider)
|
||||
return provider.name
|
||||
return None
|
||||
|
||||
return selected
|
||||
|
||||
|
||||
def create_provider_form(dialog_prog: str, existing: Optional[Provider] = None) -> Optional[Provider]:
|
||||
"""Show form for creating/editing a provider."""
|
||||
title = f"Edit Provider: {existing.name}" if existing else "Add New Provider"
|
||||
|
||||
fields = {
|
||||
"name": (
|
||||
"Name:",
|
||||
existing.name if existing else "",
|
||||
2 if existing else 0 # readonly if editing
|
||||
),
|
||||
"command": (
|
||||
"Command:",
|
||||
existing.command if existing else "",
|
||||
0
|
||||
),
|
||||
"description": (
|
||||
"Description:",
|
||||
existing.description if existing else "",
|
||||
0
|
||||
),
|
||||
}
|
||||
|
||||
result = show_mixed_form(title, fields, dialog_prog, height=12)
|
||||
|
||||
if not result:
|
||||
return None
|
||||
|
||||
name = result["name"].strip()
|
||||
command = result["command"].strip()
|
||||
|
||||
Returns:
|
||||
Tool object or None if cancelled
|
||||
"""
|
||||
# Get basic info
|
||||
name = show_input(
|
||||
"Tool Name",
|
||||
"Enter tool name (used as command):",
|
||||
existing.name if existing else "",
|
||||
dialog_prog
|
||||
)
|
||||
if not name:
|
||||
show_message("Error", "Provider name is required.", dialog_prog)
|
||||
return None
|
||||
|
||||
# Check if name already exists (for new tools)
|
||||
if not existing and tool_exists(name):
|
||||
if not show_yesno("Overwrite?", f"Tool '{name}' already exists. Overwrite?", dialog_prog):
|
||||
return None
|
||||
|
||||
description = show_input(
|
||||
"Description",
|
||||
"Enter tool description:",
|
||||
existing.description if existing else "",
|
||||
dialog_prog
|
||||
)
|
||||
if description is None:
|
||||
if not command:
|
||||
show_message("Error", "Command is required.", dialog_prog)
|
||||
return None
|
||||
|
||||
provider = show_input(
|
||||
"Provider",
|
||||
"Enter AI provider command (e.g., codex, claude, gemini):",
|
||||
existing.provider if existing else "codex",
|
||||
dialog_prog
|
||||
)
|
||||
if not provider:
|
||||
return None
|
||||
|
||||
provider_args = show_input(
|
||||
"Provider Args",
|
||||
"Enter provider arguments (e.g., -p):",
|
||||
existing.provider_args if existing else "-p",
|
||||
dialog_prog
|
||||
)
|
||||
if provider_args is None:
|
||||
return None
|
||||
|
||||
# Edit prompt
|
||||
default_prompt = existing.prompt if existing else "Process the following input:\n\n{input}"
|
||||
prompt = show_textbox("Prompt Template", default_prompt, dialog_prog)
|
||||
if not prompt:
|
||||
return None
|
||||
|
||||
# For simplicity, skip custom inputs in the UI for now
|
||||
# They can be added by editing config.yaml directly
|
||||
inputs = existing.inputs if existing else []
|
||||
|
||||
return Tool(
|
||||
return Provider(
|
||||
name=name,
|
||||
description=description,
|
||||
command=command,
|
||||
description=result["description"].strip()
|
||||
)
|
||||
|
||||
|
||||
def ui_manage_providers(dialog_prog: str):
|
||||
"""Manage providers menu."""
|
||||
while True:
|
||||
providers = load_providers()
|
||||
choices = [(p.name, f"{p.command}") for p in providers]
|
||||
choices.append(("__add__", "[ + Add New Provider ]"))
|
||||
choices.append(("__back__", "[ <- Back to Main Menu ]"))
|
||||
|
||||
selected = show_menu("Manage Providers", choices, dialog_prog)
|
||||
|
||||
if selected is None or selected == "__back__":
|
||||
break
|
||||
elif selected == "__add__":
|
||||
provider = create_provider_form(dialog_prog)
|
||||
if provider:
|
||||
add_provider(provider)
|
||||
show_message("Success", f"Provider '{provider.name}' added.", dialog_prog)
|
||||
else:
|
||||
# Edit or delete existing provider
|
||||
provider = get_provider(selected)
|
||||
if provider:
|
||||
action = show_menu(
|
||||
f"Provider: {selected}",
|
||||
[
|
||||
("edit", "Edit provider"),
|
||||
("delete", "Delete provider"),
|
||||
("back", "Back"),
|
||||
],
|
||||
dialog_prog
|
||||
)
|
||||
|
||||
if action == "edit":
|
||||
updated = create_provider_form(dialog_prog, provider)
|
||||
if updated:
|
||||
add_provider(updated)
|
||||
show_message("Success", f"Provider '{updated.name}' updated.", dialog_prog)
|
||||
elif action == "delete":
|
||||
if show_yesno("Confirm", f"Delete provider '{selected}'?", dialog_prog):
|
||||
delete_provider(selected)
|
||||
show_message("Deleted", f"Provider '{selected}' deleted.", dialog_prog)
|
||||
|
||||
|
||||
# ============ Tool Builder UI ============
|
||||
|
||||
def format_tool_summary(tool: Tool) -> str:
|
||||
"""Format a summary of the tool's components."""
|
||||
lines = []
|
||||
lines.append(f"Name: {tool.name}")
|
||||
lines.append(f"Description: {tool.description or '(none)'}")
|
||||
lines.append("")
|
||||
|
||||
if tool.arguments:
|
||||
lines.append("Arguments:")
|
||||
for arg in tool.arguments:
|
||||
default = f" = {arg.default}" if arg.default else ""
|
||||
lines.append(f" {arg.flag} -> {{{arg.variable}}}{default}")
|
||||
lines.append("")
|
||||
|
||||
if tool.steps:
|
||||
lines.append("Steps:")
|
||||
for i, step in enumerate(tool.steps):
|
||||
if isinstance(step, PromptStep):
|
||||
preview = step.prompt[:40].replace('\n', ' ') + "..."
|
||||
lines.append(f" {i+1}. PROMPT [{step.provider}] -> {{{step.output_var}}}")
|
||||
lines.append(f" {preview}")
|
||||
elif isinstance(step, CodeStep):
|
||||
preview = step.code[:40].replace('\n', ' ') + "..."
|
||||
lines.append(f" {i+1}. CODE -> {{{step.output_var}}}")
|
||||
lines.append(f" {preview}")
|
||||
lines.append("")
|
||||
|
||||
lines.append(f"Output: {tool.output}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def get_available_variables(tool: Tool, up_to_step: int = -1) -> List[str]:
|
||||
"""Get list of available variables at a given point in the tool."""
|
||||
variables = ["input"]
|
||||
|
||||
for arg in tool.arguments:
|
||||
variables.append(arg.variable)
|
||||
|
||||
if up_to_step == -1:
|
||||
up_to_step = len(tool.steps)
|
||||
|
||||
for i, step in enumerate(tool.steps):
|
||||
if i >= up_to_step:
|
||||
break
|
||||
variables.append(step.output_var)
|
||||
|
||||
return variables
|
||||
|
||||
|
||||
def edit_argument(dialog_prog: str, existing: Optional[ToolArgument] = None) -> Optional[ToolArgument]:
|
||||
"""Edit or create an argument."""
|
||||
title = f"Edit Argument: {existing.flag}" if existing else "Add Argument"
|
||||
|
||||
fields = {
|
||||
"flag": ("Flag:", existing.flag if existing else "--", 0),
|
||||
"variable": ("Variable:", existing.variable if existing else "", 0),
|
||||
"default": ("Default:", existing.default or "" if existing else "", 0),
|
||||
"description": ("Description:", existing.description if existing else "", 0),
|
||||
}
|
||||
|
||||
result = show_mixed_form(title, fields, dialog_prog, height=14)
|
||||
if not result:
|
||||
return None
|
||||
|
||||
flag = result["flag"].strip()
|
||||
variable = result["variable"].strip()
|
||||
|
||||
if not flag:
|
||||
show_message("Error", "Flag is required (e.g., --max-size).", dialog_prog)
|
||||
return None
|
||||
|
||||
if not variable:
|
||||
# Auto-generate variable name from flag
|
||||
variable = flag.lstrip("-").replace("-", "_")
|
||||
|
||||
return ToolArgument(
|
||||
flag=flag,
|
||||
variable=variable,
|
||||
default=result["default"].strip() or None,
|
||||
description=result["description"].strip()
|
||||
)
|
||||
|
||||
|
||||
def edit_prompt_step(dialog_prog: str, existing: Optional[PromptStep] = None,
|
||||
available_vars: List[str] = None) -> Optional[PromptStep]:
|
||||
"""Edit or create a prompt step."""
|
||||
title = "Edit Prompt Step" if existing else "Add Prompt Step"
|
||||
|
||||
# First, select provider
|
||||
provider = select_provider(dialog_prog)
|
||||
if not provider:
|
||||
provider = existing.provider if existing else "mock"
|
||||
|
||||
# Show variable help
|
||||
var_help = "Available: " + ", ".join(f"{{{v}}}" for v in (available_vars or ["input"]))
|
||||
|
||||
# Edit prompt text
|
||||
default_prompt = existing.prompt if existing else f"Process this input:\n\n{{input}}"
|
||||
prompt = show_textbox(f"Prompt Template\n{var_help}", default_prompt, dialog_prog)
|
||||
if prompt is None:
|
||||
return None
|
||||
|
||||
# Get output variable
|
||||
output_var = show_input(
|
||||
"Output Variable",
|
||||
"Variable name to store the result:",
|
||||
existing.output_var if existing else "result",
|
||||
dialog_prog
|
||||
)
|
||||
if not output_var:
|
||||
return None
|
||||
|
||||
return PromptStep(
|
||||
prompt=prompt,
|
||||
provider=provider,
|
||||
provider_args=provider_args,
|
||||
inputs=inputs,
|
||||
post_process=existing.post_process if existing else None
|
||||
output_var=output_var.strip()
|
||||
)
|
||||
|
||||
|
||||
def edit_code_step(dialog_prog: str, existing: Optional[CodeStep] = None,
|
||||
available_vars: List[str] = None) -> Optional[CodeStep]:
|
||||
"""Edit or create a code step."""
|
||||
title = "Edit Code Step" if existing else "Add Code Step"
|
||||
|
||||
# Show variable help
|
||||
var_help = "Variables: " + ", ".join(available_vars or ["input"])
|
||||
var_help += "\nSet 'result' variable for output"
|
||||
|
||||
# Edit code
|
||||
default_code = existing.code if existing else "# Available variables: " + ", ".join(available_vars or ["input"]) + "\n# Set 'result' for output\nresult = input.upper()"
|
||||
code = show_textbox(f"Python Code\n{var_help}", default_code, dialog_prog)
|
||||
if code is None:
|
||||
return None
|
||||
|
||||
# Get output variable
|
||||
output_var = show_input(
|
||||
"Output Variable",
|
||||
"Variable name to store the result:",
|
||||
existing.output_var if existing else "processed",
|
||||
dialog_prog
|
||||
)
|
||||
if not output_var:
|
||||
return None
|
||||
|
||||
return CodeStep(
|
||||
code=code,
|
||||
output_var=output_var.strip()
|
||||
)
|
||||
|
||||
|
||||
def edit_tool_info(tool: Tool, is_edit: bool, dialog_prog: str) -> None:
|
||||
"""Edit basic tool info (name, description, output)."""
|
||||
while True:
|
||||
# Build info section menu
|
||||
output_preview = tool.output[:35] + "..." if len(tool.output) > 35 else tool.output
|
||||
args_count = len(tool.arguments)
|
||||
args_summary = f"({args_count} defined)" if args_count else "(none)"
|
||||
|
||||
choices = [
|
||||
("name", f"Name: {tool.name or '(not set)'}"),
|
||||
("desc", f"Description: {tool.description[:35] + '...' if len(tool.description) > 35 else tool.description or '(none)'}"),
|
||||
("---1", "─" * 40),
|
||||
]
|
||||
|
||||
# Show arguments
|
||||
if tool.arguments:
|
||||
for i, arg in enumerate(tool.arguments):
|
||||
default = f" = {arg.default}" if arg.default else ""
|
||||
choices.append((f"arg_{i}", f" {arg.flag} -> {{{arg.variable}}}{default}"))
|
||||
choices.append(("add_arg", " [ + Add Argument ]"))
|
||||
|
||||
choices.append(("---2", "─" * 40))
|
||||
choices.append(("output", f"Output Template: {output_preview}"))
|
||||
choices.append(("---3", "─" * 40))
|
||||
choices.append(("back", "<- Back to Tool Builder"))
|
||||
|
||||
selected = show_menu("Tool Info & Arguments", choices, dialog_prog)
|
||||
|
||||
if selected is None or selected == "back":
|
||||
break
|
||||
|
||||
elif selected == "name":
|
||||
if is_edit:
|
||||
show_message("Info", "Cannot change tool name after creation.", dialog_prog)
|
||||
else:
|
||||
new_name = show_input("Tool Name", "Enter tool name:", tool.name, dialog_prog)
|
||||
if new_name:
|
||||
tool.name = new_name.strip()
|
||||
|
||||
elif selected == "desc":
|
||||
new_desc = show_input("Description", "Enter tool description:", tool.description, dialog_prog)
|
||||
if new_desc is not None:
|
||||
tool.description = new_desc.strip()
|
||||
|
||||
elif selected == "add_arg":
|
||||
arg = edit_argument(dialog_prog)
|
||||
if arg:
|
||||
tool.arguments.append(arg)
|
||||
|
||||
elif selected.startswith("arg_"):
|
||||
idx = int(selected[4:])
|
||||
arg = tool.arguments[idx]
|
||||
action = show_menu(
|
||||
f"Argument: {arg.flag}",
|
||||
[("edit", "Edit"), ("delete", "Delete"), ("back", "Back")],
|
||||
dialog_prog
|
||||
)
|
||||
if action == "edit":
|
||||
updated = edit_argument(dialog_prog, arg)
|
||||
if updated:
|
||||
tool.arguments[idx] = updated
|
||||
elif action == "delete":
|
||||
if show_yesno("Delete", f"Delete argument {arg.flag}?", dialog_prog):
|
||||
tool.arguments.pop(idx)
|
||||
|
||||
elif selected == "output":
|
||||
available = get_available_variables(tool)
|
||||
var_help = "Variables: " + ", ".join(f"{{{v}}}" for v in available)
|
||||
new_output = show_textbox(f"Output Template\n{var_help}", tool.output, dialog_prog)
|
||||
if new_output is not None:
|
||||
tool.output = new_output
|
||||
|
||||
|
||||
def edit_tool_steps(tool: Tool, dialog_prog: str) -> None:
|
||||
"""Edit tool processing steps."""
|
||||
while True:
|
||||
# Build steps section menu
|
||||
choices = []
|
||||
|
||||
if tool.steps:
|
||||
for i, step in enumerate(tool.steps):
|
||||
if isinstance(step, PromptStep):
|
||||
choices.append((f"step_{i}", f"{i+1}. PROMPT [{step.provider}] -> {{{step.output_var}}}"))
|
||||
elif isinstance(step, CodeStep):
|
||||
choices.append((f"step_{i}", f"{i+1}. CODE -> {{{step.output_var}}}"))
|
||||
else:
|
||||
choices.append(("none", "(no steps defined)"))
|
||||
|
||||
choices.append(("---1", "─" * 40))
|
||||
choices.append(("add_prompt", "[ + Add Prompt Step ]"))
|
||||
choices.append(("add_code", "[ + Add Code Step ]"))
|
||||
choices.append(("---2", "─" * 40))
|
||||
choices.append(("back", "<- Back to Tool Builder"))
|
||||
|
||||
selected = show_menu("Processing Steps", choices, dialog_prog)
|
||||
|
||||
if selected is None or selected == "back" or selected == "none":
|
||||
if selected == "none":
|
||||
continue
|
||||
break
|
||||
|
||||
elif selected == "add_prompt":
|
||||
available = get_available_variables(tool)
|
||||
step = edit_prompt_step(dialog_prog, available_vars=available)
|
||||
if step:
|
||||
tool.steps.append(step)
|
||||
|
||||
elif selected == "add_code":
|
||||
available = get_available_variables(tool)
|
||||
step = edit_code_step(dialog_prog, available_vars=available)
|
||||
if step:
|
||||
tool.steps.append(step)
|
||||
|
||||
elif selected.startswith("step_"):
|
||||
idx = int(selected[5:])
|
||||
step = tool.steps[idx]
|
||||
step_type = "Prompt" if isinstance(step, PromptStep) else "Code"
|
||||
|
||||
move_choices = [("edit", "Edit"), ("delete", "Delete")]
|
||||
if idx > 0:
|
||||
move_choices.insert(1, ("move_up", "Move Up"))
|
||||
if idx < len(tool.steps) - 1:
|
||||
move_choices.insert(2 if idx > 0 else 1, ("move_down", "Move Down"))
|
||||
move_choices.append(("back", "Back"))
|
||||
|
||||
action = show_menu(f"Step {idx+1}: {step_type}", move_choices, dialog_prog)
|
||||
|
||||
if action == "edit":
|
||||
available = get_available_variables(tool, idx)
|
||||
if isinstance(step, PromptStep):
|
||||
updated = edit_prompt_step(dialog_prog, step, available)
|
||||
else:
|
||||
updated = edit_code_step(dialog_prog, step, available)
|
||||
if updated:
|
||||
tool.steps[idx] = updated
|
||||
elif action == "move_up" and idx > 0:
|
||||
tool.steps[idx], tool.steps[idx-1] = tool.steps[idx-1], tool.steps[idx]
|
||||
elif action == "move_down" and idx < len(tool.steps) - 1:
|
||||
tool.steps[idx], tool.steps[idx+1] = tool.steps[idx+1], tool.steps[idx]
|
||||
elif action == "delete":
|
||||
if show_yesno("Delete", f"Delete step {idx+1}?", dialog_prog):
|
||||
tool.steps.pop(idx)
|
||||
|
||||
|
||||
def tool_builder(dialog_prog: str, existing: Optional[Tool] = None) -> Optional[Tool]:
|
||||
"""Main tool builder interface with tabbed sections."""
|
||||
is_edit = existing is not None
|
||||
|
||||
# Initialize tool
|
||||
if existing:
|
||||
tool = Tool(
|
||||
name=existing.name,
|
||||
description=existing.description,
|
||||
arguments=list(existing.arguments),
|
||||
steps=list(existing.steps),
|
||||
output=existing.output
|
||||
)
|
||||
else:
|
||||
tool = Tool(name="", description="", arguments=[], steps=[], output="{input}")
|
||||
|
||||
while True:
|
||||
# Build main menu with section summaries
|
||||
args_count = len(tool.arguments)
|
||||
steps_count = len(tool.steps)
|
||||
|
||||
# Info summary
|
||||
name_display = tool.name or "(not set)"
|
||||
info_summary = f"{name_display}"
|
||||
if tool.arguments:
|
||||
info_summary += f" | {args_count} arg{'s' if args_count != 1 else ''}"
|
||||
|
||||
# Steps summary
|
||||
if tool.steps:
|
||||
step_types = []
|
||||
for s in tool.steps:
|
||||
if isinstance(s, PromptStep):
|
||||
step_types.append(f"P:{s.provider}")
|
||||
else:
|
||||
step_types.append("C")
|
||||
steps_summary = " -> ".join(step_types)
|
||||
else:
|
||||
steps_summary = "(none)"
|
||||
|
||||
choices = [
|
||||
("info", f"[1] Info & Args : {info_summary}"),
|
||||
("steps", f"[2] Steps : {steps_summary}"),
|
||||
("---", "─" * 50),
|
||||
("preview", "Preview Full Summary"),
|
||||
("save", "Save Tool"),
|
||||
("cancel", "Cancel"),
|
||||
]
|
||||
|
||||
title = f"Tool Builder: {tool.name}" if tool.name else "Tool Builder: New Tool"
|
||||
selected = show_menu(title, choices, dialog_prog)
|
||||
|
||||
if selected is None or selected == "cancel":
|
||||
if show_yesno("Cancel", "Discard changes?", dialog_prog):
|
||||
return None
|
||||
continue
|
||||
|
||||
elif selected == "info":
|
||||
edit_tool_info(tool, is_edit, dialog_prog)
|
||||
|
||||
elif selected == "steps":
|
||||
edit_tool_steps(tool, dialog_prog)
|
||||
|
||||
elif selected == "preview":
|
||||
summary = format_tool_summary(tool)
|
||||
show_message("Tool Summary", summary, dialog_prog)
|
||||
|
||||
elif selected == "save":
|
||||
if not tool.name:
|
||||
show_message("Error", "Tool name is required. Go to Info & Args to set it.", dialog_prog)
|
||||
continue
|
||||
|
||||
if not is_edit and tool_exists(tool.name):
|
||||
if not show_yesno("Overwrite?", f"Tool '{tool.name}' exists. Overwrite?", dialog_prog):
|
||||
continue
|
||||
|
||||
return tool
|
||||
|
||||
|
||||
# ============ Main Menu Functions ============
|
||||
|
||||
def ui_list_tools(dialog_prog: str):
|
||||
"""Show list of tools."""
|
||||
tools = list_tools()
|
||||
@@ -208,17 +637,42 @@ def ui_list_tools(dialog_prog: str):
|
||||
tool = load_tool(name)
|
||||
if tool:
|
||||
text += f" {name}: {tool.description or 'No description'}\n"
|
||||
text += f" Provider: {tool.provider}\n\n"
|
||||
if tool.arguments:
|
||||
args = ", ".join(arg.flag for arg in tool.arguments)
|
||||
text += f" Arguments: {args}\n"
|
||||
if tool.steps:
|
||||
step_info = []
|
||||
for step in tool.steps:
|
||||
if isinstance(step, PromptStep):
|
||||
step_info.append(f"PROMPT[{step.provider}]")
|
||||
else:
|
||||
step_info.append("CODE")
|
||||
text += f" Steps: {' -> '.join(step_info)}\n"
|
||||
text += "\n"
|
||||
|
||||
show_message("Tools", text, dialog_prog)
|
||||
|
||||
|
||||
def ui_create_tool(dialog_prog: str):
|
||||
"""Create a new tool."""
|
||||
tool = create_tool_form(dialog_prog)
|
||||
tool = tool_builder(dialog_prog)
|
||||
if tool:
|
||||
path = save_tool(tool)
|
||||
show_message("Success", f"Tool '{tool.name}' created!\n\nConfig: {path}\n\nRun it with: {tool.name} -i <file>", dialog_prog)
|
||||
|
||||
# Build usage example
|
||||
usage = f"{tool.name}"
|
||||
for arg in tool.arguments:
|
||||
if arg.default:
|
||||
usage += f" [{arg.flag} <{arg.variable}>]"
|
||||
else:
|
||||
usage += f" {arg.flag} <{arg.variable}>"
|
||||
usage += " < input.txt"
|
||||
|
||||
show_message("Success",
|
||||
f"Tool '{tool.name}' created!\n\n"
|
||||
f"Config: {path}\n\n"
|
||||
f"Usage: {usage}",
|
||||
dialog_prog)
|
||||
|
||||
|
||||
def ui_edit_tool(dialog_prog: str):
|
||||
@@ -228,13 +682,18 @@ def ui_edit_tool(dialog_prog: str):
|
||||
show_message("Edit Tool", "No tools found.", dialog_prog)
|
||||
return
|
||||
|
||||
choices = [(name, load_tool(name).description or "No description") for name in tools]
|
||||
choices = []
|
||||
for name in tools:
|
||||
tool = load_tool(name)
|
||||
desc = tool.description if tool else "No description"
|
||||
choices.append((name, desc))
|
||||
|
||||
selected = show_menu("Select Tool to Edit", choices, dialog_prog)
|
||||
|
||||
if selected:
|
||||
existing = load_tool(selected)
|
||||
if existing:
|
||||
tool = create_tool_form(dialog_prog, existing)
|
||||
tool = tool_builder(dialog_prog, existing)
|
||||
if tool:
|
||||
save_tool(tool)
|
||||
show_message("Success", f"Tool '{tool.name}' updated!", dialog_prog)
|
||||
@@ -247,7 +706,12 @@ def ui_delete_tool(dialog_prog: str):
|
||||
show_message("Delete Tool", "No tools found.", dialog_prog)
|
||||
return
|
||||
|
||||
choices = [(name, load_tool(name).description or "No description") for name in tools]
|
||||
choices = []
|
||||
for name in tools:
|
||||
tool = load_tool(name)
|
||||
desc = tool.description if tool else "No description"
|
||||
choices.append((name, desc))
|
||||
|
||||
selected = show_menu("Select Tool to Delete", choices, dialog_prog)
|
||||
|
||||
if selected:
|
||||
@@ -265,13 +729,17 @@ def ui_test_tool(dialog_prog: str):
|
||||
show_message("Test Tool", "No tools found.", dialog_prog)
|
||||
return
|
||||
|
||||
choices = [(name, load_tool(name).description or "No description") for name in tools]
|
||||
choices = []
|
||||
for name in tools:
|
||||
tool = load_tool(name)
|
||||
desc = tool.description if tool else "No description"
|
||||
choices.append((name, desc))
|
||||
|
||||
selected = show_menu("Select Tool to Test", choices, dialog_prog)
|
||||
|
||||
if selected:
|
||||
tool = load_tool(selected)
|
||||
if tool:
|
||||
# Get test input
|
||||
test_input = show_textbox("Test Input", "Enter test input here...", dialog_prog)
|
||||
if test_input:
|
||||
from .runner import run_tool
|
||||
@@ -284,11 +752,10 @@ def ui_test_tool(dialog_prog: str):
|
||||
show_prompt=False,
|
||||
verbose=False
|
||||
)
|
||||
show_message(
|
||||
"Test Result",
|
||||
f"Exit code: {code}\n\n--- Output ---\n{output[:500]}",
|
||||
dialog_prog
|
||||
)
|
||||
result_text = f"Exit code: {code}\n\n--- Output ---\n{output[:1000]}"
|
||||
if len(output) > 1000:
|
||||
result_text += "\n... (truncated)"
|
||||
show_message("Test Result", result_text, dialog_prog)
|
||||
|
||||
|
||||
def main_menu(dialog_prog: str):
|
||||
@@ -302,6 +769,7 @@ def main_menu(dialog_prog: str):
|
||||
("edit", "Edit existing tool"),
|
||||
("delete", "Delete tool"),
|
||||
("test", "Test tool (mock provider)"),
|
||||
("providers", "Manage providers"),
|
||||
("exit", "Exit"),
|
||||
],
|
||||
dialog_prog
|
||||
@@ -319,15 +787,33 @@ def main_menu(dialog_prog: str):
|
||||
ui_delete_tool(dialog_prog)
|
||||
elif choice == "test":
|
||||
ui_test_tool(dialog_prog)
|
||||
elif choice == "providers":
|
||||
ui_manage_providers(dialog_prog)
|
||||
|
||||
|
||||
def run_ui():
|
||||
"""Entry point for the UI."""
|
||||
# Prefer urwid (has mouse support)
|
||||
if _check_urwid():
|
||||
from .ui_urwid import run_ui as run_urwid_ui
|
||||
run_urwid_ui()
|
||||
return
|
||||
|
||||
# Fallback to snack (BIOS-style)
|
||||
if _check_snack():
|
||||
from .ui_snack import run_ui as run_snack_ui
|
||||
run_snack_ui()
|
||||
return
|
||||
|
||||
# Fallback to dialog/whiptail
|
||||
dialog_prog = check_dialog()
|
||||
|
||||
if not dialog_prog:
|
||||
print("Error: Neither 'dialog' nor 'whiptail' found.", file=sys.stderr)
|
||||
print("Install with: sudo apt install dialog", file=sys.stderr)
|
||||
print("Error: No TUI library found.", file=sys.stderr)
|
||||
print("Install one of:", file=sys.stderr)
|
||||
print(" pip install urwid (recommended - has mouse support)", file=sys.stderr)
|
||||
print(" sudo apt install python3-newt", file=sys.stderr)
|
||||
print(" sudo apt install dialog", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
@@ -335,7 +821,6 @@ def run_ui():
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
# Clear screen on exit
|
||||
subprocess.run(["clear"], check=False)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user