تنفيذ التعليمات البرمجية Python ‏(Monty)

يشغّل ADK-Rust تعليمات Python البرمجية التي يكتبها النموذج ضمن العملية نفسها عبر مفسّر Pydantic Monty — من دون حاوية، ومن دون عملية فرعية، مع بدء التشغيل خلال ميكروثوانٍ. تأتي هذه الإمكانية في طبقتين:

  • adk-code (ميزة embedded-python) — MontyExecutorBuilder ومنتجا التنفيذ، MontyOneShotExecutor وMontyReplExecutor، وكلاهما يطبّق CodeExecutor.
  • adk-tool (ميزة code-embedded-python) — MontyPythonCodeTool (monty_python_code)، وهي الأداة الموجّهة إلى الوكيل فوق منفّذي التنفيذ هذين.

وهذا مكمّل لـ PythonCodeTool المدعوم بالحاويات (python_code)، الذي يشغّل CPython الكامل في Docker — استخدمه عندما تحتاج البرامج النصية إلى منظومة Python الحقيقية (حزم pip، وامتدادات C، والمكتبة القياسية الكاملة). يطبّق Monty مجموعة فرعية من Python، مقابل سرعة التنفيذ ضمن العملية نفسها، وحالة مفسّر قابلة للتسلسل، وضمان عدم استخدام الشبكة أو العمليات الفرعية يتحقق بحكم التصميم.

[dependencies]
adk-tool = { version = "2.1.0", features = ["code-embedded-python"] }

أو عبر الحزمة الجامعة:

[dependencies]
adk-rust = { version = "2.1.0", features = ["minimal", "code-embedded-python"] }

التنفيذ لمرة واحدة مقابل REPL

ينتج منشئ واحد كلا المنتجين؛ ويُشفَّر النمط في النوع، وليس في راية:

الوضعالبناءالحالةالتزامن
لمرة واحدةbuild_one_shot()مفسّر جديد لكل استدعاءآمن للتزامن
REPLbuild_repl()تستمر المتغيرات والدوال وعمليات الاستيراد بين الاستدعاءاتتُسلسل الاستدعاءات لكل جلسة
use adk_code::{MontyExecutorBuilder, PathAccess};

let builder = MontyExecutorBuilder::new()
    .allow_path("/data", "/srv/agent/data", PathAccess::ReadOnly)
    .allow_path("/out", "/srv/agent/out", PathAccess::ReadWrite)
    .environ_var("PROJECT", "acme")
    .system_clock();

let one_shot = builder.clone().build_one_shot()?;
let repl = builder.build_repl()?;

يخزّن منفّذ REPL المفسّر المتسلسل بين الاستدعاءات. يحافظ Monty على الجلسة عبر الاستثناءات على مستوى Python، لذا لا يؤدي المقطع البرمجي الفاشل إلى تدمير الحالة المتراكمة. تدير طرق دورة الحياة في CodeExecutor الجلسة: تهيّئها start()، وتسقطها stop()، وتعيد ضبطها restart()، كما تهيّئها execute() بشكل كسول قبل start().

نموذج الأمان

يجمع العزل بين سياسة صريحة وإنفاذ يعتمد على المنع:

  • نظام الملفات. لا يمكن الوصول إلا إلى الأدلة الممنوحة باستخدام allow_path، ويكون كل منها للقراءة فقط أو للقراءة والكتابة، عبر pathlib.Path مقابل مسار التحميل الافتراضي. يفرض جدول التحميل في Monty الحدود (التطبيع + اكتشاف الهروب عبر الروابط الرمزية). يؤدي أي مسار آخر إلى رفع OSError القابل للاعتراض (بينما تعيد عمليات التحقق من الوجود False).
  • البيئة. لا يقرأ os.getenv / os.environ سوى الخريطة الصريحة الممنوحة عند الإنشاء — ولا تُكشف بيئة العملية المضيفة مطلقًا.
  • الساعة. لا تعمل date.today() / datetime.now() إلا عند منح .system_clock()؛ وإلا فإنها ترفع OSError.
  • الشبكة والعمليات الفرعية. لا يوفّر Monty واجهة لأيٍّ منهما — وهذا مستحيل بغض النظر عن الإعدادات.
  • المهلات الزمنية. تُحوّل SandboxPolicy::timeout إلى ResourceLimits::max_duration في Monty (إيقاف استباقي حقيقي داخل الآلة الافتراضية، لكل استدعاء). يحدّ سقف الذاكرة (الافتراضي 256 MiB) الذاكرة المُكدّسة؛ وفي وضع REPL يحدّ الذاكرة المُكدّسة التراكمية للجلسة.

