-
Notifications
You must be signed in to change notification settings - Fork 2
feat(python): add external function handler support #394
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -20,7 +20,9 @@ use monty::{ | |||||
| MontyObject, MontyRun, OsFunction, PrintWriter, ResourceLimits, RunProgress, | ||||||
| }; | ||||||
| use std::collections::HashMap; | ||||||
| use std::future::Future; | ||||||
| use std::path::{Path, PathBuf}; | ||||||
| use std::pin::Pin; | ||||||
| use std::sync::Arc; | ||||||
| use std::time::Duration; | ||||||
|
|
||||||
|
|
@@ -107,6 +109,27 @@ impl PythonLimits { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// Async handler for external Python function calls. | ||||||
| /// | ||||||
| /// Receives `(function_name, positional_args, keyword_args)` directly from monty. | ||||||
| /// Return `ExternalResult::Return(value)` for success or `ExternalResult::Error(exc)` for failure. | ||||||
| pub type PythonExternalFnHandler = Arc< | ||||||
| dyn Fn(String, Vec<MontyObject>, Vec<(MontyObject, MontyObject)>) | ||||||
| -> Pin<Box<dyn Future<Output = ExternalResult> + Send>> | ||||||
| + Send | ||||||
| + Sync, | ||||||
| >; | ||||||
|
|
||||||
| /// External function configuration for the Python builtin. | ||||||
| /// | ||||||
| /// Groups function names and their async handler together. | ||||||
| pub struct PythonExternalFns { | ||||||
| /// Function names callable from Python (e.g., `"call_tool"`). | ||||||
| pub names: Vec<String>, | ||||||
| /// Async handler invoked when Python calls one of these functions. | ||||||
| pub handler: PythonExternalFnHandler, | ||||||
| } | ||||||
|
|
||||||
| /// The python/python3 builtin command. | ||||||
| /// | ||||||
| /// Executes Python code using the embedded Monty interpreter (pydantic/monty). | ||||||
|
|
@@ -126,19 +149,38 @@ impl PythonLimits { | |||||
| pub struct Python { | ||||||
| /// Resource limits for the Monty interpreter. | ||||||
| pub limits: PythonLimits, | ||||||
| /// Optional external function configuration. | ||||||
| external_fns: Option<PythonExternalFns>, | ||||||
| } | ||||||
|
|
||||||
| impl Python { | ||||||
| /// Create with default limits. | ||||||
| pub fn new() -> Self { | ||||||
| Self { | ||||||
| limits: PythonLimits::default(), | ||||||
| external_fns: None, | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// Create with custom limits. | ||||||
| pub fn with_limits(limits: PythonLimits) -> Self { | ||||||
| Self { limits } | ||||||
| Self { | ||||||
| limits, | ||||||
| external_fns: None, | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// Set external function names and handler. | ||||||
| /// | ||||||
| /// External functions are callable from Python by name. | ||||||
| /// When called, execution pauses and the handler is invoked with the raw monty arguments. | ||||||
| pub fn with_external_handler( | ||||||
| mut self, | ||||||
| names: Vec<String>, | ||||||
| handler: PythonExternalFnHandler, | ||||||
| ) -> Self { | ||||||
| self.external_fns = Some(PythonExternalFns { names, handler }); | ||||||
| self | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -263,6 +305,7 @@ impl Builtin for Python { | |||||
| ctx.cwd, | ||||||
| ctx.env, | ||||||
| &self.limits, | ||||||
| self.external_fns.as_ref(), | ||||||
| ) | ||||||
| .await | ||||||
| } | ||||||
|
|
@@ -279,6 +322,7 @@ async fn run_python( | |||||
| cwd: &Path, | ||||||
| env: &HashMap<String, String>, | ||||||
| py_limits: &PythonLimits, | ||||||
| external_fns: Option<&PythonExternalFns>, | ||||||
| ) -> Result<ExecResult> { | ||||||
| // Strip shebang if present | ||||||
| let code = if code.starts_with("#!") { | ||||||
|
|
@@ -290,7 +334,10 @@ async fn run_python( | |||||
| code | ||||||
| }; | ||||||
|
|
||||||
| let runner = match MontyRun::new(code.to_owned(), filename, vec![], vec![]) { | ||||||
| let ext_fn_names = external_fns | ||||||
| .map(|ef| ef.names.clone()) | ||||||
| .unwrap_or_default(); | ||||||
| let runner = match MontyRun::new(code.to_owned(), filename, vec![], ext_fn_names) { | ||||||
| Ok(r) => r, | ||||||
| Err(e) => return Ok(format_exception(e)), | ||||||
| }; | ||||||
|
|
@@ -338,14 +385,25 @@ async fn run_python( | |||||
| } | ||||||
| } | ||||||
| } | ||||||
| RunProgress::FunctionCall { state, .. } => { | ||||||
| // No external functions registered; return error | ||||||
| let err = MontyException::new( | ||||||
| ExcType::RuntimeError, | ||||||
| Some("external function not available in virtual mode".into()), | ||||||
| ); | ||||||
| RunProgress::FunctionCall { | ||||||
| function_name, | ||||||
| args, | ||||||
| kwargs, | ||||||
| state, | ||||||
| .. | ||||||
| } => { | ||||||
| let result = if let Some(ef) = external_fns { | ||||||
| (ef.handler)(function_name, args, kwargs).await | ||||||
| } else { | ||||||
| // No external functions registered; return error | ||||||
| ExternalResult::Error(MontyException::new( | ||||||
| ExcType::RuntimeError, | ||||||
| Some("external function not available in virtual mode".into()), | ||||||
|
||||||
| Some("external function not available in virtual mode".into()), | |
| Some("no external function handler configured (external functions not enabled)".into()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PythonExternalFnsis a new public API type but doesn't derive common traits likeDebug/Clone. In this codebase, similar public config structs (e.g.,ExecutionLimits,GitConfig,NetworkAllowlist) derive these; adding#[derive(Debug, Clone)]here would improve ergonomics and keep the public surface consistent.