ToolOrchestratorEnv / tools /runtime.py
Andrew Lara
Rename project to ToolOrchestratorEnv
a419ab1
Raw
History Blame Contribute Delete
5.52 kB
"""Shared tool catalog and dispatch helpers for ToolOrchestratorEnv.
This module keeps the tool contract explicit:
- the catalog describes every tool, its purpose, and its input field
- registry validation catches config drift early
- dispatch normalizes failures into ToolResult objects instead of
letting exceptions leak through the environment loop
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
from typing import Any, Callable, Dict, List, Mapping
from env.config import EnvConfig
from env.models import OrchestratorAction, TOOL_IDS, ToolResult
@dataclass(frozen=True)
class ToolSpec:
tool_id: str
label: str
purpose: str
input_field: str
cost: float
notes: str
_TOOL_SPEC_TEMPLATES: Dict[str, Dict[str, str]] = {
"ceramic_search": {
"label": "Ceramic web search",
"purpose": "Web retrieval for multi-hop factual QA",
"input_field": "query",
"notes": "Falls back to deterministic offline search when Ceramic credentials are unavailable.",
},
"wiki_lookup": {
"label": "Wikipedia lookup",
"purpose": "Entity facts, definitions, and short summaries",
"input_field": "query",
"notes": "Returns an explicit not-found or HTTP error result instead of crashing.",
},
"calculator": {
"label": "Calculator",
"purpose": "Arithmetic and symbolic math",
"input_field": "expression",
"notes": "Uses a restricted AST evaluator with comparisons and common math functions.",
},
"code_executor": {
"label": "Python executor",
"purpose": "HumanEval-style coding tasks",
"input_field": "code_snippet",
"notes": "Sandboxed exec with blocked imports, dunder attribute access, and unsafe builtins.",
},
"llm_reason": {
"label": "LLM reasoning",
"purpose": "Costly model-backed reasoning on hard problems",
"input_field": "query",
"notes": "Returns a clear no_api_key error when Together is unavailable.",
},
"commit": {
"label": "Commit answer",
"purpose": "Submit the final answer and advance the episode",
"input_field": "answer",
"notes": "Pass-through only; grading happens inside the environment.",
},
}
def validate_tool_costs(config: EnvConfig) -> None:
"""Fail fast if the configured cost map drifts from the canonical tool set."""
missing = [tool_id for tool_id in TOOL_IDS if tool_id not in config.tool_costs]
if missing:
raise ValueError(f"EnvConfig.tool_costs is missing required tools: {missing}")
negative = {tool_id: cost for tool_id, cost in config.tool_costs.items() if cost < 0}
if negative:
raise ValueError(f"Tool costs must be non-negative: {negative}")
def build_tool_catalog(config: EnvConfig | None = None) -> List[ToolSpec]:
"""Return the canonical ordered catalog used by the UI and docs."""
cfg = config or EnvConfig()
validate_tool_costs(cfg)
catalog: List[ToolSpec] = []
for tool_id in TOOL_IDS:
template = _TOOL_SPEC_TEMPLATES[tool_id]
catalog.append(
ToolSpec(
tool_id=tool_id,
label=template["label"],
purpose=template["purpose"],
input_field=template["input_field"],
cost=cfg.tool_costs[tool_id],
notes=template["notes"],
)
)
return catalog
def build_tool_registry(config: EnvConfig | None = None) -> Dict[str, Callable[[OrchestratorAction], ToolResult]]:
"""Return the canonical mapping of tool_id -> tool callable."""
from .calculator import calculator_tool
from .ceramic_search import make_search_tool
from .code_executor import code_executor_tool
from .commit import commit_tool
from .llm_reason import llm_reason_tool
from .wiki_lookup import wiki_lookup_tool
cfg = config or EnvConfig()
validate_tool_costs(cfg)
return {
"ceramic_search": make_search_tool(),
"calculator": calculator_tool,
"wiki_lookup": wiki_lookup_tool,
"code_executor": code_executor_tool,
"llm_reason": llm_reason_tool,
"commit": commit_tool,
}
def dispatch_tool(
tool_id: str,
action: OrchestratorAction,
registry: Mapping[str, Callable[[OrchestratorAction], ToolResult]],
) -> ToolResult:
"""Call a tool and normalize missing-tool and crash cases into ToolResult."""
tool_fn = registry.get(tool_id)
if tool_fn is None:
return ToolResult(
tool_id=tool_id,
output="[Tool not available in this environment]",
error="not_available",
)
try:
result = tool_fn(action)
except Exception as exc: # pragma: no cover - defensive wrapper
return ToolResult(
tool_id=tool_id,
output=f"[Error: {exc}]",
error=str(exc),
)
if not isinstance(result, ToolResult):
return ToolResult(
tool_id=tool_id,
output=f"[Tool error: unexpected return type {type(result).__name__}]",
error="invalid_return_type",
)
if result.tool_id != tool_id:
result = result.model_copy(update={"tool_id": tool_id})
return result
def catalog_as_dicts(config: EnvConfig | None = None) -> List[dict[str, Any]]:
"""Convenience helper for JSON serialization."""
return [asdict(spec) for spec in build_tool_catalog(config)]