
Un interpréteur Python minimal et sécurisé écrit en Rust pour utilisation par l'IA.
Expérimental - Ce projet est encore en développement et n'est pas prêt pour une utilisation en production.
Un interpréteur Python minimal et sécurisé écrit en Rust pour être utilisé par l'IA.
Monty évite le coût, la latence, la complexité et les tracas d'un sandbox complet basé sur des conteneurs pour exécuter du code généré par un LLM.
Au lieu de cela, il vous permet d'exécuter en toute sécurité du code Python écrit par un LLM intégré à votre agent, avec des temps de démarrage mesurés en microsecondes à un chiffre, et non en centaines de millisecondes.
Ce que Monty peut faire :
sys, os, typing, asyncio, re, datetime, json, (bientôt)Ce que Monty ne peut pas faire :
En bref, Monty est extrêmement limité et conçu pour un seul cas d'usage :
Exécuter du code écrit par des agents.
Pour comprendre pourquoi vous pourriez vouloir faire cela, consultez :
En termes très simples, l'idée de tout ce qui précède est que les LLM peuvent travailler plus vite, à moindre coût et de manière plus fiable si on leur demande d'écrire du code Python (ou Javascript) au lieu de s'appuyer sur l'appel d'outils traditionnel. Monty rend cela possible sans la complexité d'un sandbox ni le risque d'exécuter du code directement sur l'hôte.
Remarque : Monty sera (bientôt) utilisé pour implémenter codemode dans Pydantic AI
Monty peut être appelé depuis Python, JavaScript/TypeScript ou Rust.
Pour installer :```bash uv add pydantic-monty
(Ou `pip install pydantic-monty` pour les boomers)
`pydantic-monty` est un méta-paquet associant `pydantic-monty-client` (le module `pydantic_monty`) avec `pydantic-monty-runtime` (le binaire worker `monty`). Installez `pydantic-monty-client` seul si le binaire provient déjà d'ailleurs.
Utilisation :```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())
L'exécution a lieu dans un pool de sous-processus de travail monty, donc même une erreur mémoire
déclenchée par du code hostile (débordement de pile, arrêt de l'allocateur) ne peut
jamais faire planter votre processus — le travailleur meurt, lève MontyCrashedError, et
est remplacé. Il existe également une API entièrement synchrone :```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
Pour installer :```bash
npm install @pydantic/monty
Le package JS est un binding natif (napi) sur le même pool de workers Rust que le
package Python utilise — le binding et le binaire worker monty sont fournis via
des packages npm spécifiques à la plateforme :```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' }, })
Pour les navigateurs (ou partout où les sous-processus sont impossibles), le même paquet
expose une version WebAssembly en processus sous le sous-chemin `@pydantic/monty/wasm`
(pas d'isolation en cas de crash : un crash de la sandbox y est un crash de l'hôte).
### Rust
Pour exécuter du code non fiable depuis Rust, nous recommandons la
crate [`monty-pool`](https://crates.io/crates/monty-pool) plutôt que l'API en processus ci-dessous.
`monty-pool` n'exécute le code que dans des sous-processus worker `monty`, ce qui offre des protections supplémentaires :
un crash déclenché par du code adversaire (dépassement de pile, arrêt de l'allocateur) ne tue que le worker —
le pool détecte la mort et remplace le worker — et un chien de garde côté parent peut tuer les workers
qui dépassent un délai d'attente strict. C'est le même moteur que celui sur lequel les paquets Python
et JavaScript ci-dessus sont construits. Voir le [README de monty-pool](https://github.com/pydantic/monty/tree/main/crates/monty-pool)
pour l'utilisation.
La crate `monty` elle-même fournit l'interpréteur en processus :```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));
Une session REPL peut être sérialisée avec dump() et restaurée avec Dump::load(). Le dump transporte les métadonnées de session (nom du script, stubs de vérification de type) ainsi que l'état de l'interpréteur, sous une version que la build de chargement vérifie :```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` et `RunProgress` n'ont pas de format de dump propre, mais implémentent tous deux `serde::Serialize`/`Deserialize`, si bien qu'un hôte peut sérialiser du code analysé ou une exécution suspendue avec le format qu'il utilise déjà.
## Limites de mémoire dans les workers
La `max_memory` d'une session est mesurée par l'allocateur du worker. L'interpréteur
signale proprement une `MemoryError` après avoir franchi la limite souple ; une limite
stricte plus élevée tue et remplace le worker si une allocation saute trop loin entre deux points de contrôle.
Voir [`limitations/resource_limits.md`](https://github.com/pydantic/monty/blob/HEAD/limitations/resource_limits.md) pour savoir comment
le dépassement d'une limite se manifeste pour un hôte, et `monty-alloc` pour l'allocateur sous
lequel s'exécutent les workers du sous-processus et WebAssembly.
## Intégration PydanticAI
Monty propulsera le mode code dans
[Pydantic AI](https://github.com/pydantic/pydantic-ai). Au lieu d'effectuer des
appels d'outils séquentiels, le LLM écrit du code Python qui appelle vos outils
comme fonctions et Monty l'exécute en toute sécurité.```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())
Il y a généralement deux réactions lorsqu'on montre Monty à des gens :
Où X est une technologie alternative. Curieusement, ces réactions sont souvent combinées, ce qui suggère que les gens n'ont pas encore trouvé une alternative qui leur convient, mais sont incrédules qu'il n'existe vraiment pas de bonne alternative à la création d'une implémentation Python entièrement nouvelle.
Je vais passer en revue les alternatives les plus évidentes et expliquer pourquoi elles ne convenaient pas à ce que nous voulions.
NOTE: toutes ces technologies sont impressionnantes et ont des usages répandus, ce commentaire sur leurs limites pour notre cas d'usage ne doit pas être perçu comme une critique. La plupart de ces solutions n'ont pas été conçues dans le but de fournir un sandbox pour LLM, c'est pourquoi elles n'y sont pas nécessairement excellentes.
Voir ./scripts/startup_performance.py pour le script utilisé pour calculer les chiffres de performance au démarrage.
Détails sur chaque ligne ci-dessous :
pip install pydantic-monty ou npm install @pydantic/monty, téléchargement d'environ 4.5 Modump() et load() rend triviale la pause, la reprise et la duplication (fork) de l'exécution.python:3.14-alpine pèse 50 Mo - docker ne peut pas être installé depuis PyPIVoir starlark-rust.
Exécuter Python dans WebAssembly via Wasmer.
python/python wasmer n'a pas de readme, pas de licence, pas de lien source et aucune indication sur la façon dont il est construit ; les versions récemment téléversées affichent une taille « 0B » alors que le téléchargement fait ~50 Mo - le processus de construction du binaire Python n'est ni clair ni transparent. (Si je me trompe, veuillez créer une issue pour me corriger)Des services comme Daytona, E2B, Modal.
Il y a des défis similaires, plus de complexité de configuration mais une latence réseau plus faible pour mettre en place votre propre configuration sandbox avec k8s.
Exécuter Python directement via exec() (~0.1ms) ou subprocess (~30ms).
exec(), ~30ms pour subprocessLe Pydantic Stack est tout ce dont vous avez besoin pour livrer des agents IA de qualité production :
dataclasses| Technologie | Complétude du langage | Sécurité | Latence de démarrage | FOSS | Complexité de configuration | Montage de fichiers | Instantanés |
|---|
| Monty | partielle | stricte | 0.06ms | gratuit / OSS | facile | facile | facile |
| Docker | complète | bonne | 195ms | gratuit / OSS | intermédiaire | facile | intermédiaire |
| Pyodide | complète | mauvaise | 2800ms | gratuit / OSS | intermédiaire | facile | difficile |
| starlark-rust | très limitée | bonne | 1.7ms | gratuit / OSS | facile | non disponible ? | impossible ? |
| WASI / Wasmer | partielle, presque complète | stricte | 66ms | gratuit * | intermédiaire | facile | intermédiaire |
| service de sandbox | complète | stricte | 1033ms | non gratuit | intermédiaire | difficile | intermédiaire |
| YOLO Python | complète | inexistante | 0.1ms / 30ms | gratuit / OSS | facile | facile / effrayant | difficile |