feat: auto-install Python deps from template pyproject.toml via uv
ExternalToolManager.discover() now accepts template root dir, detects pyproject.toml and runs `uv sync` to create a venv. Tool invocation and schema discovery inject the venv PATH/VIRTUAL_ENV so template tools can import declared dependencies without manual installation.
This commit is contained in:
97
src/tools.rs
97
src/tools.rs
@@ -11,18 +11,73 @@ struct ExternalTool {
|
||||
|
||||
pub struct ExternalToolManager {
|
||||
tools: HashMap<String, ExternalTool>,
|
||||
venv_bin: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl ExternalToolManager {
|
||||
/// Scan a tools/ directory, calling `--print-schema` on each executable to discover tools.
|
||||
pub async fn discover(tools_dir: &Path) -> Self {
|
||||
/// Build the PATH string with venv bin prepended (if present).
|
||||
fn env_path(&self) -> Option<String> {
|
||||
self.venv_bin.as_ref().map(|venv_bin| {
|
||||
format!(
|
||||
"{}:{}",
|
||||
venv_bin.display(),
|
||||
std::env::var("PATH").unwrap_or_default()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Scan a template directory for external tools and Python dependencies.
|
||||
///
|
||||
/// - Discovers executables in `template_dir/tools/`
|
||||
/// - If `template_dir/pyproject.toml` exists, runs `uv sync` to create a venv
|
||||
pub async fn discover(template_dir: &Path) -> Self {
|
||||
let mut tools = HashMap::new();
|
||||
|
||||
let mut entries = match tokio::fs::read_dir(tools_dir).await {
|
||||
Ok(e) => e,
|
||||
Err(_) => return Self { tools },
|
||||
// --- Python venv setup ---
|
||||
let venv_bin = if template_dir.join("pyproject.toml").is_file() {
|
||||
tracing::info!("Found pyproject.toml in {}, running uv sync", template_dir.display());
|
||||
let output = tokio::process::Command::new("uv")
|
||||
.args(["sync", "--project", &template_dir.to_string_lossy(), "--quiet"])
|
||||
.output()
|
||||
.await;
|
||||
match output {
|
||||
Ok(o) if o.status.success() => {
|
||||
let bin = template_dir.join(".venv/bin");
|
||||
tracing::info!("uv sync succeeded, venv bin: {}", bin.display());
|
||||
Some(bin)
|
||||
}
|
||||
Ok(o) => {
|
||||
tracing::warn!(
|
||||
"uv sync failed: {}",
|
||||
String::from_utf8_lossy(&o.stderr)
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to run uv sync: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// --- Tool discovery ---
|
||||
let tools_dir = template_dir.join("tools");
|
||||
let mut entries = match tokio::fs::read_dir(&tools_dir).await {
|
||||
Ok(e) => e,
|
||||
Err(_) => return Self { tools, venv_bin },
|
||||
};
|
||||
|
||||
// Build PATH with venv for --print-schema calls
|
||||
let env_path = venv_bin.as_ref().map(|bin| {
|
||||
format!(
|
||||
"{}:{}",
|
||||
bin.display(),
|
||||
std::env::var("PATH").unwrap_or_default()
|
||||
)
|
||||
});
|
||||
|
||||
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||
let path = entry.path();
|
||||
|
||||
@@ -41,12 +96,16 @@ impl ExternalToolManager {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Call --print-schema
|
||||
let output = match tokio::process::Command::new(&path)
|
||||
.arg("--print-schema")
|
||||
.output()
|
||||
.await
|
||||
{
|
||||
// Call --print-schema (with venv PATH if available)
|
||||
let mut cmd = tokio::process::Command::new(&path);
|
||||
cmd.arg("--print-schema");
|
||||
if let Some(ref p) = env_path {
|
||||
cmd.env("PATH", p);
|
||||
}
|
||||
if let Some(ref bin) = venv_bin {
|
||||
cmd.env("VIRTUAL_ENV", bin.parent().unwrap().display().to_string());
|
||||
}
|
||||
let output = match cmd.output().await {
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to run --print-schema on {}: {}", path.display(), e);
|
||||
@@ -102,7 +161,7 @@ impl ExternalToolManager {
|
||||
);
|
||||
}
|
||||
|
||||
Self { tools }
|
||||
Self { tools, venv_bin }
|
||||
}
|
||||
|
||||
/// Return all discovered Tool definitions for LLM API calls.
|
||||
@@ -122,11 +181,15 @@ impl ExternalToolManager {
|
||||
.get(name)
|
||||
.ok_or_else(|| anyhow::anyhow!("External tool not found: {}", name))?;
|
||||
|
||||
let output = tokio::process::Command::new(&tool.path)
|
||||
.arg(args_json)
|
||||
.current_dir(workdir)
|
||||
.output()
|
||||
.await?;
|
||||
let mut cmd = tokio::process::Command::new(&tool.path);
|
||||
cmd.arg(args_json).current_dir(workdir);
|
||||
if let Some(ref p) = self.env_path() {
|
||||
cmd.env("PATH", p);
|
||||
}
|
||||
if let Some(ref venv_bin) = self.venv_bin {
|
||||
cmd.env("VIRTUAL_ENV", venv_bin.parent().unwrap().display().to_string());
|
||||
}
|
||||
let output = cmd.output().await?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
|
||||
Reference in New Issue
Block a user