Modernize provider support

This commit is contained in:
rob
2026-07-19 20:58:04 -03:00
parent 15943f251c
commit 6908d8ee6f
25 changed files with 1749 additions and 581 deletions
+64 -37
View File
@@ -161,32 +161,6 @@ No text before or after the JSON. No markdown fences.{' ' + field_guidance if fi
{prompt}"""
# Legacy function kept for rollback if needed
def _append_schema_instructions_legacy(prompt: str, schema: dict) -> str:
"""
DEPRECATED: Append schema instructions to a prompt.
This approach causes small models to echo the schema. Use
prepend_schema_instructions() instead.
"""
schema_json = json.dumps(schema, indent=2)
return f"""{prompt}
---
RESPONSE FORMAT:
You must respond with ONLY valid JSON matching this exact schema:
{schema_json}
CRITICAL RULES:
- Output ONLY the JSON object, no other text before or after
- Put any thinking, reasoning, or analysis in the "reasoning" field
- Put your actual answer/response in the "output" field
- Do not include markdown code fences
- Do not include any preamble like "Here is my response:"
"""
def check_system_dependencies(tool: Tool) -> list:
"""
Check if system dependencies are satisfied.
@@ -506,11 +480,30 @@ def _extract_json(text: str) -> dict | list | None:
return None
def _read_step_file(filename: str, base_dir: Optional[Path], step_type: str) -> str:
"""Read a prompt/code file relative to the tool directory."""
if not base_dir:
raise ValueError(f"{step_type}_file requires the tool to have a filesystem path")
relative_path = Path(filename)
if relative_path.is_absolute():
raise ValueError(f"{step_type}_file must be relative to the tool directory")
base = base_dir.resolve()
path = (base / relative_path).resolve()
if path == base or base not in path.parents:
raise ValueError(f"{step_type}_file cannot reference files outside the tool directory")
if not path.is_file():
raise FileNotFoundError(f"{step_type}_file not found: {filename}")
return path.read_text()
def execute_prompt_step(
step: PromptStep,
variables: dict,
provider_override: str = None,
verbose: bool = False
verbose: bool = False,
base_dir: Optional[Path] = None
) -> tuple[str, bool]:
"""
Execute a prompt step.
@@ -529,7 +522,13 @@ def execute_prompt_step(
import re
# Build prompt with variable substitution
prompt = substitute_variables(step.prompt, variables, warn_non_scalar=verbose)
try:
prompt_template = _read_step_file(step.prompt_file, base_dir, "prompt") if step.prompt_file else step.prompt
except (OSError, ValueError) as e:
print(f"Error in prompt step: {e}", file=sys.stderr)
return "", False
prompt = substitute_variables(prompt_template, variables, warn_non_scalar=verbose)
# Inject profile system prompt if specified
if step.profile:
@@ -633,7 +632,8 @@ def execute_code_step(
step: CodeStep,
variables: dict,
step_num: int = 0,
verbose: bool = False
verbose: bool = False,
base_dir: Optional[Path] = None
) -> tuple[dict, bool]:
"""
Execute a code step.
@@ -647,7 +647,14 @@ def execute_code_step(
Tuple of (output_vars_dict, success)
"""
# Substitute variables in code (like {outputfile} -> actual value)
code = substitute_variables(step.code, variables, warn_non_scalar=verbose)
try:
code_template = _read_step_file(step.code_file, base_dir, "code") if step.code_file else step.code
except (OSError, ValueError) as e:
print(f"Error in code step (step {step_num}):", file=sys.stderr)
print(f" {e}", file=sys.stderr)
return {}, False
code = substitute_variables(code_template, variables, warn_non_scalar=verbose)
# Create execution environment with variables
# IMPORTANT: Use the same dict for both globals and locals.
@@ -887,6 +894,8 @@ def run_tool(
print(f"[verbose] Variables: {list(variables.keys())}", file=sys.stderr)
print(f"[verbose] Steps: {len(tool.steps)}", file=sys.stderr)
tool_base_dir = tool.path.parent if tool.path else None
# If no steps, just substitute output template
if not tool.steps:
output = substitute_variables(tool.output, variables, warn_non_scalar=verbose)
@@ -916,7 +925,13 @@ def run_tool(
if dry_run:
variables[step.output_var] = f"[DRY RUN - would call {step.provider}]"
else:
output, success = execute_prompt_step(step, variables, provider_override, verbose=verbose)
output, success = execute_prompt_step(
step,
variables,
provider_override,
verbose=verbose,
base_dir=tool_base_dir
)
if not success:
return "", 2
variables[step.output_var] = output
@@ -932,7 +947,13 @@ def run_tool(
for var in [v.strip() for v in step.output_var.split(',')]:
variables[var] = "[DRY RUN - would execute code]"
else:
outputs, success = execute_code_step(step, variables, step_num=i+1, verbose=verbose)
outputs, success = execute_code_step(
step,
variables,
step_num=i+1,
verbose=verbose,
base_dir=tool_base_dir
)
if not success:
return "", 1
# Merge all output vars into variables
@@ -1011,6 +1032,16 @@ def create_argument_parser(tool: Tool) -> argparse.ArgumentParser:
return parser
def collect_custom_args(tool: Tool, args: argparse.Namespace) -> dict:
"""Collect tool-specific arguments from a parsed argparse namespace."""
custom_args = {}
for arg in tool.arguments:
value = getattr(args, arg.variable, None)
if value is not None:
custom_args[arg.variable] = value
return custom_args
def main():
"""Entry point for tool execution via wrapper script."""
if len(sys.argv) < 2:
@@ -1060,11 +1091,7 @@ def main():
input_text = ""
# Collect custom args
custom_args = {}
for arg in tool.arguments:
value = getattr(args, arg.variable, None)
if value is not None:
custom_args[arg.variable] = value
custom_args = collect_custom_args(tool, args)
# Determine provider override (CLI flag takes precedence over manifest)
effective_provider = args.provider or provider_override_from_manifest