ggen_cli_lib/cmds/
framework.rs1use clap_noun_verb::Result as NounVerbResult;
12use clap_noun_verb_macros::verb;
13use serde::Serialize;
14use std::fs;
15use std::path::{Path, PathBuf};
16use std::process::Command;
17
18#[derive(Serialize)]
23struct BridgeLangChainOutput {
24 tool_name: String,
25 framework: String,
26 output_path: String,
27 status: String,
28 message: String,
29 python_syntax_valid: bool,
30}
31
32#[derive(Clone, Debug)]
37struct TemplateContext {
38 tool_name: String,
39 description: String,
40 parameters: String,
41}
42
43#[verb]
60fn bridge_langchain(name: String) -> NounVerbResult<BridgeLangChainOutput> {
61 validate_component_name(&name)?;
63
64 let output_dir = PathBuf::from("output/langchain");
66 fs::create_dir_all(&output_dir).map_err(|e| {
67 clap_noun_verb::NounVerbError::execution_error(format!(
68 "Failed to create output directory: {}",
69 e
70 ))
71 })?;
72
73 let context = TemplateContext {
75 tool_name: name.clone(),
76 description: format!("LangChain adapter for {}", name),
77 parameters: "input: str".to_string(),
78 };
79
80 let adapter_code = render_langchain_template(&context)?;
82
83 let syntax_valid = verify_python_syntax(&adapter_code).unwrap_or(false);
85 if !syntax_valid {
86 return Err(clap_noun_verb::NounVerbError::execution_error(
87 "Generated Python code has invalid syntax".to_string(),
88 ));
89 }
90
91 let output_file = output_dir.join(format!("{}.tool.py", name));
93 fs::write(&output_file, &adapter_code).map_err(|e| {
94 clap_noun_verb::NounVerbError::execution_error(format!(
95 "Failed to write output file: {}",
96 e
97 ))
98 })?;
99
100 let output_path = output_file.to_string_lossy().to_string();
102 Ok(BridgeLangChainOutput {
103 tool_name: name.clone(),
104 framework: "langchain".to_string(),
105 output_path,
106 status: "generated".to_string(),
107 message: format!(
108 "LangChain adapter for '{}' generated successfully. Location: {}",
109 name,
110 output_dir.display()
111 ),
112 python_syntax_valid: syntax_valid,
113 })
114}
115
116fn validate_component_name(name: &str) -> NounVerbResult<()> {
122 if name.trim().is_empty() {
123 return Err(clap_noun_verb::NounVerbError::argument_error(
124 "Component name must not be empty",
125 ));
126 }
127 let valid = name
128 .chars()
129 .all(|c| c.is_alphanumeric() || c == '_' || c == '-');
130 if !valid {
131 return Err(clap_noun_verb::NounVerbError::argument_error(
132 "Component name contains invalid characters. Use alphanumeric, underscores, hyphens only.",
133 ));
134 }
135 Ok(())
136}
137
138fn render_langchain_template(context: &TemplateContext) -> NounVerbResult<String> {
140 let template_path = "templates/langchain.tool.py.tera";
142
143 if Path::new(template_path).exists() {
144 use tera::Tera;
146
147 let tera = Tera::new("templates/*.tera").map_err(|e| {
149 clap_noun_verb::NounVerbError::execution_error(format!(
150 "Failed to load templates: {}",
151 e
152 ))
153 })?;
154
155 let mut ctx = tera::Context::new();
156 ctx.insert("tool_name", &context.tool_name);
157 ctx.insert("description", &context.description);
158 ctx.insert("parameters", &context.parameters);
159
160 tera.render("langchain.tool.py.tera", &ctx).map_err(|e| {
161 clap_noun_verb::NounVerbError::execution_error(format!(
162 "Failed to render template: {}",
163 e
164 ))
165 })
166 } else {
167 generate_langchain_fallback(context)
169 }
170}
171
172fn generate_langchain_fallback(context: &TemplateContext) -> NounVerbResult<String> {
174 let pascal_name = pascal_case(&context.tool_name);
175
176 let code = format!(
177 r#"from langchain.tools import BaseTool
178from typing import Optional
179
180class {}Tool(BaseTool):
181 """{}"""
182
183 name = "{}"
184 description = "{}"
185
186 def _run(self, {}) -> str:
187 """Execute the tool."""
188 # Call wrapped component
189 return "{} executed"
190
191 async def _arun(self, {}) -> str:
192 """Execute the tool asynchronously."""
193 return self._run({})
194"#,
195 pascal_name,
196 context.description,
197 context.tool_name,
198 context.description,
199 context.parameters,
200 context.tool_name,
201 context.parameters,
202 extract_param_names(&context.parameters)
203 );
204
205 Ok(code)
206}
207
208fn pascal_case(input: &str) -> String {
210 input
211 .split('_')
212 .map(|word| {
213 let mut chars = word.chars();
214 match chars.next() {
215 None => String::new(),
216 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
217 }
218 })
219 .collect()
220}
221
222fn extract_param_names(params: &str) -> String {
224 params
225 .split(',')
226 .filter_map(|p| {
227 p.trim()
228 .split(':')
229 .next()
230 .map(|name| name.trim().to_string())
231 })
232 .collect::<Vec<_>>()
233 .join(", ")
234}
235
236fn verify_python_syntax(code: &str) -> Result<bool, String> {
238 let temp_file = std::env::temp_dir().join("ggen_verify_syntax.py");
240
241 fs::write(&temp_file, code).map_err(|e| format!("Failed to write temp file: {}", e))?;
242
243 let output = Command::new("python3")
245 .arg("-m")
246 .arg("py_compile")
247 .arg(temp_file.to_str().unwrap())
248 .output()
249 .or_else(|_| {
250 Command::new("python")
252 .arg("-m")
253 .arg("py_compile")
254 .arg(temp_file.to_str().unwrap())
255 .output()
256 })
257 .map_err(|e| format!("Failed to run Python: {}", e))?;
258
259 let _ = fs::remove_file(&temp_file);
261
262 Ok(output.status.success())
263}