実験的 - このプロジェクトはまだ開発中であり、本番環境での使用には耐えられません。
AIが使用するための、Rustで書かれた最小限で安全なPythonインタープリター。
Montyは、LLMが生成したコードを実行するために完全なコンテナベースのサンドボックスを使う際のコスト、レイテンシ、複雑さ、そして一般的な手間を回避します。
その代わりに、エージェントに組み込まれたLLMが書いたPythonコードを安全に実行できます。起動時間は数百ミリ秒ではなく、一桁マイクロ秒単位です。
Montyができること:
sys、os、typing、asyncio、re、datetime、json、dataclasses(近日対応予定)Montyができないこと:
要するに、Montyは非常に制限されており、1つのユースケースのために設計されています:
エージェントが書いたコードを実行すること。
なぜこれを行う必要があるのかについての動機は、以下を参照してください:
非常に簡単に言うと、上記すべての考え方は、従来のツール呼び出しに依存する代わりにPython(またはJavaScript)コードを書くように求められれば、LLMはより速く、より安く、より確実に動作できるというものです。Montyは、サンドボックスの複雑さやホスト上で直接コードを実行するリスクなしに、それを可能にします。
注記: Montyは(近い将来)Pydantic AIでcodemodeを実装するために使用される予定です。
MontyはPython、JavaScript/TypeScript、またはRustから呼び出すことができます。
インストールするには:```bash uv add pydantic-monty
(または、古い世代向けには `pip install pydantic-monty`)
`pydantic-monty` は、`pydantic-monty-client`(
`pydantic_monty` モジュール)と `pydantic-monty-runtime`(`monty` ワーカー
バイナリ)をペアにするメタパッケージです。バイナリが他の場所から既に
入手できる場合は、`pydantic-monty-client` だけをインストールしてください。
使用方法:```python
from typing import Any
import pydantic_monty
code = """
async def agent(prompt: str, messages: Messages):
while True:
print(f'messages so far: {messages}')
output = await call_llm(prompt, messages)
if isinstance(output, str):
return output
messages.extend(output)
await agent(prompt, [])
"""
type_definitions = """
from typing import Any
Messages = list[dict[str, Any]]
async def call_llm(prompt: str, messages: Messages) -> str | Messages:
raise NotImplementedError()
prompt: str = ''
"""
Messages = list[dict[str, Any]]
async def call_llm(prompt: str, messages: Messages) -> str | Messages:
if len(messages) < 2:
return [{'role': 'system', 'content': 'example response'}]
else:
return f'example output, message count {len(messages)}'
async def main():
async with pydantic_monty.AsyncMonty() as pool:
async with pool.checkout(
script_name='agent.py',
type_check=True,
type_check_stubs=type_definitions,
) as session:
output = await session.feed_run(
code,
inputs={'prompt': 'testing'},
external_lookup={'call_llm': call_llm},
)
print(output)
#> example output, message count 2
if __name__ == '__main__':
import asyncio
asyncio.run(main())
実行は monty ワーカーサブプロセスのプールで行われるため、敵対的なコードによって引き起こされるメモリエラー(スタックオーバーフロー、アロケータのアボート)でも、あなたのプロセスをクラッシュさせることは決してありません。ワーカーが終了し、MontyCrashedError を発生させ、そして置き換えられます。完全に同期するAPIもあります。```python
import pydantic_monty
with pydantic_monty.Monty() as pool: with pool.checkout() as session: # session state persists between feed_run calls session.feed_run('x = 21') print(session.feed_run('x * 2')) #> 42
### JavaScript / TypeScript
インストールするには:```bash
npm install @pydantic/monty
JS パッケージは、Python パッケージが使用するのと同じ Rust ワーカープールに対するネイティブ (napi) バインディングであり、
バインディングと monty ワーカーバイナリは、プラットフォーム固有の npm パッケージとして
配布されます:```ts
import { Monty } from '@pydantic/monty'
await using pool = await Monty.create() await using session = await pool.checkout()
// session state persists between feedRun calls await session.feedRun('x = 21') console.log(await session.feedRun('x * 2')) // 42
// external functions may be async const result = await session.feedRun('await fetch_data()', { externalLookup: { fetch_data: async () => 'data' }, })
ブラウザ(またはサブプロセスが不可能な任意の場所)では、同じパッケージは
`@pydantic/monty/wasm` サブパス配下にインプロセスの WebAssembly ビルドを公開します
(クラッシュ分離なし: サンドボックスのクラッシュはそこではホストのクラッシュです)。
### Rust
Rust から信頼できないコードを実行する場合、以下に示すインプロセス API ではなく、
[`monty-pool`](https://crates.io/crates/monty-pool) クレートを推奨します。
`monty-pool` は `monty` ワーカーのサブプロセスでのみコードを実行するため、追加の保護が得られます:
敵対的なコードによって引き起こされるクラッシュ(スタックオーバーフロー、アロケータのアボート)はワーカーのみを終了させます —
プールはその終了を検出してワーカーを置き換えます — また、親側のウォッチドッグは、ハードタイムアウトを超えた
ワーカーを強制終了できます。これは上記の Python パッケージと JavaScript パッケージが
基づいているのと同じエンジンです。使用法については
[monty-pool README](https://github.com/pydantic/monty/tree/main/crates/monty-pool) を参照してください。
`monty` クレート自体はインプロセスのインタプリタを提供します:```rust
use monty::MontyRun;
use monty_types::{CompileOptions, ResourceTracker, MontyObject, PrintWriter, ResourceLimits};
let code = r#"
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
fib(x)
"#;
let runner = MontyRun::new(code.to_owned(), "fib.py", vec!["x".to_owned()], CompileOptions::default()).unwrap();
let result = runner.run(vec![MontyObject::Int(10)], ResourceTracker::default(), PrintWriter::Stdout).unwrap();
assert_eq!(result, MontyObject::Int(55));
REPL セッションは dump() でシリアライズでき、Dump::load() で復元できます。ダンプには、読み込み側のビルドが確認するバージョンが付与され、インタープリタの状態とともにセッションのメタデータ(スクリプト名、型チェック用スタブ)が格納されます。```rust
use monty::{Dump, MontyRepl, Session, SessionRef, dump};
use monty_types::{CompileOptions, MontyObject, PrintWriter, ResourceTracker};
// Snapshot a session between snippets let mut repl = MontyRepl::new("main.py", ResourceTracker::default(), CompileOptions::default()); repl.feed_run("x = 41", vec![], PrintWriter::Stdout).unwrap(); let bytes = dump("main.py", None, SessionRef::Idle(&repl)).unwrap();
// Later, restore and carry on feeding let Session::Idle(mut restored) = Dump::load(&bytes).unwrap().state else { panic!("dumped an idle session") }; let result = restored.feed_run("x + 1", vec![], PrintWriter::Stdout).unwrap(); assert_eq!(result, MontyObject::Int(42));
`MontyRun` と `RunProgress` には独自のダンプ形式はありませんが、どちらも `serde::Serialize`/`Deserialize` を実装しているため、ホストは解析済みコードや一時停止中の実行を、すでに使用している任意の形式でシリアライズできます。
## ワーカーにおけるメモリ制限
セッションの `max_memory` はワーカーのアロケータによって測定されます。インタプリタは
ソフトリミットを超えるとグレースフルな `MemoryError` を報告します。より高いハード
リミットは、チェックポイント間で1つの割り当てが大きくなりすぎた場合、ワーカーを強制終了して置き換えます。
[`limitations/resource_limits.md`](https://github.com/pydantic/monty/blob/HEAD/limitations/resource_limits.md) に、制限の超過が
ホストにどのように現れるかが説明されています。また、`monty-alloc` は、
サブプロセスとWebAssemblyの両方のワーカーが実行時に使用するアロケータです。
## PydanticAI 統合
Monty は [Pydantic AI](https://github.com/pydantic/pydantic-ai) の code-mode を
強化します。逐次的なツール呼び出しを行う代わりに、LLM はあなたのツールを
関数として呼び出す Python コードを書き、Monty がそれを安全に
実行します。```python test="skip"
import asyncio
import json
import logfire
from httpx import AsyncClient
from pydantic_ai import Agent, RunContext
from pydantic_ai.toolsets.code_mode import CodeModeToolset
from pydantic_ai.toolsets.function import FunctionToolset
from typing_extensions import TypedDict
logfire.configure()
logfire.instrument_pydantic_ai()
class LatLng(TypedDict):
lat: float
lng: float
weather_toolset: FunctionToolset[AsyncClient] = FunctionToolset()
@weather_toolset.tool
async def get_lat_lng(
ctx: RunContext[AsyncClient], location_description: str
) -> LatLng:
"""Get the latitude and longitude of a location."""
# NOTE: the response here will be random, and is not related to the location description.
r = await ctx.deps.get(
'https://demo-endpoints.pydantic.workers.dev/latlng',
params={'location': location_description},
)
r.raise_for_status()
return json.loads(r.content)
@weather_toolset.tool
async def get_temp(ctx: RunContext[AsyncClient], lat: float, lng: float) -> float:
"""Get the temp at a location."""
# NOTE: the responses here will be random, and are not related to the lat and lng.
r = await ctx.deps.get(
'https://demo-endpoints.pydantic.workers.dev/number',
params={'min': 10, 'max': 30},
)
r.raise_for_status()
return float(r.text)
@weather_toolset.tool
async def get_weather_description(
ctx: RunContext[AsyncClient], lat: float, lng: float
) -> str:
"""Get the weather description at a location."""
# NOTE: the responses here will be random, and are not related to the lat and lng.
r = await ctx.deps.get(
'https://demo-endpoints.pydantic.workers.dev/weather',
params={'lat': lat, 'lng': lng},
)
r.raise_for_status()
return r.text
agent = Agent(
'gateway/anthropic:claude-sonnet-4-5',
# toolsets=[weather_toolset],
toolsets=[CodeModeToolset(weather_toolset)],
deps_type=AsyncClient,
)
async def main():
async with AsyncClient() as client:
await agent.run('Compare the weather of London, Paris, and Tokyo.', deps=client)
if __name__ == '__main__':
asyncio.run(main())
人々にMontyを見せると、一般的に2つの反応があります:
Xは何らかの代替技術です。不思議なことに、この2つの反応が組み合わされることがよくあります。つまり、人々は自分たちに合う代替技術をまだ見つけていない一方で、Python実装全体をゼロから作ることに本当に良い代替手段がないとは信じられない、ということです。
最もわかりやすい代替技術と、それらが私たちの求めるものに適していない理由を順に見ていきます。
注記:これらの技術はすべて素晴らしく、広く使われています。私たちのユースケースにおける制限に関するこのコメントは、批判と見なすべきではありません。これらのソリューションのほとんどはLLMサンドボックスの提供を目的として考案されたものではなく、それが必ずしも得意ではない理由です。
起動パフォーマンス数値の算出に使用したスクリプトについては、./scripts/startup_performance.py を参照してください。
各項目の詳細は以下の通りです:
pip install pydantic-monty または npm install @pydantic/monty のみ、ダウンロード約4.5MBdump()とload()による一時停止・再開機能により、実行の一時停止、再開、フォークが簡単にできるpython:3.14-alpine は50MB - dockerはPyPIからインストールできないstarlark-rust を参照。
Wasmer を介してWebAssemblyでPythonを実行する。
python/python wasmerパッケージにはREADME、ライセンス、ソースリンク、ビルド方法の記載がない。最近アップロードされたバージョンではサイズが「0B」と表示されるが、ダウンロードは約50MB - Pythonバイナリのビルドプロセスは明確かつ透過的ではない。(ここで間違っていたら、issueを作成して訂正してください)k8sで独自のサンドボックス環境を構築する場合も同様の課題があり、セットアップの複雑さは増すが、ネットワークレイテンシは低くなる。
exec()(約0.1ms)またはsubprocess(約30ms)でPythonを直接実行する。
exec()ではほぼゼロ、subprocessでは約30msPydanticスタックは、本番環境レベルのAIエージェントをリリースするために必要なすべてを提供します:
| 技術 | 言語の完全性 | セキュリティ | 起動レイテンシ | FOSS | セットアップの複雑さ | ファイルマウント | スナップショット |
|---|
| Monty | 部分 | 厳格 | 0.06ms | 無料 / OSS | 簡単 | 簡単 | 簡単 |
| Docker | 完全 | 良好 | 195ms | 無料 / OSS | 中程度 | 簡単 | 中程度 |
| Pyodide | 完全 | 不良 | 2800ms | 無料 / OSS | 中程度 | 簡単 | 困難 |
| starlark-rust | 非常に限定的 | 良好 | 1.7ms | 無料 / OSS | 簡単 | 利用不可? | 不可能? |
| WASI / Wasmer | 部分的、ほぼ完全 | 厳格 | 66ms | 無料 * | 中程度 | 簡単 | 中程度 |
| sandboxing service | 完全 | 厳格 | 1033ms | 有料 | 中程度 | 困難 | 中程度 |
| YOLO Python | 完全 | 存在しない | 0.1ms / 30ms | 無料 / OSS | 簡単 | 簡単 / 危険 | 困難 |