المنح مقابل سياسة الطلب. تمثل منح المُنشئ أقصى مستوى وصول يمكن لأي برنامج نصي امتلاكه. لا يجوز لـ SandboxPolicy لكل طلب إلا تقليص هذا النطاق — ويُرفض الطلب الذي يتجاوز المنح رفضًا آمنًا عند الفشل، مع ExecutionError::UnsupportedPolicy الذي يحدد التجاوز، قبل تشغيل أي تعليمات برمجية. تغطي المنحة شجرة الأدلة الفرعية بأكملها: ينجح طلب نقطة تحميل ممنوحة أو أي دليل فرعي منها، وتكون نقطة التحميل الفعلية هي المسار المطلوب المدعوم بالدليل الفرعي المطابق على المضيف. استخدم granted_policy() لطلب ما يقدمه المنفذ بالضبط.

يجب ألا تختلف السياسة الفعلية لجلسة REPL بين الاستدعاءات؛ ويُرفض الاستدعاء الذي تختلف سياسته عن السياسة المعتمدة للجلسة مع توجيه إلى restart().

وظائف المضيف

تصبح وظائف Rust المسجلة (المتزامنة أو غير المتزامنة) وظائف Python قابلة للاستدعاء، وتكون مرئية للبرامج النصية بالاسم المجرد:

use adk_code::MontyExecutorBuilder;
use serde_json::json;

let executor = MontyExecutorBuilder::new()
    .function_fn("row_count", "Count rows in the loaded dataset.", |args, _kwargs| async move {
        Ok(json!(args.len()))
    })
    .build_one_shot()?;

للاطلاع على صيغة السمة الكاملة، نفّذ HostFunction (name، وdescription، والاختيارية signature لمطالبة LLM، وcall غير المتزامنة مع وسيطات موضعية ومسمّاة محوّلة بواسطة JSON). يحدث التحقق من السجل في build_*(): يجب أن تكون الأسماء معرّفات Python صالحة وفريدة، وألا تتعارض مع الدوال المضمنة في Python.

داخل البرنامج النصي، تُستدعى وظائف المضيف بشكل متزامن — وليس باستخدام await مطلقًا. ويصبح Err المُعاد استثناءً في Python يمكن التقاطه ويحمل الرسالة؛ بينما يؤدي استدعاء اسم غير مسجل إلى رفع استثناء تصحيحي يسرد الأسماء المسجلة. لتنفيذ وظيفة المضيف حد زمني خاص به لساعة الجدار (host_function_timeout، وقيمته الافتراضية 30 ثانية)، بحيث لا تتمكن وظيفة عالقة من تعطيل execute().

ملاحظة: تعمل وظائف المضيف بصفتها تعليمات برمجية على المضيف. وهي حدود الثقة الخاصة بالمستخدم، وليست حدود Monty — إذ إن وضع الحماية في المفسّر لا يحتوي على آثارها الجانبية.

المنفذات ذات الوصف الذاتي

ينفّذ كلا المنفّذين CodeExecutor::prompt_snippet()، مع عرض إمكاناتهما المبنية: دلالات الوضع، وجذور نظام الملفات مع مستويات الوصول، وأسماء متغيرات البيئة (لا تُعرض القيم مطلقًا)، وتوفّر الساعة، وضمان عدم وجود شبكة أو عمليات فرعية، وعقد الإخراج، وكتلة stub بلغة Python لدوال المضيف المسجّلة. يضيف MontyPythonCodeTool المقتطف إلى الوصف المواجه لـ LLM، ولذلك يستمد كلٌّ من المطالبة والسلوك داخل المفسّر من الإعداد نفسه، ولا يمكن أن ينحرف أحدهما عن الآخر.

MontyPythonCodeTool

تعكس الأداة المواجهة للوكيل (monty_python_code، والنطاق code:execute) JavaScriptCodeTool: الخطأ باعتباره معلومة JSON، ومفاتيح الإخراج بصيغة camelCase، وبديل "rejected" منظّم عند تعطيل الميزة.

use adk_code::PathAccess;
use adk_tool::MontyPythonCodeTool;
use serde_json::json;
use std::sync::Arc;

