1use crate::schema::{AgentSchema, AgentType, ProjectSchema, ToolConfig};
4use anyhow::Result;
5
6pub fn generate_rust_project(project: &ProjectSchema) -> Result<GeneratedProject> {
7 let files = vec![
8 GeneratedFile { path: "src/main.rs".to_string(), content: generate_main_rs(project) },
9 GeneratedFile { path: "Cargo.toml".to_string(), content: generate_cargo_toml(project) },
10 ];
11
12 Ok(GeneratedProject { files })
13}
14
15#[derive(Debug, serde::Serialize)]
16pub struct GeneratedProject {
17 pub files: Vec<GeneratedFile>,
18}
19
20#[derive(Debug, serde::Serialize)]
21pub struct GeneratedFile {
22 pub path: String,
23 pub content: String,
24}
25
26fn generate_main_rs(project: &ProjectSchema) -> String {
27 let mut code = String::new();
28
29 code.push_str("#![allow(unused_imports, unused_variables)]\n\n");
30
31 let uses_mcp = project
33 .agents
34 .values()
35 .any(|a| a.tools.iter().any(|t| t == "mcp" || t.starts_with("mcp_")));
36 let uses_browser = project.agents.values().any(|a| a.tools.contains(&"browser".to_string()));
37
38 code.push_str("use adk_agent::LlmAgentBuilder;\n");
40 code.push_str("use adk_core::ToolContext;\n");
41 code.push_str("use adk_graph::{\n");
42 code.push_str(" edge::{Router, END, START},\n");
43 code.push_str(" graph::StateGraph,\n");
44 code.push_str(" node::{AgentNode, ExecutionConfig, NodeOutput},\n");
45 code.push_str(" state::State,\n");
46 code.push_str(" StreamEvent,\n");
47 code.push_str("};\n");
48 code.push_str("use adk_model::gemini::GeminiModel;\n");
49 code.push_str(
50 "use adk_tool::{FunctionTool, GoogleSearchTool, ExitLoopTool, LoadArtifactsTool};\n",
51 );
52 if uses_mcp || uses_browser {
53 code.push_str("use adk_core::{ReadonlyContext, Toolset, Content};\n");
54 }
55 if uses_mcp {
56 code.push_str("use adk_tool::McpToolset;\n");
57 code.push_str("use rmcp::{ServiceExt, transport::TokioChildProcess};\n");
58 code.push_str("use tokio::process::Command;\n");
59 }
60 if uses_mcp || uses_browser {
61 code.push_str("use async_trait::async_trait;\n");
62 }
63 if uses_browser {
64 code.push_str("use adk_browser::{BrowserSession, BrowserConfig, BrowserToolset};\n");
65 }
66 code.push_str("use anyhow::Result;\n");
67 code.push_str("use serde_json::{json, Value};\n");
68 code.push_str("use std::sync::Arc;\n");
69 code.push_str("use tracing_subscriber::{fmt, EnvFilter};\n\n");
70
71 if uses_mcp || uses_browser {
73 code.push_str("// Minimal context for toolset initialization\n");
74 code.push_str("struct MinimalContext { content: Content }\n");
75 code.push_str("impl MinimalContext { fn new() -> Self { Self { content: Content { role: String::new(), parts: vec![] } } } }\n");
76 code.push_str("#[async_trait]\n");
77 code.push_str("impl ReadonlyContext for MinimalContext {\n");
78 code.push_str(" fn invocation_id(&self) -> &str { \"init\" }\n");
79 code.push_str(" fn agent_name(&self) -> &str { \"init\" }\n");
80 code.push_str(" fn user_id(&self) -> &str { \"init\" }\n");
81 code.push_str(" fn app_name(&self) -> &str { \"init\" }\n");
82 code.push_str(" fn session_id(&self) -> &str { \"init\" }\n");
83 code.push_str(" fn branch(&self) -> &str { \"main\" }\n");
84 code.push_str(" fn user_content(&self) -> &Content { &self.content }\n");
85 code.push_str("}\n\n");
86 }
87
88 for (agent_id, agent) in &project.agents {
90 for tool_type in &agent.tools {
91 if tool_type.starts_with("function") {
92 let tool_id = format!("{}_{}", agent_id, tool_type);
93 if let Some(ToolConfig::Function(config)) = project.tool_configs.get(&tool_id) {
94 code.push_str(&generate_function_schema(config));
95 code.push_str(&generate_function_tool(config));
96 }
97 }
98 }
99 }
100
101 code.push_str("#[tokio::main]\n");
102 code.push_str("async fn main() -> Result<()> {\n");
103 code.push_str(" // Initialize tracing\n");
105 code.push_str(" fmt().with_env_filter(EnvFilter::from_default_env().add_directive(\"adk=info\".parse()?)).json().with_writer(std::io::stderr).init();\n\n");
106 code.push_str(" let api_key = std::env::var(\"GOOGLE_API_KEY\")\n");
107 code.push_str(" .or_else(|_| std::env::var(\"GEMINI_API_KEY\"))\n");
108 code.push_str(" .expect(\"GOOGLE_API_KEY or GEMINI_API_KEY must be set\");\n\n");
109
110 let uses_browser = project.agents.values().any(|a| a.tools.contains(&"browser".to_string()));
112 if uses_browser {
113 code.push_str(" // Initialize browser session\n");
114 code.push_str(" let browser_config = BrowserConfig::new().headless(true);\n");
115 code.push_str(" let browser = Arc::new(BrowserSession::new(browser_config));\n");
116 code.push_str(" browser.start().await?;\n");
117 code.push_str(" let browser_toolset = BrowserToolset::new(browser.clone());\n\n");
118 }
119
120 let all_sub_agents: std::collections::HashSet<_> =
122 project.agents.values().flat_map(|a| a.sub_agents.iter().cloned()).collect();
123 let top_level: Vec<_> =
124 project.agents.keys().filter(|id| !all_sub_agents.contains(*id)).collect();
125
126 let first_agent: Option<&str> =
128 project.workflow.edges.iter().find(|e| e.from == "START").map(|e| e.to.as_str());
129
130 for agent_id in &top_level {
132 if let Some(agent) = project.agents.get(*agent_id) {
133 let is_first = first_agent == Some(agent_id.as_str());
134 match agent.agent_type {
135 AgentType::Router => {
136 code.push_str(&generate_router_node(agent_id, agent));
137 }
138 AgentType::Llm => {
139 code.push_str(&generate_llm_node(agent_id, agent, project, is_first));
140 }
141 _ => {
142 code.push_str(&generate_container_node(agent_id, agent, project));
144 }
145 }
146 }
147 }
148
149 code.push_str(" // Build the graph\n");
151 code.push_str(" let graph = StateGraph::with_channels(&[\"message\", \"classification\", \"response\"])\n");
152
153 for agent_id in &top_level {
155 code.push_str(&format!(" .add_node({}_node)\n", agent_id));
156 }
157
158 for edge in &project.workflow.edges {
160 let from =
161 if edge.from == "START" { "START".to_string() } else { format!("\"{}\"", edge.from) };
162 let to = if edge.to == "END" { "END".to_string() } else { format!("\"{}\"", edge.to) };
163
164 if let Some(agent) = project.agents.get(&edge.from) {
166 if agent.agent_type == AgentType::Router && !agent.routes.is_empty() {
167 let conditions: Vec<String> = agent
169 .routes
170 .iter()
171 .map(|r| {
172 let target = if r.target == "END" {
173 "END".to_string()
174 } else {
175 format!("\"{}\"", r.target)
176 };
177 format!("(\"{}\", {})", r.condition, target)
178 })
179 .collect();
180
181 code.push_str(" .add_conditional_edges(\n");
182 code.push_str(&format!(" \"{}\",\n", edge.from));
183 code.push_str(" Router::by_field(\"classification\"),\n");
184 code.push_str(&format!(" [{}],\n", conditions.join(", ")));
185 code.push_str(" )\n");
186 continue;
187 }
188 }
189
190 code.push_str(&format!(" .add_edge({}, {})\n", from, to));
191 }
192
193 code.push_str(" .compile()?;\n\n");
194
195 code.push_str(" // Get session ID from args or generate new one\n");
197 code.push_str(" let session_id = std::env::args().nth(1).unwrap_or_else(|| uuid::Uuid::new_v4().to_string());\n");
198 code.push_str(" println!(\"SESSION:{}\", session_id);\n\n");
199 code.push_str(" // Conversation history for memory\n");
200 code.push_str(" let mut history: Vec<(String, String)> = Vec::new();\n\n");
201 code.push_str(" // Interactive loop\n");
202 code.push_str(
203 " println!(\"Graph workflow ready. Type your message (or 'quit' to exit):\");\n",
204 );
205 code.push_str(" let stdin = std::io::stdin();\n");
206 code.push_str(" let mut input = String::new();\n");
207 code.push_str(" let mut turn = 0;\n");
208 code.push_str(" loop {\n");
209 code.push_str(" input.clear();\n");
210 code.push_str(" print!(\"> \");\n");
211 code.push_str(" use std::io::Write;\n");
212 code.push_str(" std::io::stdout().flush()?;\n");
213 code.push_str(" stdin.read_line(&mut input)?;\n");
214 code.push_str(" let msg = input.trim();\n");
215 code.push_str(" if msg.is_empty() || msg == \"quit\" { break; }\n\n");
216 code.push_str(" // Build message with conversation history\n");
217 code.push_str(" let context = if history.is_empty() {\n");
218 code.push_str(" msg.to_string()\n");
219 code.push_str(" } else {\n");
220 code.push_str(" let hist: String = history.iter().map(|(u, a)| format!(\"User: {}\\nAssistant: {}\\n\", u, a)).collect();\n");
221 code.push_str(" format!(\"{}\\nUser: {}\", hist, msg)\n");
222 code.push_str(" };\n\n");
223 code.push_str(" let mut state = State::new();\n");
224 code.push_str(" state.insert(\"message\".to_string(), json!(context));\n");
225 code.push_str(" \n");
226 code.push_str(" use adk_graph::StreamMode;\n");
227 code.push_str(" use tokio_stream::StreamExt;\n");
228 code.push_str(" let stream = graph.stream(state, ExecutionConfig::new(&format!(\"{}-turn-{}\", session_id, turn)), StreamMode::Messages);\n");
229 code.push_str(" tokio::pin!(stream);\n");
230 code.push_str(" let mut final_response = String::new();\n");
231 code.push_str(" \n");
232 code.push_str(" while let Some(event) = stream.next().await {\n");
233 code.push_str(" match event {\n");
234 code.push_str(" Ok(e) => {\n");
235 code.push_str(" // Stream Message events as chunks\n");
236 code.push_str(
237 " if let adk_graph::StreamEvent::Message { content, .. } = &e {\n",
238 );
239 code.push_str(" final_response.push_str(content);\n");
240 code.push_str(" println!(\"CHUNK:{}\", serde_json::to_string(&final_response).unwrap_or_default());\n");
241 code.push_str(" }\n");
242 code.push_str(" // Output trace event as JSON\n");
243 code.push_str(" if let Ok(json) = serde_json::to_string(&e) {\n");
244 code.push_str(" println!(\"TRACE:{}\", json);\n");
245 code.push_str(" }\n");
246 code.push_str(" // Capture final response from Done event\n");
247 code.push_str(" if let adk_graph::StreamEvent::Done { state, .. } = &e {\n");
248 code.push_str(" if let Some(resp) = state.get(\"response\").and_then(|v| v.as_str()) {\n");
249 code.push_str(" final_response = resp.to_string();\n");
250 code.push_str(" }\n");
251 code.push_str(" }\n");
252 code.push_str(" }\n");
253 code.push_str(" Err(e) => eprintln!(\"Error: {}\", e),\n");
254 code.push_str(" }\n");
255 code.push_str(" }\n");
256 code.push_str(" turn += 1;\n\n");
257 code.push_str(" // Save to history\n");
258 code.push_str(" if !final_response.is_empty() {\n");
259 code.push_str(" history.push((msg.to_string(), final_response.clone()));\n");
260 code.push_str(" println!(\"RESPONSE:{}\", serde_json::to_string(&final_response).unwrap_or_default());\n");
261 code.push_str(" }\n");
262 code.push_str(" }\n\n");
263
264 code.push_str(" Ok(())\n");
265 code.push_str("}\n");
266
267 code
268}
269
270fn generate_router_node(id: &str, agent: &AgentSchema) -> String {
271 let mut code = String::new();
272 let model = agent.model.as_deref().unwrap_or("gemini-2.0-flash");
273
274 code.push_str(&format!(" // Router: {}\n", id));
275 code.push_str(&format!(" let {}_llm = Arc::new(\n", id));
276 code.push_str(&format!(" LlmAgentBuilder::new(\"{}\")\n", id));
277 code.push_str(&format!(
278 " .model(Arc::new(GeminiModel::new(&api_key, \"{}\")?))\n",
279 model
280 ));
281
282 let route_options: Vec<&str> = agent.routes.iter().map(|r| r.condition.as_str()).collect();
283 let instruction = if agent.instruction.is_empty() {
284 format!(
285 "Classify the input into one of: {}. Respond with ONLY the category name.",
286 route_options.join(", ")
287 )
288 } else {
289 agent.instruction.clone()
290 };
291 let escaped = instruction.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
292 code.push_str(&format!(" .instruction(\"{}\")\n", escaped));
293 code.push_str(" .build()?\n");
294 code.push_str(" );\n\n");
295
296 code.push_str(&format!(" let {}_node = AgentNode::new({}_llm)\n", id, id));
297 code.push_str(" .with_input_mapper(|state| {\n");
298 code.push_str(
299 " let msg = state.get(\"message\").and_then(|v| v.as_str()).unwrap_or(\"\");\n",
300 );
301 code.push_str(" adk_core::Content::new(\"user\").with_text(msg.to_string())\n");
302 code.push_str(" })\n");
303 code.push_str(" .with_output_mapper(|events| {\n");
304 code.push_str(" let mut updates = std::collections::HashMap::new();\n");
305 code.push_str(" for event in events {\n");
306 code.push_str(" if let Some(content) = event.content() {\n");
307 code.push_str(" let text: String = content.parts.iter()\n");
308 code.push_str(" .filter_map(|p| p.text())\n");
309 code.push_str(" .collect::<Vec<_>>().join(\"\").to_lowercase();\n");
310
311 for (i, route) in agent.routes.iter().enumerate() {
312 let cond = if i == 0 { "if" } else { "else if" };
313 code.push_str(&format!(
314 " {} text.contains(\"{}\") {{\n",
315 cond,
316 route.condition.to_lowercase()
317 ));
318 code.push_str(&format!(" updates.insert(\"classification\".to_string(), json!(\"{}\"));\n", route.condition));
319 code.push_str(" }\n");
320 }
321 if let Some(first) = agent.routes.first() {
322 code.push_str(&format!(" else {{ updates.insert(\"classification\".to_string(), json!(\"{}\")); }}\n", first.condition));
323 }
324
325 code.push_str(" }\n");
326 code.push_str(" }\n");
327 code.push_str(" updates\n");
328 code.push_str(" });\n\n");
329
330 code
331}
332
333fn generate_llm_node(
334 id: &str,
335 agent: &AgentSchema,
336 project: &ProjectSchema,
337 is_first: bool,
338) -> String {
339 let mut code = String::new();
340 let model = agent.model.as_deref().unwrap_or("gemini-2.0-flash");
341
342 code.push_str(&format!(" // Agent: {}\n", id));
343
344 let mcp_tools: Vec<_> =
346 agent.tools.iter().filter(|t| *t == "mcp" || t.starts_with("mcp_")).collect();
347
348 for (idx, mcp_tool) in mcp_tools.iter().enumerate() {
349 let tool_id = format!("{}_{}", id, mcp_tool);
350 if let Some(ToolConfig::Mcp(config)) = project.tool_configs.get(&tool_id) {
351 let cmd = &config.server_command;
352 let var_suffix = if idx == 0 { "mcp".to_string() } else { format!("mcp_{}", idx + 1) };
353 code.push_str(&format!(
355 " let mut {}_{}_cmd = Command::new(\"{}\");\n",
356 id, var_suffix, cmd
357 ));
358 for arg in &config.server_args {
359 code.push_str(&format!(" {}_{}_cmd.arg(\"{}\");\n", id, var_suffix, arg));
360 }
361 code.push_str(&format!(
363 " let {}_{}_client = tokio::time::timeout(\n",
364 id, var_suffix
365 ));
366 code.push_str(" std::time::Duration::from_secs(10),\n");
367 code.push_str(&format!(
368 " ().serve(TokioChildProcess::new({}_{}_cmd)?)\n",
369 id, var_suffix
370 ));
371 code.push_str(&format!(" ).await.map_err(|_| anyhow::anyhow!(\"MCP server '{}' failed to start within 10s\"))??;\n", cmd));
372 code.push_str(&format!(
373 " let {}_{}_toolset = McpToolset::new({}_{}_client)",
374 id, var_suffix, id, var_suffix
375 ));
376 if !config.tool_filter.is_empty() {
377 code.push_str(&format!(
378 ".with_tools(&[{}])",
379 config
380 .tool_filter
381 .iter()
382 .map(|t| format!("\"{}\"", t))
383 .collect::<Vec<_>>()
384 .join(", ")
385 ));
386 }
387 code.push_str(";\n");
388 code.push_str(&format!(" let {}_{}_tools = {}_{}_toolset.tools(Arc::new(MinimalContext::new())).await?;\n", id, var_suffix, id, var_suffix));
389 code.push_str(&format!(
390 " eprintln!(\"Loaded {{}} tools from MCP server '{}'\", {}_{}_tools.len());\n\n",
391 cmd, id, var_suffix
392 ));
393 }
394 }
395
396 code.push_str(&format!(" let mut {}_builder = LlmAgentBuilder::new(\"{}\")\n", id, id));
397 code.push_str(&format!(
398 " .model(Arc::new(GeminiModel::new(&api_key, \"{}\")?));\n",
399 model
400 ));
401
402 if !agent.instruction.is_empty() {
403 let escaped =
404 agent.instruction.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
405 code.push_str(&format!(
406 " {}_builder = {}_builder.instruction(\"{}\");\n",
407 id, id, escaped
408 ));
409 }
410
411 for (idx, mcp_tool) in mcp_tools.iter().enumerate() {
413 let tool_id = format!("{}_{}", id, mcp_tool);
414 if project.tool_configs.contains_key(&tool_id) {
415 let var_suffix = if idx == 0 { "mcp".to_string() } else { format!("mcp_{}", idx + 1) };
416 code.push_str(&format!(" for tool in {}_{}_tools {{\n", id, var_suffix));
417 code.push_str(&format!(" {}_builder = {}_builder.tool(tool);\n", id, id));
418 code.push_str(" }\n");
419 }
420 }
421
422 for tool_type in &agent.tools {
423 if tool_type.starts_with("function") {
425 let tool_id = format!("{}_{}", id, tool_type);
426 if let Some(ToolConfig::Function(config)) = project.tool_configs.get(&tool_id) {
427 let struct_name = to_pascal_case(&config.name);
428 code.push_str(&format!(" {}_builder = {}_builder.tool(Arc::new(FunctionTool::new(\"{}\", \"{}\", {}_fn).with_parameters_schema::<{}Args>()));\n",
429 id, id, config.name, config.description.replace('"', "\\\""), config.name, struct_name));
430 }
431 } else if !tool_type.starts_with("mcp") {
432 match tool_type.as_str() {
433 "google_search" => code.push_str(&format!(
434 " {}_builder = {}_builder.tool(Arc::new(GoogleSearchTool::new()));\n",
435 id, id
436 )),
437 "exit_loop" => code.push_str(&format!(
438 " {}_builder = {}_builder.tool(Arc::new(ExitLoopTool::new()));\n",
439 id, id
440 )),
441 "load_artifact" => code.push_str(&format!(
442 " {}_builder = {}_builder.tool(Arc::new(LoadArtifactsTool::new()));\n",
443 id, id
444 )),
445 "browser" => {
446 code.push_str(" for tool in browser_toolset.tools(Arc::new(MinimalContext::new())).await? {\n");
447 code.push_str(&format!(
448 " {}_builder = {}_builder.tool(tool);\n",
449 id, id
450 ));
451 code.push_str(" }\n");
452 }
453 _ => {}
454 }
455 }
456 }
457
458 code.push_str(&format!(" let {}_llm = Arc::new({}_builder.build()?);\n\n", id, id));
459
460 code.push_str(&format!(" let {}_node = AgentNode::new({}_llm)\n", id, id));
461 code.push_str(" .with_input_mapper(|state| {\n");
462
463 if is_first {
465 code.push_str(" let msg = state.get(\"message\").and_then(|v| v.as_str()).unwrap_or(\"\");\n");
466 } else {
467 code.push_str(
468 " // Read previous agent's response, fall back to original message\n",
469 );
470 code.push_str(" let msg = state.get(\"response\").and_then(|v| v.as_str())\n");
471 code.push_str(" .or_else(|| state.get(\"message\").and_then(|v| v.as_str())).unwrap_or(\"\");\n");
472 }
473
474 code.push_str(" adk_core::Content::new(\"user\").with_text(msg.to_string())\n");
475 code.push_str(" })\n");
476 code.push_str(" .with_output_mapper(|events| {\n");
477 code.push_str(" let mut updates = std::collections::HashMap::new();\n");
478 code.push_str(" let mut full_text = String::new();\n");
479 code.push_str(" for event in events {\n");
480 code.push_str(" if let Some(content) = event.content() {\n");
481 code.push_str(" for part in &content.parts {\n");
482 code.push_str(" if let Some(text) = part.text() {\n");
483 code.push_str(" full_text.push_str(text);\n");
484 code.push_str(" }\n");
485 code.push_str(" }\n");
486 code.push_str(" }\n");
487 code.push_str(" }\n");
488 code.push_str(" if !full_text.is_empty() {\n");
489 code.push_str(" updates.insert(\"response\".to_string(), json!(full_text));\n");
490 code.push_str(" }\n");
491 code.push_str(" updates\n");
492 code.push_str(" });\n\n");
493
494 code
495}
496
497fn generate_container_node(id: &str, agent: &AgentSchema, project: &ProjectSchema) -> String {
498 let mut code = String::new();
499
500 for sub_id in &agent.sub_agents {
502 if let Some(sub) = project.agents.get(sub_id) {
503 let model = sub.model.as_deref().unwrap_or("gemini-2.0-flash");
504 let has_tools = !sub.tools.is_empty();
505 let has_instruction = !sub.instruction.is_empty();
506 let mut_kw = if has_tools || has_instruction { "mut " } else { "" };
507
508 for tool_type in &sub.tools {
510 let tool_id = format!("{}_{}", sub_id, tool_type);
511 if tool_type.starts_with("mcp") {
512 if let Some(ToolConfig::Mcp(config)) = project.tool_configs.get(&tool_id) {
513 let var_suffix = tool_type.replace("mcp_", "mcp");
514 code.push_str(&format!(
515 " let mut {}_{}_cmd = Command::new(\"{}\");\n",
516 sub_id, var_suffix, config.server_command
517 ));
518 for arg in &config.server_args {
519 code.push_str(&format!(
520 " {}_{}_cmd.arg(\"{}\");\n",
521 sub_id, var_suffix, arg
522 ));
523 }
524 code.push_str(&format!(
525 " let {}_{}_client = tokio::time::timeout(\n",
526 sub_id, var_suffix
527 ));
528 code.push_str(" std::time::Duration::from_secs(10),\n");
529 code.push_str(&format!(
530 " ().serve(TokioChildProcess::new({}_{}_cmd)?)\n",
531 sub_id, var_suffix
532 ));
533 code.push_str(&format!(" ).await.map_err(|_| anyhow::anyhow!(\"MCP server '{}' failed to start within 10s\"))??;\n", config.server_command));
534 code.push_str(&format!(
535 " let {}_{}_toolset = McpToolset::new({}_{}_client);\n",
536 sub_id, var_suffix, sub_id, var_suffix
537 ));
538 code.push_str(&format!(" let {}_{}_tools = {}_{}_toolset.tools(Arc::new(MinimalContext::new())).await?;\n", sub_id, var_suffix, sub_id, var_suffix));
539 code.push_str(&format!(" eprintln!(\"Loaded {{}} tools from MCP server '{}'\", {}_{}_tools.len());\n\n", config.server_command, sub_id, var_suffix));
540 }
541 }
542 }
543
544 code.push_str(&format!(
546 " let {}{}_builder = LlmAgentBuilder::new(\"{}\")\n",
547 mut_kw, sub_id, sub_id
548 ));
549 code.push_str(&format!(
550 " .model(Arc::new(GeminiModel::new(&api_key, \"{}\")?))",
551 model
552 ));
553 code.push_str(";\n");
554
555 if !sub.instruction.is_empty() {
557 let escaped =
558 sub.instruction.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
559 code.push_str(&format!(
560 " {}_builder = {}_builder.instruction(\"{}\");\n",
561 sub_id, sub_id, escaped
562 ));
563 }
564
565 for tool_type in &sub.tools {
567 let tool_id = format!("{}_{}", sub_id, tool_type);
568 if tool_type.starts_with("function") {
569 if let Some(ToolConfig::Function(config)) = project.tool_configs.get(&tool_id) {
570 let fn_name = &config.name;
571 let struct_name = to_pascal_case(fn_name);
572 code.push_str(&format!(" {}_builder = {}_builder.tool(Arc::new(FunctionTool::new(\"{}\", \"{}\", {}_fn).with_parameters_schema::<{}Args>()));\n",
573 sub_id, sub_id, fn_name, config.description.replace('"', "\\\""), fn_name, struct_name));
574 }
575 } else if tool_type.starts_with("mcp") {
576 let tool_id = format!("{}_{}", sub_id, tool_type);
578 if project.tool_configs.contains_key(&tool_id) {
579 let var_suffix = tool_type.replace("mcp_", "mcp");
580 code.push_str(&format!(
581 " for tool in {}_{}_tools {{\n",
582 sub_id, var_suffix
583 ));
584 code.push_str(&format!(
585 " {}_builder = {}_builder.tool(tool);\n",
586 sub_id, sub_id
587 ));
588 code.push_str(" }\n");
589 }
590 } else if tool_type == "google_search" {
591 code.push_str(&format!(
592 " {}_builder = {}_builder.tool(Arc::new(GoogleSearchTool::new()));\n",
593 sub_id, sub_id
594 ));
595 } else if tool_type == "exit_loop" {
596 code.push_str(&format!(
597 " {}_builder = {}_builder.tool(Arc::new(ExitLoopTool::new()));\n",
598 sub_id, sub_id
599 ));
600 } else if tool_type == "load_artifact" {
601 code.push_str(&format!(
602 " {}_builder = {}_builder.tool(Arc::new(LoadArtifactsTool::new()));\n",
603 sub_id, sub_id
604 ));
605 }
606 }
607
608 code.push_str(&format!(" let {}_agent = {}_builder.build()?;\n\n", sub_id, sub_id));
609 }
610 }
611
612 let subs: Vec<_> = agent.sub_agents.iter().map(|s| format!("Arc::new({}_agent)", s)).collect();
614 let container_type = match agent.agent_type {
615 AgentType::Sequential => "adk_agent::SequentialAgent",
616 AgentType::Loop => "adk_agent::LoopAgent",
617 AgentType::Parallel => "adk_agent::ParallelAgent",
618 _ => "adk_agent::SequentialAgent",
619 };
620
621 code.push_str(&format!(" // Container: {} ({:?})\n", id, agent.agent_type));
622 if agent.agent_type == AgentType::Loop {
623 let max_iter = agent.max_iterations.unwrap_or(3);
624 code.push_str(&format!(
625 " let {}_container = {}::new(\"{}\", vec![{}]).with_max_iterations({});\n\n",
626 id,
627 container_type,
628 id,
629 subs.join(", "),
630 max_iter
631 ));
632 } else {
633 code.push_str(&format!(
634 " let {}_container = {}::new(\"{}\", vec![{}]);\n\n",
635 id,
636 container_type,
637 id,
638 subs.join(", ")
639 ));
640 }
641
642 code.push_str(&format!(" let {}_node = AgentNode::new(Arc::new({}_container))\n", id, id));
644 code.push_str(" .with_input_mapper(|state| {\n");
645 code.push_str(
646 " let msg = state.get(\"message\").and_then(|v| v.as_str()).unwrap_or(\"\");\n",
647 );
648 code.push_str(" adk_core::Content::new(\"user\").with_text(msg.to_string())\n");
649 code.push_str(" })\n");
650 code.push_str(" .with_output_mapper(|events| {\n");
651 code.push_str(" let mut updates = std::collections::HashMap::new();\n");
652 code.push_str(" let mut full_text = String::new();\n");
653 code.push_str(" for event in events {\n");
654 code.push_str(" if let Some(content) = event.content() {\n");
655 code.push_str(" for part in &content.parts {\n");
656 code.push_str(" if let Some(text) = part.text() {\n");
657 code.push_str(" full_text.push_str(text);\n");
658 code.push_str(" }\n");
659 code.push_str(" }\n");
660 code.push_str(" }\n");
661 code.push_str(" }\n");
662 code.push_str(" // Filter out tool call artifacts\n");
663 code.push_str(" let full_text = full_text.replace(\"exit_loop\", \"\");\n");
664 code.push_str(" if !full_text.is_empty() {\n");
665 code.push_str(" updates.insert(\"response\".to_string(), json!(full_text));\n");
666 code.push_str(" }\n");
667 code.push_str(" updates\n");
668 code.push_str(" });\n\n");
669
670 code
671}
672
673fn generate_function_tool(config: &crate::schema::FunctionToolConfig) -> String {
674 let mut code = String::new();
675 let fn_name = &config.name;
676
677 code.push_str(&format!("async fn {}_fn(_ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value, adk_core::AdkError> {{\n", fn_name));
678
679 for param in &config.parameters {
681 let extract = match param.param_type {
682 crate::schema::ParamType::String => format!(
683 " let {} = args[\"{}\"].as_str().unwrap_or(\"\");\n",
684 param.name, param.name
685 ),
686 crate::schema::ParamType::Number => format!(
687 " let {} = args[\"{}\"].as_f64().unwrap_or(0.0);\n",
688 param.name, param.name
689 ),
690 crate::schema::ParamType::Boolean => format!(
691 " let {} = args[\"{}\"].as_bool().unwrap_or(false);\n",
692 param.name, param.name
693 ),
694 };
695 code.push_str(&extract);
696 }
697
698 code.push('\n');
699
700 if config.code.is_empty() {
702 let param_json = config
704 .parameters
705 .iter()
706 .map(|p| format!(" \"{}\": {}", p.name, p.name))
707 .collect::<Vec<_>>()
708 .join(",\n");
709 code.push_str(" // TODO: Add function implementation\n");
710 code.push_str(" Ok(json!({\n");
711 code.push_str(&format!(" \"function\": \"{}\",\n", fn_name));
712 if !param_json.is_empty() {
713 code.push_str(¶m_json);
714 code.push_str(",\n");
715 }
716 code.push_str(" \"status\": \"not_implemented\"\n");
717 code.push_str(" }))\n");
718 } else {
719 code.push_str(" // User-defined implementation\n");
721 for line in config.code.lines() {
722 code.push_str(&format!(" {}\n", line));
723 }
724 }
725
726 code.push_str("}\n\n");
727 code
728}
729
730fn generate_function_schema(config: &crate::schema::FunctionToolConfig) -> String {
731 let mut code = String::new();
732 let struct_name = to_pascal_case(&config.name);
733
734 code.push_str("#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)]\n");
735 code.push_str(&format!("struct {}Args {{\n", struct_name));
736
737 for param in &config.parameters {
738 if !param.description.is_empty() {
739 code.push_str(&format!(" /// {}\n", param.description));
740 }
741 let rust_type = match param.param_type {
742 crate::schema::ParamType::String => "String",
743 crate::schema::ParamType::Number => "f64",
744 crate::schema::ParamType::Boolean => "bool",
745 };
746 code.push_str(&format!(" {}: {},\n", param.name, rust_type));
747 }
748
749 code.push_str("}\n\n");
750 code
751}
752
753fn to_pascal_case(s: &str) -> String {
754 s.split('_')
755 .map(|word| {
756 let mut chars = word.chars();
757 match chars.next() {
758 None => String::new(),
759 Some(c) => c.to_uppercase().chain(chars).collect(),
760 }
761 })
762 .collect()
763}
764
765fn generate_cargo_toml(project: &ProjectSchema) -> String {
766 let mut name = project
767 .name
768 .to_lowercase()
769 .replace(' ', "_")
770 .replace(|c: char| !c.is_alphanumeric() && c != '_', "");
771 if name.is_empty() || name.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) {
773 name = format!("project_{}", name);
774 }
775
776 let code_uses = |pattern: &str| -> bool {
778 project.tool_configs.values().any(|tc| {
779 if let ToolConfig::Function(fc) = tc { fc.code.contains(pattern) } else { false }
780 })
781 };
782
783 let needs_reqwest = code_uses("reqwest::");
784 let needs_lettre = code_uses("lettre::");
785 let needs_base64 = code_uses("base64::");
786
787 let use_path_deps = std::env::var("ADK_DEV_MODE").is_ok();
789
790 let adk_root = "/data/projects/production/adk/adk-rust";
791
792 let adk_deps = if use_path_deps {
793 format!(
794 r#"adk-agent = {{ path = "{}/adk-agent" }}
795adk-core = {{ path = "{}/adk-core" }}
796adk-model = {{ path = "{}/adk-model" }}
797adk-tool = {{ path = "{}/adk-tool" }}
798adk-graph = {{ path = "{}/adk-graph" }}"#,
799 adk_root, adk_root, adk_root, adk_root, adk_root
800 )
801 } else {
802 r#"adk-agent = "0.2.0"
803adk-core = "0.2.0"
804adk-model = "0.2.0"
805adk-tool = "0.2.0"
806adk-graph = "0.2.0""#
807 .to_string()
808 };
809
810 let patch_section = String::new();
812
813 let mut deps = format!(
814 r#"[package]
815name = "{}"
816version = "0.1.0"
817edition = "2021"
818
819[dependencies]
820{}
821tokio = {{ version = "1", features = ["full", "macros"] }}
822tokio-stream = "0.1"
823anyhow = "1"
824serde = {{ version = "1", features = ["derive"] }}
825serde_json = "1"
826schemars = "0.8"
827tracing-subscriber = {{ version = "0.3", features = ["json", "env-filter"] }}
828uuid = {{ version = "1", features = ["v4"] }}
829"#,
830 name, adk_deps
831 );
832
833 if needs_reqwest {
834 deps.push_str("reqwest = { version = \"0.12\", features = [\"json\"] }\n");
835 }
836 if needs_lettre {
837 deps.push_str("lettre = \"0.11\"\n");
838 }
839 if needs_base64 {
840 deps.push_str("base64 = \"0.21\"\n");
841 }
842
843 let uses_mcp = project
845 .agents
846 .values()
847 .any(|a| a.tools.iter().any(|t| t == "mcp" || t.starts_with("mcp_")));
848 if uses_mcp {
849 deps.push_str(
850 "rmcp = { version = \"0.9\", features = [\"client\", \"transport-child-process\"] }\n",
851 );
852 deps.push_str("async-trait = \"0.1\"\n");
853 }
854
855 let uses_browser = project.agents.values().any(|a| a.tools.contains(&"browser".to_string()));
857 if uses_browser {
858 if use_path_deps {
859 deps.push_str(&format!("adk-browser = {{ path = \"{}/adk-browser\" }}\n", adk_root));
860 } else {
861 deps.push_str("adk-browser = \"0.1\"\n");
862 }
863 if !uses_mcp {
865 deps.push_str("async-trait = \"0.1\"\n");
866 }
867 }
868
869 deps.push_str(&patch_section);
871
872 deps
873}