use clap_noun_verb::Result as NounVerbResult;
use clap_noun_verb_macros::verb;
use serde::Serialize;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Serialize)]
struct BridgeLangChainOutput {
tool_name: String,
framework: String,
output_path: String,
status: String,
message: String,
python_syntax_valid: bool,
}
#[derive(Clone, Debug)]
struct TemplateContext {
tool_name: String,
description: String,
parameters: String,
}
#[verb]
fn bridge_langchain(name: String) -> NounVerbResult<BridgeLangChainOutput> {
validate_component_name(&name)?;
let output_dir = PathBuf::from("output/langchain");
fs::create_dir_all(&output_dir).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to create output directory: {}",
e
))
})?;
let context = TemplateContext {
tool_name: name.clone(),
description: format!("LangChain adapter for {}", name),
parameters: "input: str".to_string(),
};
let adapter_code = render_langchain_template(&context)?;
let syntax_valid = verify_python_syntax(&adapter_code).unwrap_or(false);
if !syntax_valid {
return Err(clap_noun_verb::NounVerbError::execution_error(
"Generated Python code has invalid syntax".to_string(),
));
}
let output_file = output_dir.join(format!("{}.tool.py", name));
fs::write(&output_file, &adapter_code).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write output file: {}",
e
))
})?;
let output_path = output_file.to_string_lossy().to_string();
Ok(BridgeLangChainOutput {
tool_name: name.clone(),
framework: "langchain".to_string(),
output_path,
status: "generated".to_string(),
message: format!(
"LangChain adapter for '{}' generated successfully. Location: {}",
name,
output_dir.display()
),
python_syntax_valid: syntax_valid,
})
}
fn validate_component_name(name: &str) -> NounVerbResult<()> {
if name.trim().is_empty() {
return Err(clap_noun_verb::NounVerbError::argument_error(
"Component name must not be empty",
));
}
let valid = name
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-');
if !valid {
return Err(clap_noun_verb::NounVerbError::argument_error(
"Component name contains invalid characters. Use alphanumeric, underscores, hyphens only.",
));
}
Ok(())
}
fn render_langchain_template(context: &TemplateContext) -> NounVerbResult<String> {
let template_path = "templates/langchain.tool.py.tera";
if Path::new(template_path).exists() {
use tera::Tera;
let tera = Tera::new("templates/*.tera").map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to load templates: {}",
e
))
})?;
let mut ctx = tera::Context::new();
ctx.insert("tool_name", &context.tool_name);
ctx.insert("description", &context.description);
ctx.insert("parameters", &context.parameters);
tera.render("langchain.tool.py.tera", &ctx).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to render template: {}",
e
))
})
} else {
generate_langchain_fallback(context)
}
}
fn generate_langchain_fallback(context: &TemplateContext) -> NounVerbResult<String> {
let pascal_name = pascal_case(&context.tool_name);
let code = format!(
r#"from langchain.tools import BaseTool
from typing import Optional
class {}Tool(BaseTool):
"""{}"""
name = "{}"
description = "{}"
def _run(self, {}) -> str:
"""Execute the tool."""
# Call wrapped component
return "{} executed"
async def _arun(self, {}) -> str:
"""Execute the tool asynchronously."""
return self._run({})
"#,
pascal_name,
context.description,
context.tool_name,
context.description,
context.parameters,
context.tool_name,
context.parameters,
extract_param_names(&context.parameters)
);
Ok(code)
}
fn pascal_case(input: &str) -> String {
input
.split('_')
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect()
}
fn extract_param_names(params: &str) -> String {
params
.split(',')
.filter_map(|p| {
p.trim()
.split(':')
.next()
.map(|name| name.trim().to_string())
})
.collect::<Vec<_>>()
.join(", ")
}
fn verify_python_syntax(code: &str) -> Result<bool, String> {
let temp_file = std::env::temp_dir().join("ggen_verify_syntax.py");
fs::write(&temp_file, code).map_err(|e| format!("Failed to write temp file: {}", e))?;
let temp_path_str = temp_file.to_string_lossy().to_string();
let output = Command::new("python3")
.arg("-m")
.arg("py_compile")
.arg(&temp_path_str)
.output()
.or_else(|_| {
Command::new("python")
.arg("-m")
.arg("py_compile")
.arg(&temp_path_str)
.output()
})
.map_err(|e| format!("Failed to run Python: {}", e))?;
let _ = fs::remove_file(&temp_file);
Ok(output.status.success())
}