let tool = MontyPythonCodeTool::builder()
    .allow_path("/out", "/srv/agent/out", PathAccess::ReadWrite)
    .environ_var("PROJECT", "acme")
    .system_clock()
    .function_fn("get_weather", "Current weather for a city.", |args, _kwargs| async move {
        Ok(json!({ "temp_c": 21 }))
    })
    .build_repl()?;

let agent = LlmAgentBuilder::new("data_agent")
    .instruction("Use monty_python_code for calculations and data work.")
    .model(model)
    .tool(Arc::new(tool))
    .build()?;

ينشئ MontyPythonCodeTool::new() أداة لمرة واحدة معزولة بالكامل؛ بينما ينشئ MontyPythonCodeTool::repl() أداة REPL معزولة بالكامل.

تحديد نطاق الجلسة

في وضع REPL، تُفهرس جلسات المفسّر باستخدام هوية الجلسة الكاملة في ADK — اسم التطبيق، ومعرّف المستخدم، ومعرّف الجلسة — ولذلك لا تتسرّب الحالة بين المستخدمين حتى عندما تتكرر سلاسل معرّفات الجلسات بين المستخدمين. تشترك جميع الجلسات في المنح وسجل دوال المضيف نفسه — أما حالة المفسّر وحدها فهي خاصة بكل جلسة. تكون خريطة الجلسات محدودة بسعة LRU (max_sessions، والافتراضي 100؛ وتُعامل القيمة 0 على أنها 1)؛ وعند إخلاء جلسة، تبدأ مكالمتها التالية بشفافية مفسّرًا جديدًا.

وسيطات الأداة

الوسيطةالنوعالوصف
codeسلسلة نصية (مطلوبة)مصدر Python المراد تنفيذه
inputأيًّا كانقيمة JSON اختيارية مرتبطة بالمتغير input
timeout_secsعدد صحيحميزانية وقت المفسّر (الافتراضي 30، ومحصورة بين 1 و300)
resetمنطقيوضع REPL فقط: تجاهل الجلسة الدائمة قبل التنفيذ

ظرف المخرجات

{ "status": "success", "stdout": "", "stderr": "", "output": {"n": 42},
  "stdoutTruncated": false, "stderrTruncated": false, "durationMs": 3 }

لا يوجد exitCode — فالتنفيذ يتم داخل العملية، ولا يتم إنشاء أي عملية؛ أما status فهو إشارة النجاح أو الفشل. وتُبلغ stdoutTruncated / stderrTruncated عن اقتطاع المخرجات الملتقطة عند حد البايت الذي تفرضه سياسة البيئة المعزولة (1 ميغابايت لكل منها افتراضيًا).

تُعاد قيمة التعبير النهائي للبرنامج النصي بوصفها output؛ وتُلتقط مخرجات print() بوصفها stdout. حالات الفشل: "failed" (استثناء في Python — أثر التتبع في stderr، بما في ذلك الاستثناءات التي ترفعها دوال المضيف)، و"timeout" (تجاوز الميزانية الزمنية)، و"rejected" (وسائط غير صالحة أو ميزة معطّلة). لا يوجد مطلقًا ToolError.

يظل الظرف ثابتًا في كلا النمطين، ولذلك تعلنه الأداة عبر Tool::response_schema() — إذ تتلقى المزوّدات التي تعرض مخططات الاستجابة هذا الظرف في تعريف الأداة إلى جانب parameters.

العلاقة مع CodeAct

يشغّل مسار CodeActAgent + adk-codeact-monty Python أيضًا عبر Monty، ولكن مع إرسال ADK Tool من داخل البرامج النصية (call_tool(...))، ومع التعليق والاستئناف عبر أدوار الوكيل. يستبعد MontyPythonCodeTool عمدًا كليهما — فهو أداة مستقلة لتنفيذ التعليمات البرمجية، ونقطة التوسعة فيها هي سجل دوال المضيف. راجع وكيل البرمجة للاطلاع على CodeAct.

مثال

يشغّل examples/monty_python_code_toolLlmAgent مع MontyPythonCodeTool في نمط REPL، وقد تم تكوينه بوصلة قراءة وكتابة، ومتغير بيئي، ودالة مضيف مسجّلة — موضحًا استمرارية المتغيرات عبر أدوار متعددة واستدعاءات دوال المضيف من Python الذي كتبه النموذج.