use crate::event::new_one_shot_channel;
use crate::model::RuntimeCtx;
use crate::run::RunSubAgentParams;
use crate::runtime::Runtime;
use crate::script::LuaValueExt;
use crate::types::{RunAgentOptions, RunAgentResponse};
use crate::{Error, Result};
use mlua::{IntoLua, Lua, Table, Value};
use simple_fs::SPath;
pub fn init_module(lua: &Lua, runtime: &Runtime) -> Result<Table> {
let table = lua.create_table()?;
let rt = runtime.clone();
let agent_run = lua.create_async_function(
move |lua, (agent_name, run_options): (String, Option<RunAgentOptions>)| {
let rt = rt.clone();
async move { aip_agent_run(&lua, &rt, agent_name, run_options.unwrap_or_default()).await }
},
)?;
let extract_options = lua.create_function(move |lua, value: Value| aip_agent_extract_options(lua, value))?;
table.set("run", agent_run)?;
table.set("extract_options", extract_options)?;
Ok(table)
}
pub async fn aip_agent_run(
lua: &Lua,
runtime: &Runtime,
agent_name: String,
run_options: RunAgentOptions,
) -> mlua::Result<Value> {
let parent_agent_dir = get_agent_dir_from_lua(lua);
let (tx, rx) = new_one_shot_channel::<Result<RunAgentResponse>>("agent-run");
let rt_ctx = RuntimeCtx::extract_from_global(lua)?;
let parent_uid = rt_ctx
.run_uid()
.ok_or(Error::custom("Cannot call agent, no parent run uid found"))?;
let run_agent_params = RunSubAgentParams::new(
runtime.clone(),
parent_uid,
parent_agent_dir,
agent_name,
run_options,
Some(tx),
)?;
runtime.executor_sender().send(run_agent_params.into()).await;
let res = rx.recv().await;
let run_agent_response = res.map_err(|err| Error::custom(format!("rx.recv_async fail. Cause: {err}")))??;
run_agent_response.into_lua(lua)
}
pub fn aip_agent_extract_options(lua: &Lua, value: Value) -> mlua::Result<Value> {
match value {
Value::Table(table) => {
let result_table = lua.create_table()?;
let keys_to_copy = [
"model",
"model_aliases",
"input_concurrency",
"temperature",
"top_p",
"allow_run_on_task_fail",
];
for key in keys_to_copy.iter() {
if let Some(val) = table.x_get_value(key)
&& val != Value::Nil
{
result_table.set(*key, val)?;
}
}
Ok(Value::Table(result_table))
}
_ => Ok(Value::Nil),
}
}
fn get_agent_dir_from_lua(lua: &Lua) -> Option<SPath> {
lua.globals()
.x_get_value("CTX")?
.x_get_string("AGENT_FILE_DIR")
.map(|s| s.into())
}
#[cfg(test)]
mod tests {
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use crate::_test_support::{assert_contains, eval_lua, run_reflective_agent, run_test_agent, setup_lua};
use crate::agent::Agent;
use crate::runtime::Runtime;
use value_ext::JsonValueExt;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_script_aip_agent_run_simple() -> Result<()> {
let script = r#"
local result = aip.agent.run("agent-script/agent-hello-world")
return result
"#;
let mut res = run_reflective_agent(script, None).await?;
let mut outputs = res.x_remove::<serde_json::Value>("outputs")?;
let len = outputs.as_array().map(|a| a.len()).ok_or("'outputs' should be array")?;
assert_eq!(len, 1, "'outputs' should have one output");
let output = outputs.x_remove::<String>("/0")?;
assert_contains(&output, "Hello Wonderful World");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_script_aip_agent_run_relative() -> Result<()> {
let runtime = Runtime::new_test_runtime_sandbox_01().await?;
let agent_file = runtime
.dir_context()
.wks_dir()
.ok_or("Should have workspace setup")?
.join("agent-script/agent-calling-hello.aip");
let agent = Agent::mock_from_file(agent_file.as_str())?;
let mut res = run_test_agent(&runtime, &agent).await?;
let res = res.x_remove::<String>("res")?;
assert_contains(&res, "Hello 'me' from agent-hello.aip");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_script_aip_agent_run_with_input() -> Result<()> {
let script = r#"
local result = aip.agent.run("agent-script/agent-hello", { inputs = {"John"} })
return result
"#;
let mut res = run_reflective_agent(script, None).await?;
let mut outputs = res.x_remove::<serde_json::Value>("outputs")?;
let len = outputs.as_array().map(|a| a.len()).ok_or("'ouputs' should be array")?;
assert_eq!(len, 1, "'ouputs' should have one output");
let output = outputs.x_remove::<String>("/0")?;
assert_contains(&output, "Hello 'John' from agent-hello.aip");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_script_aip_agent_run_with_options() -> Result<()> {
let script = r#"
local result = aip.agent.run("agent-script/agent-hello",
{ inputs = {"John"},
options = {model = "super-fast"}
}
)
return result
"#;
let mut res = run_reflective_agent(script, None).await?;
let mut outputs = res.x_remove::<serde_json::Value>("outputs")?;
let len = outputs.as_array().map(|a| a.len()).ok_or("'ouputs' should be array")?;
assert_eq!(len, 1, "'ouputs' should have one output");
let output = outputs.x_remove::<String>("/0")?;
assert_contains(&output, "Hello 'John' from agent-hello.aip");
assert_contains(&output, "options.model = super-fast");
Ok(())
}
#[tokio::test]
async fn test_script_aip_agent_extract_options_simple() -> Result<()> {
let lua = setup_lua(super::init_module, "agent").await?;
let script = r#"
local big = {
model = "gpt-4",
temperature = 0.8,
input_comcurrency = 5,
some_other_prop = "ignore_me",
model_aliases = { fast = "gpt-3.5" },
nil_prop = nil -- should not be included
}
return aip.agent.extract_options(big)
"#;
let res = eval_lua(&lua, script)?;
assert_eq!(res.x_get_str("model")?, "gpt-4");
assert_eq!(res.x_get_f64("temperature")?, 0.8);
assert_eq!(res.x_get_i64("input_comcurrency")?, 5);
assert!(
res.x_get::<serde_json::Value>("some_other_prop").is_err(),
"Should not have 'some_other_prop'"
);
Ok(())
}
#[tokio::test]
async fn test_script_aip_agent_extract_options_string() -> Result<()> {
let lua = setup_lua(super::init_module, "agent").await?;
let script = r#"
return aip.agent.extract_options("Some string")
"#;
let res = eval_lua(&lua, script)?;
assert!(matches!(res, serde_json::Value::Null), "Should be nil but was: {res:?}");
Ok(())
}
#[tokio::test]
async fn test_script_aip_agent_extract_options_nil() -> Result<()> {
let lua = setup_lua(super::init_module, "agent").await?;
let script = r#"
return aip.agent.extract_options(nil)
"#;
let res = eval_lua(&lua, script)?;
assert!(matches!(res, serde_json::Value::Null), "Should be nil but was: {res:?}");
Ok(())
}
}