
AI द्वारा उपयोग के लिए Rust में लिखा गया एक न्यूनतम, सुरक्षित Python interpreter
प्रायोगिक - यह प्रोजेक्ट अभी भी विकास के अंतर्गत है, और अभी पूर्ण रूप से उपयोग के लिए तैयार नहीं है।
AI द्वारा उपयोग के लिए Rust में लिखा गया एक न्यूनतम, सुरक्षित Python इंटरप्रेटर।
Monty, LLM द्वारा उत्पन्न कोड चलाने के लिए पूर्ण कंटेनर-आधारित सैंडबॉक्स का उपयोग करने की लागत, विलंबता, जटिलता और सामान्य झंझट से बचता है।
इसके बजाय, यह आपको अपने एजेंट में एम्बेड किए गए LLM द्वारा लिखे गए Python कोड को सुरक्षित रूप से चलाने देता है, जिसमें स्टार्टअप समय सैकड़ों मिलीसेकंड नहीं बल्कि एकल अंकों के माइक्रोसेकंड में मापा जाता है।
Monty क्या कर सकता है:
sys, os, typing, asyncio, re, datetime, json, dataclasses (जल्द ही)Monty क्या नहीं कर सकता:
संक्षेप में, Monty अत्यंत सीमित है और एक उपयोग के मामले के लिए डिज़ाइन किया गया है:
एजेंटों द्वारा लिखे गए कोड को चलाने के लिए।
आप ऐसा क्यों करना चाहेंगे इसकी प्रेरणा के लिए, देखें:
बहुत सरल शब्दों में, उपरोक्त सभी का विचार यह है कि LLMs तेज़ी से, सस्ते और अधिक विश्वसनीय रूप से काम कर सकते हैं यदि उन्हें पारंपरिक टूल कॉलिंग पर निर्भर रहने के बजाय Python (या Javascript) कोड लिखने के लिए कहा जाए। 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` worker) के साथ जोड़ता है। यदि बाइनरी पहले से कहीं और से आती है तो केवल `pydantic-monty-client` इंस्टॉल करें।
Usage:```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 पैकेज उसी Rust worker pool पर एक नेटिव (napi) बाइंडिंग है जिसका Python पैकेज उपयोग करता है — बाइंडिंग और monty worker बाइनरी प्लेटफ़ॉर्म-विशिष्ट 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` लागू करते हैं, इसलिए एक होस्ट पार्स किए गए कोड या रोके गए रन को किसी भी ऐसे प्रारूप में serialize कर सकता है जिसका वह पहले से उपयोग करता है।
## वर्कर्स में मेमोरी सीमाएँ
किसी सत्र की `max_memory` वर्कर के आवंटक द्वारा मापी जाती है। इंटरप्रेटर सॉफ्ट सीमा पार करने के बाद एक सहज `MemoryError` रिपोर्ट करता है; एक उच्चतर हार्ड सीमा वर्कर को समाप्त कर देती है और उसे बदल देती है यदि कोई आवंटन चेकपॉइंट्स के बीच बहुत अधिक उछलता है।
सीमा पार होने पर यह होस्ट तक कैसे पहुँचता है, यह देखने के लिए [`limitations/resource_limits.md`](https://github.com/pydantic/monty/blob/main/limitations/resource_limits.md) देखें, और `monty-alloc` उस आवंटक के लिए देखें जिसके अंतर्गत सबप्रोसेस और WebAssembly दोनों वर्कर्स चलते हैं।
## PydanticAI एकीकरण
Monty [Pydantic AI](https://github.com/pydantic/pydantic-ai) में कोड-मोड को संचालित करेगा। अनुक्रमिक टूल कॉल करने के बजाय, 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 दिखाते हैं, तो आम तौर पर दो प्रतिक्रियाएँ मिलती हैं:
जहाँ X कोई वैकल्पिक तकनीक है। अजीब बात है कि अक्सर ये दोनों प्रतिक्रियाएँ संयुक्त होती हैं, जिससे पता चलता है कि लोगों को अभी तक कोई ऐसा विकल्प नहीं मिला है जो उनके लिए काम करता हो, लेकिन वे यह मानने को तैयार नहीं हैं कि शुरू से संपूर्ण Python इम्प्लीमेंटेशन बनाने का वास्तव में कोई अच्छा विकल्प नहीं है।
मैं सबसे स्पष्ट विकल्पों पर चर्चा करूँगा, और वे हमारी आवश्यकताओं के लिए सही क्यों नहीं हैं।
नोट: ये सभी तकनीकें प्रभावशाली हैं और इनके व्यापक उपयोग हैं; हमारे उपयोग-मामले के लिए उनकी सीमाओं पर यह टिप्पणी आलोचना के रूप में नहीं देखी जानी चाहिए। इनमें से अधिकांश समाधान LLM सैंडबॉक्स प्रदान करने के लक्ष्य के साथ नहीं बनाए गए थे, इसीलिए वे इसमें आवश्यक रूप से उत्कृष्ट नहीं हैं।
| तकनीक | भाषा पूर्णता | सुरक्षा | स्टार्ट विलंबता | 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 | आसान | आसान / डरावना | कठिन |
स्टार्टअप परफॉर्मेंस आँकड़ों की गणना करने के लिए उपयोग की गई स्क्रिप्ट के लिए ./scripts/startup_performance.py देखें।
नीचे प्रत्येक पंक्ति का विवरण:
pip install pydantic-monty या npm install @pydantic/monty, ~4.5MB डाउनलोडdump() और load() के साथ Monty की pause और resume कार्यक्षमता निष्पादन को pause, resume और fork करना बेहद आसान बना देती हैpython:3.14-alpine 50MB है - docker को PyPI से इंस्टॉल नहीं किया जा सकताstarlark-rust देखें।
Wasmer के माध्यम से WebAssembly में Python चलाना।
python/python wasmer पैकेज में कोई readme नहीं है, कोई लाइसेंस नहीं है, कोई सोर्स लिंक नहीं है और यह संकेत नहीं है कि इसे कैसे बनाया गया है; हाल ही में अपलोड किए गए संस्करणों का आकार "0B" दिखाता है जबकि डाउनलोड ~50MB है - Python बाइनरी के लिए बिल्ड प्रक्रिया स्पष्ट और पारदर्शी नहीं है। (अगर मैं यहाँ गलत हूँ, तो कृपया मुझे सही करने के लिए एक issue बनाएँ)k8s के साथ अपना स्वयं का सैंडबॉक्स सेटअप स्थापित करने के लिए समान चुनौतियाँ हैं, अधिक सेटअप जटिलता लेकिन कम नेटवर्क विलंबता।
Python को सीधे exec() (~0.1ms) या subprocess (~30ms) के माध्यम से चलाना।
exec() के लिए लगभग शून्य, subprocess के लिए ~30msPydantic स्टैक वह सब कुछ है जो आपको प्रोडक्शन-ग्रेड AI एजेंट तैनात करने के लिए चाहिए: