Metadata-Version: 2.4
Name: schema2code
Version: 0.2.0
Summary: LLMs call your tools by writing sandboxed Python instead of JSON tool calls — 70% fewer tokens, 66% fewer round trips (measured)
Author: DW-dev-UE
License-Expression: MIT
Project-URL: Homepage, https://github.com/DW-dev-UE/schema2code
Project-URL: Repository, https://github.com/DW-dev-UE/schema2code
Project-URL: Documentation, https://github.com/DW-dev-UE/schema2code#readme
Project-URL: Issues, https://github.com/DW-dev-UE/schema2code/issues
Project-URL: Changelog, https://github.com/DW-dev-UE/schema2code/releases
Keywords: llm,tools,agents,sandbox,schema,function-calling
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: ruff==0.16.0; extra == "dev"
Requires-Dist: tiktoken>=0.5; extra == "dev"
Provides-Extra: tiktoken
Requires-Dist: tiktoken>=0.5; extra == "tiktoken"
Provides-Extra: openai
Requires-Dist: openai>=1.0; extra == "openai"
Provides-Extra: docker
Requires-Dist: docker>=7.0; extra == "docker"
Provides-Extra: all
Requires-Dist: tiktoken>=0.5; extra == "all"
Requires-Dist: openai>=1.0; extra == "all"
Requires-Dist: docker>=7.0; extra == "all"
Dynamic: license-file

<div align="center">

# schema2code

**Convert LLM tool schemas into a compact, sandboxed Python code interface.**

[![PyPI](https://img.shields.io/pypi/v/schema2code.svg)](https://pypi.org/project/schema2code/)
[![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue.svg)](https://pypi.org/project/schema2code/)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![Tests](https://img.shields.io/badge/tests-97%20passing-brightgreen.svg)](tests/)
[![Dependencies](https://img.shields.io/badge/runtime%20deps-0-lightgrey.svg)](pyproject.toml)

**English** · [한국어](README.ko.md) · [日本語](README.ja.md)

</div>

---

Instead of resending JSON Schemas on every turn, hand the model short function
signatures and let it write one Python program that calls your tools inside a
restricted sandbox.

```text
Traditional:  [JSON schemas, every turn] → tool-call JSON → execute → repeat
schema2code:  [compact signatures, once] → Python code → sandbox → result
```

> [!TIP]
> The win is not only a smaller prompt — it is **fewer round trips**. One
> program replaces a chain of tool calls, so a task that cost four API turns
> costs one, and the schemas stop being re-sent with each turn.

## Measured, not estimated

Live A/B against the OpenAI API — 15 tasks × 2 arms, gpt-4o-mini ×3 repeats +
gpt-4o ×1, temperature 0, token counts taken from the API `usage` field:

|                       | JSON tool-calling | schema2code | delta |
|-----------------------|------------------:|------------:|------:|
| Tokens per task       | 3,319             | 995         | **−70%** |
| API round trips       | 3.35              | 1.15        | −66%  |
| Answer accuracy       | 61.7%             | 78.3%       | +16.7pt |
| … gpt-4o only         | 80%               | **100%**    | |
| Cost                  | $0.138            | $0.054      | −61%  |
| Sandbox-guard false positives | —         | 0 / 60 runs | |

The gap widens with task complexity: loop-style tasks drop from 4.2 round
trips to 1.0, and the hardest task saved 89% of tokens. Full method, per-task
tables, and raw transcripts live in [`benchmarks/results/`](benchmarks/results/)
— every run writes `report.md`, `transcript.jsonl`, and `runs.json`.

> [!NOTE]
> To be fair about the other side: with only 2 small tools the compact
> interface *costs* 11 tokens more than the schemas — the fixed usage-rules
> text dominates. Savings turn clearly positive around 10+ tools. See
> [Token measurement](docs/usage.md#measuring-token-savings).

## Install

```bash
pip install schema2code
```

```bash
# optional extras
pip install "schema2code[tiktoken]"   # accurate token counts
pip install "schema2code[openai]"     # agent-loop example / integration tests
```

Zero runtime dependencies. Python ≥ 3.10, Windows/macOS/Linux.

## Sixty seconds

```python
from schema2code import ToolRegistry, Sandbox

registry = ToolRegistry()

@registry.tool
def get_weather(city: str, unit: str = "celsius") -> dict:
    """Return current weather for a city."""
    return {"city": city, "temp": 20, "unit": unit}

@registry.tool
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

sandbox = Sandbox(registry, timeout=8.0)

outcome = sandbox.run("""
w = get_weather("Berlin")
result = add(w["temp"], 2)
print("temp+2 =", result)
""")
```

`outcome` is a plain dataclass:

```text
success       True
result        22
stdout        'temp+2 = 22\n'
tools_called  ['get_weather', 'add']
duration      0.0002
```

And this is the entire prompt block the model needs — the output of
`registry.to_prompt()`:

```text
# Available tools
get_weather(city: str, unit: str = 'celsius') -> dict
    # Return current weather for a city.
add(a: int, b: int) -> int
    # Add two numbers.

## How to call tools

Write Python code that calls the functions below.
- Use only the listed tools and allowed standard modules.
- Prefer clear intermediate variables.
- Print values you want to inspect; assign the final value to `result`
  or leave it as the last expression.
- Do not import disallowed modules, open files, or access the network.
```

Failures come back classified, ready to feed to the model for a retry:

```python
outcome = sandbox.run('import json\nresult = json.__builtins__')
outcome.error_type    # "security"
outcome.short_error   # "... Blocked by sandbox guard: attribute access to '__builtins__' is blocked (line 2) ..."
```

`error_type` is one of `ok · syntax · timeout · tool · security · import ·
runtime · policy · validation`.

> [!TIP]
> That single line is the whole retry loop. Append `short_error` to the
> conversation and ask for another code block — in the live benchmark this
> converged in **1.15 API calls per task** on average.

## Already have schemas?

OpenAI and Anthropic tool definitions import directly, constraints included.
Names that aren't valid Python identifiers (MCP-style `set-temp`) are
sanitized so the generated code can actually call them:

```python
registry.load_openai_tools(openai_tools, handlers={"set-temp": set_temp})
registry.load_anthropic_tools(anthropic_tools, handlers=...)
```

```text
### set_temp
Parameters:
  - value: float (required) [minimum=0, maximum=100]
  - unit: str (required) [enum=['celsius', 'fahrenheit']]
```

With `Sandbox(registry, validate_calls=True)` those constraints are enforced
at call time — `set_temp(value=150, ...)` fails with
`Invalid argument for set_temp.value: 150 > maximum 100` before your handler
runs.

## Sandbox levels

| Level | Class | Isolation | Use it for |
|------:|-------|-----------|------------|
| 0 | `RestrictedSandbox` | restricted builtins, same process | notebooks, tests |
| 1 | `Sandbox` (default) | child process + hard timeout + tool RPC | local agent loops |
| 2 | `DockerSandbox` | container (stub — no host tools) | isolated pure-Python eval |

The default `Sandbox` runs your tools in the *parent* process over an RPC
channel, so closures, bound methods, and lambdas all work — nothing gets
pickled. Timeouts kill the child; a hung host tool can't stall `run()` past
its deadline either.

> [!WARNING]
> **This is not a hard security boundary.** The guards block the known
> introspection escapes (`().__class__.__base__.__subclasses__()`,
> `json.__builtins__`, dynamic `.format()` templates, `operator.attrgetter`)
> and rejected 13/13 vectors in the regression suite, but a denylist over
> Python stays a denylist. Multi-tenant production needs OS/container
> isolation on top — read [SECURITY.md](SECURITY.md) before deploying.

<details>
<summary><b>Full feature list</b></summary>

| Area | What you get |
|---|---|
| Registration | `@registry.tool` decorator, `register()`, module-level `@tool` |
| Signatures | type hints + docstrings; imported schemas keep their constraints |
| Prompt styles | `signatures` (cheapest), `detailed` (shows constraints), `minimal` |
| Tool RPC | default path — host callables never need pickling |
| Isolation | child process with hard timeout; deadline covers host tool time |
| `Outcome` | `success`, `result`, `stdout`, `error`, `duration`, `tools_called`, `error_type`, `short_error`, truncation flags, `cached`, `debug` |
| Escape guards | AST dunder rejection + `SafeModule` import proxies (`guard=True`) |
| Call policy | `max_tool_calls`, `max_total_calls`, `max_calls_per_tool` |
| Validation | `validate_calls=True` enforces enum / min / max / pattern |
| Schema import | `load_openai_tools`, `load_anthropic_tools` (+ name sanitizing) |
| Metrics | `compare_tokens()`, `estimate_tokens()` (tiktoken when installed) |
| Cache | opt-in `ResultCache`, keyed on code + tools + security config |
| Async | `async def` tools are awaited; `await sandbox.run_async(code)` |
| Debug | `debug=True` → code hash, call counts, backend, duration |

</details>

<details>
<summary><b>Public API surface</b></summary>

```python
from schema2code import (
    ToolRegistry, tool, get_default_registry,
    Sandbox,            # process + restricted + RPC (default)
    RestrictedSandbox,  # in-process only
    ProcessSandbox, DockerSandbox, docker_available,
    Outcome, ToolSpec, CallPolicy, ResultCache,
    compare_tokens, estimate_tokens,
    load_openai_tools, load_anthropic_tools,
    SecurityError, PolicyError, ValidationError, UnsupportedError,
)
```

</details>

## What this is not

- **Not an agent framework.** No planner, memory, router, or chain. You own
  the loop — this library turns tools into a prompt block and runs the code
  that comes back. It sits underneath whatever framework you already use.
- **Not a security boundary for untrusted input.** It defends against model
  mistakes, not against an adversary. See the warning above.
- **Not a win for every setup.** Below roughly 10 tools the interface can cost
  more than the schemas it replaces.
- **Not a hosted runtime.** Everything runs on your machine — no service, no
  account, no vendor.

The measured numbers come from one workload: 15 tasks over a 14-tool registry,
OpenAI models, temperature 0. The direction should transfer — more tools and
longer chains favour code — but treat the exact percentages as
workload-specific and re-run `benchmarks/bench_v2.py` against your own tools.

## Documentation

| | | |
|---|---|---|
| Usage guide | [docs/usage.md](docs/usage.md) | registration, prompt styles, retry loop, policies, caching, async |
| Architecture | [docs/architecture.md](docs/architecture.md) | RPC protocol, guard design, cache keys, error taxonomy |
| Security model | [SECURITY.md](SECURITY.md) | threat model, backend levels, hard requirements |
| Releasing | [RELEASING.md](RELEASING.md) | build & publish checklist |

한국어: [docs/usage.ko.md](docs/usage.ko.md) · [docs/architecture.ko.md](docs/architecture.ko.md)
/ 日本語: [docs/usage.ja.md](docs/usage.ja.md) · [docs/architecture.ja.md](docs/architecture.ja.md)

## Development

```bash
git clone https://github.com/DW-dev-UE/schema2code.git && cd schema2code
pip install -e ".[dev]"
python -m pytest -q                  # 97 tests
python -m ruff check src benchmarks
python benchmarks/run_all.py         # local suite (no API key needed)
python benchmarks/bench_v2.py --dry-run   # live A/B cost preview
```

> [!IMPORTANT]
> The live benchmark (`bench_v2.py`) calls a paid API and spends real money.
> It needs `OPENAI_API_KEY`, prints a cost estimate before the first call,
> and stops hard at `--budget`. Start with `--dry-run`.

## License

MIT
