1pub mod action_node_codegen;
23pub mod action_node_types;
24mod validation;
25
26pub use action_node_codegen as action_nodes;
28
29pub use validation::{
30 EnvVarRequirement, EnvVarWarning, ValidationError, ValidationErrorCode, ValidationResult,
31 check_env_vars, get_required_env_vars, validate_project,
32};
33
34use crate::schema::{AgentSchema, AgentType, ProjectSchema, ToolConfig};
35use anyhow::{Result, bail};
36
37const DEFAULT_ADK_VERSION: &str = "0.6.0";
38
39fn detect_provider(model: &str) -> &'static str {
42 let m = model.to_lowercase();
43 if m.contains("gemini") || m.contains("gemma") {
44 "gemini"
45 } else if m.contains("gpt")
46 || m.contains("o1")
47 || m.contains("o3")
48 || m.contains("o4")
49 || m.contains("codex")
50 {
51 "openai"
52 } else if m.contains("claude")
53 && !m.starts_with("us.")
54 && !m.starts_with("eu.")
55 && !m.starts_with("ap.")
56 {
57 "anthropic"
58 } else if m.starts_with("us.") || m.starts_with("eu.") || m.starts_with("ap.") {
59 "bedrock"
61 } else if m.contains("deepseek") && !m.contains(':') {
62 "deepseek"
64 } else if m.contains("sonar") {
65 "perplexity"
66 } else if (m.contains("mistral-large")
67 || m.contains("mistral-small")
68 || m.contains("codestral"))
69 && !m.contains(':')
70 {
71 "mistral"
72 } else if m.contains("accounts/fireworks/") {
73 "fireworks"
74 } else if m.contains("-turbo") && m.contains('/') {
75 "together"
76 } else if m.contains("cohere-command") || (m.contains("mistral") && m.contains("2024")) {
77 "azure-ai"
78 } else if m.contains("llama") || m.contains("mixtral") {
79 if m.contains(':') {
81 "ollama"
82 } else if model.starts_with("Meta-Llama") {
83 "sambanova"
84 } else if m.starts_with("llama-")
85 && m.chars()
86 .nth(6)
87 .map(|c| c.is_ascii_digit())
88 .unwrap_or(false)
89 {
90 "cerebras"
91 } else {
92 "groq"
93 }
94 } else if m.contains("qwen")
95 || m.contains("mistral")
96 || m.contains("codellama")
97 || m.contains("devstral")
98 {
99 "ollama"
100 } else {
101 "gemini" }
103}
104
105fn collect_providers(project: &ProjectSchema) -> std::collections::HashSet<&'static str> {
107 let mut providers = std::collections::HashSet::new();
108 for agent in project.agents.values() {
109 let model = agent.model.as_deref().unwrap_or("gemini-3.1-flash-lite-preview");
110 providers.insert(detect_provider(model));
111 }
112 if let Some(ref dp) = project.settings.default_provider {
114 let p = match dp.as_str() {
115 "gemini" | "openai" | "anthropic" | "deepseek" | "groq" | "ollama" | "fireworks"
116 | "together" | "mistral" | "perplexity" | "cerebras" | "sambanova" | "bedrock"
117 | "azure-ai" => dp.as_str(),
118 _ => "gemini",
119 };
120 providers.insert(match p {
121 "openai" => "openai",
122 "anthropic" => "anthropic",
123 "deepseek" => "deepseek",
124 "groq" => "groq",
125 "ollama" => "ollama",
126 "fireworks" => "fireworks",
127 "together" => "together",
128 "mistral" => "mistral",
129 "perplexity" => "perplexity",
130 "cerebras" => "cerebras",
131 "sambanova" => "sambanova",
132 "bedrock" => "bedrock",
133 "azure-ai" => "azure-ai",
134 _ => "gemini",
135 });
136 }
137 providers
138}
139
140pub fn generate_rust_project(project: &ProjectSchema) -> Result<GeneratedProject> {
160 let validation = validate_project(project);
162 if !validation.is_valid() {
163 let error_messages: Vec<String> = validation.errors.iter().map(|e| e.to_string()).collect();
164 bail!("Workflow validation failed:\n{}", error_messages.join("\n"));
165 }
166
167 let files = vec![
168 GeneratedFile {
169 path: "src/main.rs".to_string(),
170 content: generate_main_rs(project),
171 },
172 GeneratedFile {
173 path: "Cargo.toml".to_string(),
174 content: generate_cargo_toml(project),
175 },
176 ];
177
178 Ok(GeneratedProject { files })
179}
180
181pub fn generate_rust_project_with_validation(
186 project: &ProjectSchema,
187) -> Result<(GeneratedProject, ValidationResult, Vec<EnvVarWarning>)> {
188 let validation = validate_project(project);
189 if !validation.is_valid() {
190 let error_messages: Vec<String> = validation.errors.iter().map(|e| e.to_string()).collect();
191 bail!("Workflow validation failed:\n{}", error_messages.join("\n"));
192 }
193
194 let env_warnings = check_env_vars(project);
196
197 let files = vec![
198 GeneratedFile {
199 path: "src/main.rs".to_string(),
200 content: generate_main_rs(project),
201 },
202 GeneratedFile {
203 path: "Cargo.toml".to_string(),
204 content: generate_cargo_toml(project),
205 },
206 ];
207
208 Ok((GeneratedProject { files }, validation, env_warnings))
209}
210
211fn generate_workflow_header_comment(project: &ProjectSchema) -> String {
222 let mut comment = String::new();
223
224 comment.push_str("//! ");
226 comment.push_str(&project.name);
227 comment.push_str("\n//!\n");
228
229 if !project.description.is_empty() {
230 comment.push_str("//! ");
231 comment.push_str(&project.description);
232 comment.push_str("\n//!\n");
233 }
234
235 comment.push_str("//! ## Workflow Structure\n//!\n");
237
238 let workflow_type = determine_workflow_type(project);
240 comment.push_str("//! **Type:** ");
241 comment.push_str(&workflow_type);
242 comment.push_str("\n//!\n");
243
244 comment.push_str("//! ## Agents\n//!\n");
246
247 let all_sub_agents: std::collections::HashSet<_> = project
249 .agents
250 .values()
251 .flat_map(|a| a.sub_agents.iter().cloned())
252 .collect();
253
254 for (agent_id, agent) in &project.agents {
255 let is_sub_agent = all_sub_agents.contains(agent_id);
256 let prefix = if is_sub_agent { " - " } else { "- " };
257
258 comment.push_str("//! ");
259 comment.push_str(prefix);
260 comment.push_str("**");
261 comment.push_str(agent_id);
262 comment.push_str("** (");
263 comment.push_str(&format!("{:?}", agent.agent_type).to_lowercase());
264 comment.push(')');
265
266 if !agent.instruction.is_empty() {
268 let brief = truncate_instruction(&agent.instruction, 60);
269 comment.push_str(": ");
270 comment.push_str(&brief);
271 }
272 comment.push('\n');
273
274 if !agent.tools.is_empty() {
276 comment.push_str("//! Tools: ");
277 comment.push_str(&agent.tools.join(", "));
278 comment.push('\n');
279 }
280
281 if !agent.sub_agents.is_empty() {
283 comment.push_str("//! Sub-agents: ");
284 comment.push_str(&agent.sub_agents.join(" → "));
285 comment.push('\n');
286 }
287 }
288
289 comment.push_str("//!\n//! ## Execution Flow\n//!\n");
291 comment.push_str("//! ```text\n");
292 comment.push_str(&generate_flow_diagram(project));
293 comment.push_str("//! ```\n//!\n");
294
295 comment.push_str("//! Generated by ADK Studio v2.0\n//!\n");
297
298 let env_vars = get_required_env_vars(project);
300 if !env_vars.is_empty() {
301 comment.push_str("//! ## Required Environment Variables\n//!\n");
302 for env_var in &env_vars {
303 if env_var.required {
304 comment.push_str("//! - **");
305 } else {
306 comment.push_str("//! - ");
307 }
308 comment.push_str(&env_var.name);
309 if env_var.required {
310 comment.push_str("** (required)");
311 } else {
312 comment.push_str(" (optional)");
313 }
314 comment.push_str(": ");
315 comment.push_str(&env_var.description);
316 if !env_var.alternatives.is_empty() {
317 comment.push_str(" [alt: ");
318 comment.push_str(&env_var.alternatives.join(", "));
319 comment.push(']');
320 }
321 comment.push('\n');
322 }
323 comment.push_str("//!\n");
324 }
325
326 comment
327}
328
329fn determine_workflow_type(project: &ProjectSchema) -> String {
331 let has_router = project
332 .agents
333 .values()
334 .any(|a| a.agent_type == AgentType::Router);
335 let has_loop = project
336 .agents
337 .values()
338 .any(|a| a.agent_type == AgentType::Loop);
339 let has_parallel = project
340 .agents
341 .values()
342 .any(|a| a.agent_type == AgentType::Parallel);
343 let has_sequential = project
344 .agents
345 .values()
346 .any(|a| a.agent_type == AgentType::Sequential);
347
348 let all_sub_agents: std::collections::HashSet<_> = project
350 .agents
351 .values()
352 .flat_map(|a| a.sub_agents.iter().cloned())
353 .collect();
354 let top_level_count = project
355 .agents
356 .keys()
357 .filter(|id| !all_sub_agents.contains(*id))
358 .count();
359
360 if has_router {
361 "Router-based workflow with conditional branching".to_string()
362 } else if has_loop {
363 "Iterative loop workflow with refinement".to_string()
364 } else if has_parallel {
365 "Parallel execution workflow".to_string()
366 } else if has_sequential || top_level_count > 1 {
367 "Sequential pipeline workflow".to_string()
368 } else {
369 "Single agent workflow".to_string()
370 }
371}
372
373fn truncate_instruction(instruction: &str, max_len: usize) -> String {
375 let clean = instruction.replace('\n', " ").trim().to_string();
376 if clean.len() <= max_len {
377 clean
378 } else {
379 format!("{}...", &clean[..max_len.saturating_sub(3)])
380 }
381}
382
383fn strip_template_variables(instruction: &str) -> String {
390 let mut result = instruction.to_string();
391 while let Some(start) = result.find("{{") {
393 if let Some(end) = result[start..].find("}}") {
394 let end_pos = start + end + 2;
395 result = format!("{}{}", &result[..start], &result[end_pos..]);
396 } else {
397 break;
398 }
399 }
400 while result.contains(" ") {
402 result = result.replace(" ", " ");
403 }
404 result.trim().to_string()
405}
406
407fn generate_flow_diagram(project: &ProjectSchema) -> String {
409 let mut diagram = String::new();
410
411 let mut current = "START";
413 let mut visited = std::collections::HashSet::new();
414
415 diagram.push_str("//! START");
416
417 while current != "END" && !visited.contains(current) {
418 visited.insert(current);
419
420 let next_edges: Vec<_> = project
422 .workflow
423 .edges
424 .iter()
425 .filter(|e| e.from == current)
426 .collect();
427
428 if next_edges.is_empty() {
429 break;
430 }
431
432 if next_edges.len() == 1 {
433 let next = &next_edges[0].to;
434 if project.action_nodes.contains_key(next) {
436 current = next;
437 continue;
438 }
439 diagram.push_str(" → ");
440 diagram.push_str(next);
441 current = next;
442 } else {
443 diagram.push_str(" → [");
445 let targets: Vec<_> = next_edges
446 .iter()
447 .map(|e| e.to.as_str())
448 .filter(|t| !project.action_nodes.contains_key(*t))
449 .collect();
450 diagram.push_str(&targets.join(" | "));
451 diagram.push(']');
452 break; }
454 }
455
456 diagram.push('\n');
457 diagram
458}
459
460#[derive(Debug, serde::Serialize)]
461pub struct GeneratedProject {
462 pub files: Vec<GeneratedFile>,
463}
464
465#[derive(Debug, serde::Serialize)]
466pub struct GeneratedFile {
467 pub path: String,
468 pub content: String,
469}
470
471fn generate_main_rs(project: &ProjectSchema) -> String {
472 let mut code = String::new();
473
474 code.push_str(&generate_workflow_header_comment(project));
476
477 code.push_str("#![allow(unused_imports, unused_variables)]\n\n");
478
479 let uses_mcp = project
481 .agents
482 .values()
483 .any(|a| a.tools.iter().any(|t| t == "mcp" || t.starts_with("mcp_")));
484 let uses_browser = project
485 .agents
486 .values()
487 .any(|a| a.tools.contains(&"browser".to_string()));
488
489 code.push_str("use adk_agent::LlmAgentBuilder;\n");
491 code.push_str("use adk_core::ToolContext;\n");
492 code.push_str("use adk_graph::{\n");
493 code.push_str(" edge::{Router, END, START},\n");
494 code.push_str(" graph::StateGraph,\n");
495 code.push_str(" node::{AgentNode, ExecutionConfig, NodeOutput},\n");
496 code.push_str(" state::State,\n");
497 code.push_str(" StreamEvent,\n");
498 code.push_str("};\n");
499 let providers = collect_providers(project);
501 if providers.contains("gemini") {
502 code.push_str("use adk_model::gemini::GeminiModel;\n");
503 }
504 if providers.contains("openai") {
505 code.push_str("use adk_model::openai::{OpenAIClient, OpenAIConfig};\n");
506 }
507 if providers.contains("anthropic") {
508 code.push_str("use adk_model::anthropic::{AnthropicClient, AnthropicConfig};\n");
509 }
510 if providers.contains("deepseek") {
511 code.push_str("use adk_model::deepseek::{DeepSeekClient, DeepSeekConfig};\n");
512 }
513 if providers.contains("groq") {
514 code.push_str("use adk_model::groq::{GroqClient, GroqConfig};\n");
515 }
516 if providers.contains("ollama") {
517 code.push_str("use adk_model::ollama::{OllamaModel, OllamaConfig};\n");
518 }
519 if providers.contains("fireworks") {
520 code.push_str("use adk_model::fireworks::{FireworksClient, FireworksConfig};\n");
521 }
522 if providers.contains("together") {
523 code.push_str("use adk_model::together::{TogetherClient, TogetherConfig};\n");
524 }
525 if providers.contains("mistral") {
526 code.push_str("use adk_model::mistral::{MistralClient, MistralConfig};\n");
527 }
528 if providers.contains("perplexity") {
529 code.push_str("use adk_model::perplexity::{PerplexityClient, PerplexityConfig};\n");
530 }
531 if providers.contains("cerebras") {
532 code.push_str("use adk_model::cerebras::{CerebrasClient, CerebrasConfig};\n");
533 }
534 if providers.contains("sambanova") {
535 code.push_str("use adk_model::sambanova::{SambaNovaClient, SambaNovaConfig};\n");
536 }
537 if providers.contains("bedrock") {
538 code.push_str("use adk_model::bedrock::{BedrockClient, BedrockConfig};\n");
539 }
540 if providers.contains("azure-ai") {
541 code.push_str("use adk_model::azure_ai::{AzureAIClient, AzureAIConfig};\n");
542 }
543 code.push_str(
544 "use adk_tool::{FunctionTool, GoogleSearchTool, ExitLoopTool, LoadArtifactsTool};\n",
545 );
546 if uses_mcp || uses_browser {
547 code.push_str("use adk_core::{ReadonlyContext, Toolset, Content};\n");
548 }
549 if uses_mcp {
550 code.push_str("use adk_tool::McpToolset;\n");
551 code.push_str("use rmcp::{ServiceExt, transport::TokioChildProcess};\n");
552 code.push_str("use tokio::process::Command;\n");
553 }
554 if uses_mcp || uses_browser {
555 code.push_str("use async_trait::async_trait;\n");
556 }
557 if uses_browser {
558 code.push_str("use adk_browser::{BrowserSession, BrowserConfig, BrowserToolset};\n");
559 }
560 code.push_str("use anyhow::Result;\n");
561 code.push_str("use serde_json::{json, Value};\n");
562 code.push_str("use std::sync::Arc;\n");
563 code.push_str("use tracing_subscriber::{fmt, EnvFilter};\n\n");
564
565 if uses_mcp || uses_browser {
567 code.push_str("// Minimal context for toolset initialization\n");
568 code.push_str("struct MinimalContext { content: Content }\n");
569 code.push_str("impl MinimalContext { fn new() -> Self { Self { content: Content { role: String::new(), parts: vec![] } } } }\n");
570 code.push_str("#[async_trait]\n");
571 code.push_str("impl ReadonlyContext for MinimalContext {\n");
572 code.push_str(" fn invocation_id(&self) -> &str { \"init\" }\n");
573 code.push_str(" fn agent_name(&self) -> &str { \"init\" }\n");
574 code.push_str(" fn user_id(&self) -> &str { \"init\" }\n");
575 code.push_str(" fn app_name(&self) -> &str { \"init\" }\n");
576 code.push_str(" fn session_id(&self) -> &str { \"init\" }\n");
577 code.push_str(" fn branch(&self) -> &str { \"main\" }\n");
578 code.push_str(" fn user_content(&self) -> &Content { &self.content }\n");
579 code.push_str("}\n\n");
580 }
581
582 for (agent_id, agent) in &project.agents {
584 for tool_type in &agent.tools {
585 if tool_type.starts_with("function") {
586 let tool_id = format!("{}_{}", agent_id, tool_type);
587 if let Some(ToolConfig::Function(config)) = project.tool_configs.get(&tool_id) {
588 code.push_str(&generate_function_schema(config));
589 code.push_str(&generate_function_tool(config));
590 }
591 }
592 }
593 }
594
595 {
597 use crate::codegen::action_node_types::CodeLanguage;
598 use crate::codegen::action_nodes::ActionNodeConfig;
599 let has_js_code_nodes = project.action_nodes.values().any(|n| {
600 matches!(
601 n,
602 ActionNodeConfig::Code(cfg) if matches!(cfg.language, CodeLanguage::Javascript | CodeLanguage::Typescript)
603 )
604 });
605 if has_js_code_nodes {
606 code.push_str("/// Convert a boa_engine JsValue to serde_json::Value\n");
607 code.push_str("fn js_value_to_json(val: &boa_engine::JsValue, ctx: &mut boa_engine::Context) -> serde_json::Value {\n");
608 code.push_str(
609 " // Handle Undefined/Null before to_json (which panics on Undefined)\n",
610 );
611 code.push_str(" if val.is_undefined() || val.is_null() {\n");
612 code.push_str(" return serde_json::Value::Null;\n");
613 code.push_str(" }\n");
614 code.push_str(" match val.to_json(ctx) {\n");
615 code.push_str(" Ok(json) => json,\n");
616 code.push_str(" Err(_) => {\n");
617 code.push_str(" match val {\n");
618 code.push_str(" boa_engine::JsValue::Boolean(b) => json!(*b),\n");
619 code.push_str(" boa_engine::JsValue::Integer(n) => json!(*n),\n");
620 code.push_str(" boa_engine::JsValue::Rational(n) => if n.is_finite() { json!(*n) } else { serde_json::Value::Null },\n");
621 code.push_str(" boa_engine::JsValue::String(s) => json!(s.to_std_string_escaped()),\n");
622 code.push_str(" _ => json!(val.display().to_string()),\n");
623 code.push_str(" }\n");
624 code.push_str(" }\n");
625 code.push_str(" }\n");
626 code.push_str("}\n\n");
627 }
628 }
629
630 code.push_str("#[tokio::main]\n");
631 code.push_str("async fn main() -> Result<()> {\n");
632 code.push_str(" // Initialize tracing\n");
634 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");
635 if providers.contains("gemini") {
637 code.push_str(" let gemini_api_key = std::env::var(\"GOOGLE_API_KEY\")\n");
638 code.push_str(" .or_else(|_| std::env::var(\"GEMINI_API_KEY\"))\n");
639 code.push_str(" .expect(\"GOOGLE_API_KEY or GEMINI_API_KEY must be set\");\n\n");
640 }
641 if providers.contains("openai") {
642 code.push_str(" let openai_api_key = std::env::var(\"OPENAI_API_KEY\")\n");
643 code.push_str(" .expect(\"OPENAI_API_KEY must be set\");\n\n");
644 }
645 if providers.contains("anthropic") {
646 code.push_str(" let anthropic_api_key = std::env::var(\"ANTHROPIC_API_KEY\")\n");
647 code.push_str(" .expect(\"ANTHROPIC_API_KEY must be set\");\n\n");
648 }
649 if providers.contains("deepseek") {
650 code.push_str(" let deepseek_api_key = std::env::var(\"DEEPSEEK_API_KEY\")\n");
651 code.push_str(" .expect(\"DEEPSEEK_API_KEY must be set\");\n\n");
652 }
653 if providers.contains("groq") {
654 code.push_str(" let groq_api_key = std::env::var(\"GROQ_API_KEY\")\n");
655 code.push_str(" .expect(\"GROQ_API_KEY must be set\");\n\n");
656 }
657 if providers.contains("ollama") {
658 code.push_str(" let _ollama_host = std::env::var(\"OLLAMA_HOST\")\n");
659 code.push_str(" .unwrap_or_else(|_| \"http://localhost:11434\".to_string());\n\n");
660 }
661 if providers.contains("fireworks") {
662 code.push_str(" let fireworks_api_key = std::env::var(\"FIREWORKS_API_KEY\")\n");
663 code.push_str(" .expect(\"FIREWORKS_API_KEY must be set\");\n\n");
664 }
665 if providers.contains("together") {
666 code.push_str(" let together_api_key = std::env::var(\"TOGETHER_API_KEY\")\n");
667 code.push_str(" .expect(\"TOGETHER_API_KEY must be set\");\n\n");
668 }
669 if providers.contains("mistral") {
670 code.push_str(" let mistral_api_key = std::env::var(\"MISTRAL_API_KEY\")\n");
671 code.push_str(" .expect(\"MISTRAL_API_KEY must be set\");\n\n");
672 }
673 if providers.contains("perplexity") {
674 code.push_str(" let perplexity_api_key = std::env::var(\"PERPLEXITY_API_KEY\")\n");
675 code.push_str(" .expect(\"PERPLEXITY_API_KEY must be set\");\n\n");
676 }
677 if providers.contains("cerebras") {
678 code.push_str(" let cerebras_api_key = std::env::var(\"CEREBRAS_API_KEY\")\n");
679 code.push_str(" .expect(\"CEREBRAS_API_KEY must be set\");\n\n");
680 }
681 if providers.contains("sambanova") {
682 code.push_str(" let sambanova_api_key = std::env::var(\"SAMBANOVA_API_KEY\")\n");
683 code.push_str(" .expect(\"SAMBANOVA_API_KEY must be set\");\n\n");
684 }
685 if providers.contains("bedrock") {
686 code.push_str(" let bedrock_region = std::env::var(\"AWS_DEFAULT_REGION\")\n");
687 code.push_str(" .unwrap_or_else(|_| \"us-east-1\".to_string());\n\n");
688 }
689 if providers.contains("azure-ai") {
690 code.push_str(" let azure_ai_endpoint = std::env::var(\"AZURE_AI_ENDPOINT\")\n");
691 code.push_str(" .expect(\"AZURE_AI_ENDPOINT must be set\");\n");
692 code.push_str(" let azure_ai_api_key = std::env::var(\"AZURE_AI_API_KEY\")\n");
693 code.push_str(" .expect(\"AZURE_AI_API_KEY must be set\");\n\n");
694 }
695
696 let uses_browser = project
698 .agents
699 .values()
700 .any(|a| a.tools.contains(&"browser".to_string()));
701 if uses_browser {
702 code.push_str(" // Initialize browser session\n");
703 code.push_str(" let browser_config = BrowserConfig::new().headless(true);\n");
704 code.push_str(" let browser = Arc::new(BrowserSession::new(browser_config));\n");
705 code.push_str(" browser.start().await?;\n");
706 code.push_str(" let browser_toolset = BrowserToolset::new(browser.clone());\n\n");
707 }
708
709 let all_sub_agents: std::collections::HashSet<_> = project
711 .agents
712 .values()
713 .flat_map(|a| a.sub_agents.iter().cloned())
714 .collect();
715 let top_level: Vec<_> = project
716 .agents
717 .keys()
718 .filter(|id| !all_sub_agents.contains(*id))
719 .collect();
720
721 use crate::codegen::action_nodes::ActionNodeConfig;
727 use crate::codegen::action_nodes::EvaluationMode;
728 let loop_node_ids: std::collections::HashSet<&str> = project
729 .action_nodes
730 .iter()
731 .filter(|(_, n)| matches!(n, ActionNodeConfig::Loop(_)))
732 .map(|(id, _)| id.as_str())
733 .collect();
734
735 let mut predecessor_map: std::collections::HashMap<&str, Vec<&str>> =
736 std::collections::HashMap::new();
737 for edge in &project.workflow.edges {
738 let from_is_trigger = project
740 .action_nodes
741 .get(&edge.from)
742 .map(|n| matches!(n, ActionNodeConfig::Trigger(_)))
743 .unwrap_or(false);
744
745 let is_back_edge = loop_node_ids.contains(edge.to.as_str())
747 && edge.from != "START"
748 && !edge.from.is_empty()
749 && project
750 .action_nodes
751 .get(&edge.from)
752 .map(|n| !matches!(n, ActionNodeConfig::Loop(_)))
753 .unwrap_or(true)
754 && !project
755 .workflow
756 .edges
757 .iter()
758 .any(|e2| e2.from == edge.to && e2.to == edge.from);
759
760 if from_is_trigger || is_back_edge {
761 continue;
762 }
763
764 if edge.from != "START" && edge.to != "END" {
765 let preds = predecessor_map.entry(edge.to.as_str()).or_default();
766 if !preds.contains(&edge.from.as_str()) {
767 preds.push(edge.from.as_str());
768 }
769 } else if edge.from == "START" {
770 let preds = predecessor_map.entry(edge.to.as_str()).or_default();
771 if !preds.contains(&"START") {
772 preds.push("START");
773 }
774 }
775 }
776
777 let all_match_switch_nodes_early: std::collections::HashSet<&str> = project
779 .action_nodes
780 .iter()
781 .filter(|(_, n)| {
782 if let ActionNodeConfig::Switch(config) = n {
783 config.evaluation_mode == EvaluationMode::AllMatch
784 } else {
785 false
786 }
787 })
788 .map(|(id, _)| id.as_str())
789 .collect();
790
791 let mut parallel_branch_agents: std::collections::HashSet<String> =
794 std::collections::HashSet::new();
795 for edge in &project.workflow.edges {
796 if all_match_switch_nodes_early.contains(edge.from.as_str()) {
797 if project.agents.contains_key(&edge.to) {
800 parallel_branch_agents.insert(edge.to.clone());
801 }
802 }
803 }
804
805 for agent_id in &top_level {
807 if let Some(agent) = project.agents.get(*agent_id) {
808 let predecessors = predecessor_map.get(agent_id.as_str());
810 let first_predecessor = predecessors.and_then(|v| v.first().copied());
811 let is_parallel = parallel_branch_agents.contains(*agent_id);
812 match agent.agent_type {
813 AgentType::Router => {
814 code.push_str(&generate_router_node(agent_id, agent));
815 }
816 AgentType::Llm => {
817 code.push_str(&generate_llm_node_v2(
818 agent_id,
819 agent,
820 project,
821 first_predecessor,
822 &predecessor_map,
823 is_parallel,
824 ));
825 }
826 _ => {
827 code.push_str(&generate_container_node(agent_id, agent, project));
829 }
830 }
831 }
832 }
833
834 let executable_action_nodes: Vec<_> = project
836 .action_nodes
837 .iter()
838 .filter(|(_, node)| !matches!(node, ActionNodeConfig::Trigger(_)))
839 .collect();
840
841 for (node_id, node) in &executable_action_nodes {
842 code.push_str(&generate_action_node_function(
843 node_id,
844 node,
845 &predecessor_map,
846 ¶llel_branch_agents,
847 ));
848 }
849
850 code.push_str(" // Build the graph\n");
852
853 let mut extra_channels: Vec<String> = Vec::new();
856 for (node_id, node) in &project.action_nodes {
857 match node {
858 ActionNodeConfig::Set(config) => {
859 for var in &config.variables {
861 if !extra_channels.contains(&var.key) {
862 extra_channels.push(var.key.clone());
863 }
864 }
865 }
866 ActionNodeConfig::Transform(config) => {
867 let key = &config.standard.mapping.output_key;
869 if !key.is_empty() && !extra_channels.contains(key) {
870 extra_channels.push(key.clone());
871 }
872 }
873 ActionNodeConfig::Switch(config) => {
874 let key = if config.standard.mapping.output_key.is_empty() {
876 "branch".to_string()
877 } else {
878 config.standard.mapping.output_key.clone()
879 };
880 if !extra_channels.contains(&key) {
881 extra_channels.push(key);
882 }
883 }
884 ActionNodeConfig::Loop(config) => {
885 extra_channels.push(format!("{}_loop_index", node_id));
886 extra_channels.push(format!("{}_loop_done", node_id));
887 if let Some(fe) = &config.for_each {
888 if !extra_channels.contains(&fe.item_var) {
889 extra_channels.push(fe.item_var.clone());
890 }
891 if !extra_channels.contains(&fe.index_var) {
892 extra_channels.push(fe.index_var.clone());
893 }
894 }
895 if config.results.collect {
896 let agg_key = config
897 .results
898 .aggregation_key
899 .as_deref()
900 .unwrap_or("loop_results");
901 if !extra_channels.contains(&agg_key.to_string()) {
902 extra_channels.push(agg_key.to_string());
903 }
904 }
905 }
906 ActionNodeConfig::Merge(config) => {
907 let key = if config.standard.mapping.output_key.is_empty() {
908 "merged".to_string()
909 } else {
910 config.standard.mapping.output_key.clone()
911 };
912 if !extra_channels.contains(&key) {
913 extra_channels.push(key);
914 }
915 if let Some(keys) = &config.branch_keys {
917 for k in keys {
918 if !extra_channels.contains(k) {
919 extra_channels.push(k.clone());
920 }
921 }
922 }
923 }
924 _ => {
925 let key = &node.standard().mapping.output_key;
927 if !key.is_empty() && !extra_channels.contains(key) {
928 extra_channels.push(key.clone());
929 }
930 }
931 }
932 }
933 let base_channels = vec![
934 "message".to_string(),
935 "classification".to_string(),
936 "response".to_string(),
937 ];
938 let parallel_channels: Vec<String> = parallel_branch_agents
940 .iter()
941 .map(|id| format!("{}_response", id))
942 .collect();
943 let all_channels: Vec<String> = base_channels
944 .into_iter()
945 .chain(extra_channels)
946 .chain(parallel_channels)
947 .collect();
948 let channel_list = all_channels
949 .iter()
950 .map(|c| format!("\"{}\"", c))
951 .collect::<Vec<_>>()
952 .join(", ");
953 code.push_str(&format!(
954 " let graph = StateGraph::with_channels(&[{}])\n",
955 channel_list
956 ));
957
958 for agent_id in &top_level {
960 code.push_str(&format!(" .add_node({}_node)\n", agent_id));
961 }
962
963 for (node_id, _) in &executable_action_nodes {
965 code.push_str(&format!(" .add_node({}_node)\n", node_id));
966 }
967
968 let start_target = project
972 .workflow
973 .edges
974 .iter()
975 .find(|e| e.from == "START")
976 .map(|e| e.to.as_str());
977
978 let actual_start_target = if let Some(target) = start_target {
980 if project
981 .action_nodes
982 .get(target)
983 .map(|n| matches!(n, ActionNodeConfig::Trigger(_)))
984 .unwrap_or(false)
985 {
986 project
988 .workflow
989 .edges
990 .iter()
991 .find(|e| e.from == target)
992 .map(|e| e.to.as_str())
993 } else {
994 Some(target)
995 }
996 } else {
997 None
998 };
999
1000 let switch_nodes: std::collections::HashSet<&str> = project
1003 .action_nodes
1004 .iter()
1005 .filter(|(_, n)| matches!(n, ActionNodeConfig::Switch(_)))
1006 .map(|(id, _)| id.as_str())
1007 .collect();
1008
1009 let all_match_switch_nodes: std::collections::HashSet<&str> = project
1010 .action_nodes
1011 .iter()
1012 .filter(|(_, n)| {
1013 if let ActionNodeConfig::Switch(config) = n {
1014 config.evaluation_mode == EvaluationMode::AllMatch
1015 } else {
1016 false
1017 }
1018 })
1019 .map(|(id, _)| id.as_str())
1020 .collect();
1021
1022 let first_match_switch_nodes: std::collections::HashSet<&str> = switch_nodes
1023 .iter()
1024 .filter(|id| !all_match_switch_nodes.contains(*id))
1025 .copied()
1026 .collect();
1027
1028 let loop_nodes: std::collections::HashSet<&str> = project
1030 .action_nodes
1031 .iter()
1032 .filter(|(_, n)| matches!(n, ActionNodeConfig::Loop(_)))
1033 .map(|(id, _)| id.as_str())
1034 .collect();
1035
1036 let mut switch_edge_map: std::collections::HashMap<&str, Vec<(String, String)>> =
1039 std::collections::HashMap::new();
1040 for edge in &project.workflow.edges {
1041 if first_match_switch_nodes.contains(edge.from.as_str()) {
1042 let port = edge.from_port.clone().unwrap_or_default();
1043 let target = if edge.to == "END" {
1044 "END".to_string()
1045 } else {
1046 edge.to.clone()
1047 };
1048 switch_edge_map
1049 .entry(edge.from.as_str())
1050 .or_default()
1051 .push((port, target));
1052 }
1053 }
1054
1055 let mut loop_body_map: std::collections::HashMap<&str, Vec<String>> =
1058 std::collections::HashMap::new();
1059 let mut loop_back_edges: std::collections::HashSet<(String, String)> =
1060 std::collections::HashSet::new();
1061 for edge in &project.workflow.edges {
1062 if loop_nodes.contains(edge.from.as_str()) {
1063 loop_body_map
1064 .entry(edge.from.as_str())
1065 .or_default()
1066 .push(edge.to.clone());
1067 }
1068 if loop_nodes.contains(edge.to.as_str()) && edge.from != "START" {
1070 loop_back_edges.insert((edge.from.clone(), edge.to.clone()));
1071 }
1072 }
1073 let mut loop_exit_map: std::collections::HashMap<&str, Vec<String>> =
1077 std::collections::HashMap::new();
1078 for (back_from, back_to) in &loop_back_edges {
1079 for edge in &project.workflow.edges {
1081 if edge.from == *back_from && edge.to != *back_to {
1082 loop_exit_map
1083 .entry(back_to.as_str())
1084 .or_default()
1085 .push(edge.to.clone());
1086 }
1087 }
1088 }
1089 for loop_id in &loop_nodes {
1091 if !loop_exit_map.contains_key(loop_id) {
1092 loop_exit_map
1095 .entry(loop_id)
1096 .or_default()
1097 .push("END".to_string());
1098 }
1099 }
1100
1101 for edge in &project.workflow.edges {
1102 let from_is_trigger = project
1104 .action_nodes
1105 .get(&edge.from)
1106 .map(|n| matches!(n, ActionNodeConfig::Trigger(_)))
1107 .unwrap_or(false);
1108
1109 let to_is_trigger = project
1111 .action_nodes
1112 .get(&edge.to)
1113 .map(|n| matches!(n, ActionNodeConfig::Trigger(_)))
1114 .unwrap_or(false);
1115
1116 if from_is_trigger {
1117 continue;
1118 }
1119
1120 if to_is_trigger && edge.to != "END" {
1121 continue;
1122 }
1123
1124 if first_match_switch_nodes.contains(edge.from.as_str()) {
1127 continue;
1128 }
1129
1130 if loop_nodes.contains(edge.from.as_str()) {
1132 continue;
1133 }
1134
1135 if loop_back_edges.contains(&(edge.from.clone(), edge.to.clone())) {
1137 continue;
1138 }
1139
1140 let (from, to) = if edge.from == "START" {
1142 if let Some(actual_target) = actual_start_target {
1143 ("START".to_string(), format!("\"{}\"", actual_target))
1144 } else {
1145 continue; }
1147 } else {
1148 let from = format!("\"{}\"", edge.from);
1149 let to = if edge.to == "END" {
1150 "END".to_string()
1151 } else {
1152 format!("\"{}\"", edge.to)
1153 };
1154 (from, to)
1155 };
1156
1157 if let Some(agent) = project.agents.get(&edge.from) {
1159 if agent.agent_type == AgentType::Router && !agent.routes.is_empty() {
1160 let conditions: Vec<String> = agent
1162 .routes
1163 .iter()
1164 .map(|r| {
1165 let target = if r.target == "END" {
1166 "END".to_string()
1167 } else {
1168 format!("\"{}\"", r.target)
1169 };
1170 format!("(\"{}\", {})", r.condition, target)
1171 })
1172 .collect();
1173
1174 code.push_str(" .add_conditional_edges(\n");
1175 code.push_str(&format!(" \"{}\",\n", edge.from));
1176 code.push_str(" Router::by_field(\"classification\"),\n");
1177 code.push_str(&format!(" [{}],\n", conditions.join(", ")));
1178 code.push_str(" )\n");
1179 continue;
1180 }
1181 }
1182
1183 code.push_str(&format!(" .add_edge({}, {})\n", from, to));
1184 }
1185
1186 for (switch_id, targets) in &switch_edge_map {
1188 if let Some(ActionNodeConfig::Switch(config)) = project.action_nodes.get(*switch_id) {
1189 let output_key = if config.standard.mapping.output_key.is_empty() {
1190 "branch"
1191 } else {
1192 &config.standard.mapping.output_key
1193 };
1194
1195 let conditions: Vec<String> = targets
1197 .iter()
1198 .map(|(port, target)| {
1199 let target_str = if target == "END" {
1200 "END".to_string()
1201 } else {
1202 format!("\"{}\"", target)
1203 };
1204 format!("(\"{}\", {})", port, target_str)
1205 })
1206 .collect();
1207
1208 code.push_str(&format!(
1209 " // Switch node: {} - conditional routing by '{}'\n",
1210 switch_id, output_key
1211 ));
1212 code.push_str(" .add_conditional_edges(\n");
1213 code.push_str(&format!(" \"{}\",\n", switch_id));
1214 code.push_str(&format!(
1215 " Router::by_field(\"{}\"),\n",
1216 output_key
1217 ));
1218 code.push_str(&format!(" [{}],\n", conditions.join(", ")));
1219 code.push_str(" )\n");
1220 }
1221 }
1222
1223 for loop_id in &loop_nodes {
1226 let done_key = format!("{}_loop_done", loop_id);
1227 let body_targets = loop_body_map.get(loop_id).cloned().unwrap_or_default();
1228 let exit_targets = loop_exit_map
1229 .get(loop_id)
1230 .cloned()
1231 .unwrap_or_else(|| vec!["END".to_string()]);
1232
1233 if let Some(body_first) = body_targets.first() {
1234 let exit_first = exit_targets.first().map(|s| s.as_str()).unwrap_or("END");
1235
1236 let body_target_str = format!("\"{}\"", body_first);
1237 let exit_target_str = if exit_first == "END" {
1238 "END".to_string()
1239 } else {
1240 format!("\"{}\"", exit_first)
1241 };
1242
1243 code.push_str(&format!(
1244 " // Loop node: {} - conditional cycle by '{}'\n",
1245 loop_id, done_key
1246 ));
1247 code.push_str(" .add_conditional_edges(\n");
1248 code.push_str(&format!(" \"{}\",\n", loop_id));
1249 code.push_str(&format!(
1250 " Router::by_bool(\"{}\", \"exit\", \"body\"),\n",
1251 done_key
1252 ));
1253 code.push_str(&format!(
1254 " [(\"body\", {}), (\"exit\", {})],\n",
1255 body_target_str, exit_target_str
1256 ));
1257 code.push_str(" )\n");
1258
1259 for (back_from, back_to) in &loop_back_edges {
1261 if back_to.as_str() == *loop_id {
1262 code.push_str(&format!(
1263 " .add_edge(\"{}\", \"{}\")\n",
1264 back_from, loop_id
1265 ));
1266 }
1267 }
1268 }
1269 }
1270
1271 code.push_str(" .compile()?;\n\n");
1272
1273 code.push_str(" // Get session ID from args or generate new one\n");
1275 code.push_str(" let session_id = std::env::args().nth(1).unwrap_or_else(|| uuid::Uuid::new_v4().to_string());\n");
1276 code.push_str(" println!(\"SESSION:{}\", session_id);\n\n");
1277 code.push_str(" // Conversation history for memory\n");
1278 code.push_str(" let mut history: Vec<(String, String)> = Vec::new();\n\n");
1279 code.push_str(" // Interactive loop\n");
1280 code.push_str(
1281 " println!(\"Graph workflow ready. Type your message (or 'quit' to exit):\");\n",
1282 );
1283 code.push_str(" let stdin = std::io::stdin();\n");
1284 code.push_str(" let mut input = String::new();\n");
1285 code.push_str(" let mut turn = 0;\n");
1286 code.push_str(" loop {\n");
1287 code.push_str(" input.clear();\n");
1288 code.push_str(" print!(\"> \");\n");
1289 code.push_str(" use std::io::Write;\n");
1290 code.push_str(" std::io::stdout().flush()?;\n");
1291 code.push_str(" stdin.read_line(&mut input)?;\n");
1292 code.push_str(" let msg = input.trim();\n");
1293 code.push_str(" if msg.is_empty() || msg == \"quit\" { break; }\n\n");
1294 code.push_str(" // Build message with conversation history\n");
1295 code.push_str(" let context = if history.is_empty() {\n");
1296 code.push_str(" msg.to_string()\n");
1297 code.push_str(" } else {\n");
1298 code.push_str(" let hist: String = history.iter().map(|(u, a)| format!(\"User: {}\\nAssistant: {}\\n\", u, a)).collect();\n");
1299 code.push_str(" format!(\"{}\\nUser: {}\", hist, msg)\n");
1300 code.push_str(" };\n\n");
1301 code.push_str(" let mut state = State::new();\n");
1302 code.push_str(" state.insert(\"message\".to_string(), json!(context));\n");
1303 code.push_str(" \n");
1304 code.push_str(" use adk_graph::StreamMode;\n");
1305 code.push_str(" use tokio_stream::StreamExt;\n");
1306 code.push_str(" let stream = graph.stream(state, ExecutionConfig::new(&format!(\"{}-turn-{}\", session_id, turn)), StreamMode::Messages);\n");
1307 code.push_str(" tokio::pin!(stream);\n");
1308 code.push_str(" let mut final_response = String::new();\n");
1309 code.push_str(" \n");
1310 code.push_str(" while let Some(event) = stream.next().await {\n");
1311 code.push_str(" match event {\n");
1312 code.push_str(" Ok(e) => {\n");
1313 code.push_str(" // Stream Message events as chunks\n");
1314 code.push_str(
1315 " if let adk_graph::StreamEvent::Message { content, .. } = &e {\n",
1316 );
1317 code.push_str(" final_response.push_str(content);\n");
1318 code.push_str(" println!(\"CHUNK:{}\", serde_json::to_string(&final_response).unwrap_or_default());\n");
1319 code.push_str(" }\n");
1320 code.push_str(" // Emit THINKING: prefix for Part::Thinking content (Requirements 6.1, 6.2)\n");
1321 code.push_str(" if let adk_graph::StreamEvent::Custom { event_type, data, .. } = &e {\n");
1322 code.push_str(" if event_type == \"agent_event\" {\n");
1323 code.push_str(" if let Some(parts) = data.pointer(\"/content/parts\").and_then(|v| v.as_array()) {\n");
1324 code.push_str(" for part in parts {\n");
1325 code.push_str(" if let Some(thinking) = part.get(\"thinking\").and_then(|v| v.as_str()) {\n");
1326 code.push_str(" if !thinking.is_empty() {\n");
1327 code.push_str(" println!(\"THINKING:{}\", serde_json::to_string(&thinking).unwrap_or_default());\n");
1328 code.push_str(" }\n");
1329 code.push_str(" }\n");
1330 code.push_str(" }\n");
1331 code.push_str(" }\n");
1332 code.push_str(" }\n");
1333 code.push_str(" }\n");
1334 code.push_str(" // Output trace event as JSON\n");
1335 code.push_str(" if let Ok(json) = serde_json::to_string(&e) {\n");
1336 code.push_str(" println!(\"TRACE:{}\", json);\n");
1337 code.push_str(" }\n");
1338 code.push_str(" // Capture final response from Done event\n");
1339 code.push_str(" if let adk_graph::StreamEvent::Done { state, .. } = &e {\n");
1340 code.push_str(" if let Some(resp) = state.get(\"response\").and_then(|v| v.as_str()) {\n");
1341 code.push_str(" final_response = resp.to_string();\n");
1342 code.push_str(" }\n");
1343 code.push_str(" }\n");
1344 code.push_str(" }\n");
1345 code.push_str(" Err(e) => eprintln!(\"Error: {}\", e),\n");
1346 code.push_str(" }\n");
1347 code.push_str(" }\n");
1348 code.push_str(" turn += 1;\n\n");
1349 code.push_str(" // Save to history\n");
1350 code.push_str(" if !final_response.is_empty() {\n");
1351 code.push_str(" history.push((msg.to_string(), final_response.clone()));\n");
1352 code.push_str(" println!(\"RESPONSE:{}\", serde_json::to_string(&final_response).unwrap_or_default());\n");
1353 code.push_str(" }\n");
1354 code.push_str(" }\n\n");
1355
1356 code.push_str(" Ok(())\n");
1357 code.push_str("}\n");
1358
1359 code
1360}
1361
1362fn generate_router_node(id: &str, agent: &AgentSchema) -> String {
1363 let mut code = String::new();
1364 let model = agent.model.as_deref().unwrap_or("gemini-3.1-flash-lite-preview");
1365
1366 code.push_str(&format!(" // Router: {}\n", id));
1367 code.push_str(&format!(" let {}_llm = Arc::new(\n", id));
1368 code.push_str(&format!(" LlmAgentBuilder::new(\"{}\")\n", id));
1369
1370 let provider = detect_provider(model);
1372 match provider {
1373 "openai" => {
1374 code.push_str(&format!(
1375 " .model(Arc::new(OpenAIClient::new(OpenAIConfig::new(&openai_api_key, \"{}\"))?))\n",
1376 model
1377 ));
1378 }
1379 "anthropic" => {
1380 code.push_str(&format!(
1381 " .model(Arc::new(AnthropicClient::new(AnthropicConfig::new(&anthropic_api_key, \"{}\"))?))\n",
1382 model
1383 ));
1384 }
1385 "deepseek" => {
1386 let m = model.to_lowercase();
1387 if m.contains("reasoner") || m.contains("r1") {
1388 code.push_str(
1389 " .model(Arc::new(DeepSeekClient::reasoner(&deepseek_api_key)?))\n",
1390 );
1391 } else {
1392 code.push_str(
1393 " .model(Arc::new(DeepSeekClient::chat(&deepseek_api_key)?))\n",
1394 );
1395 }
1396 }
1397 "groq" => {
1398 code.push_str(&format!(
1399 " .model(Arc::new(GroqClient::new(GroqConfig::new(&groq_api_key, \"{}\"))?))\n",
1400 model
1401 ));
1402 }
1403 "ollama" => {
1404 code.push_str(&format!(
1405 " .model(Arc::new(OllamaModel::new(OllamaConfig::new(\"{}\"))?))\n",
1406 model
1407 ));
1408 }
1409 _ => {
1410 code.push_str(&format!(
1412 " .model(Arc::new(GeminiModel::new(&gemini_api_key, \"{}\")?))\n",
1413 model
1414 ));
1415 }
1416 }
1417
1418 let route_options: Vec<&str> = agent.routes.iter().map(|r| r.condition.as_str()).collect();
1419 let instruction = if agent.instruction.is_empty() {
1420 format!(
1421 "Classify the input into one of: {}. Respond with ONLY the category name.",
1422 route_options.join(", ")
1423 )
1424 } else {
1425 agent.instruction.clone()
1426 };
1427 let escaped = instruction
1428 .replace('\\', "\\\\")
1429 .replace('"', "\\\"")
1430 .replace('\n', "\\n");
1431 code.push_str(&format!(" .instruction(\"{}\")\n", escaped));
1432 code.push_str(" .build()?\n");
1433 code.push_str(" );\n\n");
1434
1435 code.push_str(&format!(
1436 " let {}_node = AgentNode::new({}_llm)\n",
1437 id, id
1438 ));
1439 code.push_str(" .with_input_mapper(|state| {\n");
1440 code.push_str(
1441 " let msg = state.get(\"message\").and_then(|v| v.as_str()).unwrap_or(\"\");\n",
1442 );
1443 code.push_str(" adk_core::Content::new(\"user\").with_text(msg.to_string())\n");
1444 code.push_str(" })\n");
1445 code.push_str(" .with_output_mapper(|events| {\n");
1446 code.push_str(" let mut updates = std::collections::HashMap::new();\n");
1447 code.push_str(" for event in events {\n");
1448 code.push_str(" if let Some(content) = event.content() {\n");
1449 code.push_str(" let text: String = content.parts.iter()\n");
1450 code.push_str(" .filter_map(|p| p.text())\n");
1451 code.push_str(" .collect::<Vec<_>>().join(\"\").to_lowercase();\n");
1452
1453 for (i, route) in agent.routes.iter().enumerate() {
1454 let cond = if i == 0 { "if" } else { "else if" };
1455 code.push_str(&format!(
1456 " {} text.contains(\"{}\") {{\n",
1457 cond,
1458 route.condition.to_lowercase()
1459 ));
1460 code.push_str(&format!(" updates.insert(\"classification\".to_string(), json!(\"{}\"));\n", route.condition));
1461 code.push_str(" }\n");
1462 }
1463 if let Some(first) = agent.routes.first() {
1464 code.push_str(&format!(" else {{ updates.insert(\"classification\".to_string(), json!(\"{}\")); }}\n", first.condition));
1465 }
1466
1467 code.push_str(" }\n");
1468 code.push_str(" }\n");
1469 code.push_str(" updates\n");
1470 code.push_str(" });\n\n");
1471
1472 code
1473}
1474
1475fn generate_llm_node_v2(
1482 id: &str,
1483 agent: &AgentSchema,
1484 project: &ProjectSchema,
1485 predecessor: Option<&str>,
1486 predecessor_map: &std::collections::HashMap<&str, Vec<&str>>,
1487 is_parallel_branch: bool,
1488) -> String {
1489 let mut code = String::new();
1490 let model = agent.model.as_deref().unwrap_or("gemini-3.1-flash-lite-preview");
1491
1492 code.push_str(&format!(" // Agent: {}\n", id));
1493
1494 let mcp_tools: Vec<_> = agent
1496 .tools
1497 .iter()
1498 .filter(|t| *t == "mcp" || t.starts_with("mcp_"))
1499 .collect();
1500
1501 for (idx, mcp_tool) in mcp_tools.iter().enumerate() {
1502 let tool_id = format!("{}_{}", id, mcp_tool);
1503 if let Some(ToolConfig::Mcp(config)) = project.tool_configs.get(&tool_id) {
1504 let cmd = &config.server_command;
1505 let var_suffix = if idx == 0 {
1506 "mcp".to_string()
1507 } else {
1508 format!("mcp_{}", idx + 1)
1509 };
1510 code.push_str(&format!(
1511 " let mut {}_{}_cmd = Command::new(\"{}\");\n",
1512 id, var_suffix, cmd
1513 ));
1514 for arg in &config.server_args {
1515 code.push_str(&format!(
1516 " {}_{}_cmd.arg(\"{}\");\n",
1517 id, var_suffix, arg
1518 ));
1519 }
1520 code.push_str(&format!(
1521 " let {}_{}_client = tokio::time::timeout(\n",
1522 id, var_suffix
1523 ));
1524 code.push_str(" std::time::Duration::from_secs(10),\n");
1525 code.push_str(&format!(
1526 " ().serve(TokioChildProcess::new({}_{}_cmd)?)\n",
1527 id, var_suffix
1528 ));
1529 code.push_str(&format!(" ).await.map_err(|_| anyhow::anyhow!(\"MCP server '{}' failed to start within 10s\"))??;\n", cmd));
1530 code.push_str(&format!(
1531 " let {}_{}_toolset = McpToolset::new({}_{}_client)",
1532 id, var_suffix, id, var_suffix
1533 ));
1534 if !config.tool_filter.is_empty() {
1535 code.push_str(&format!(
1536 ".with_tools(&[{}])",
1537 config
1538 .tool_filter
1539 .iter()
1540 .map(|t| format!("\"{}\"", t))
1541 .collect::<Vec<_>>()
1542 .join(", ")
1543 ));
1544 }
1545 code.push_str(";\n");
1546 code.push_str(&format!(" let {}_{}_tools = {}_{}_toolset.tools(Arc::new(MinimalContext::new())).await?;\n", id, var_suffix, id, var_suffix));
1547 code.push_str(&format!(
1548 " eprintln!(\"Loaded {{}} tools from MCP server '{}'\", {}_{}_tools.len());\n\n",
1549 cmd, id, var_suffix
1550 ));
1551 }
1552 }
1553
1554 code.push_str(&format!(
1555 " let mut {}_builder = LlmAgentBuilder::new(\"{}\")\n",
1556 id, id
1557 ));
1558
1559 let provider = detect_provider(model);
1561 match provider {
1562 "openai" => {
1563 code.push_str(&format!(
1564 " .model(Arc::new(OpenAIClient::new(OpenAIConfig::new(&openai_api_key, \"{}\"))?));\n",
1565 model
1566 ));
1567 }
1568 "anthropic" => {
1569 code.push_str(&format!(
1570 " .model(Arc::new(AnthropicClient::new(AnthropicConfig::new(&anthropic_api_key, \"{}\"))?));\n",
1571 model
1572 ));
1573 }
1574 "deepseek" => {
1575 let m = model.to_lowercase();
1577 if m.contains("reasoner") || m.contains("r1") {
1578 code.push_str(
1579 " .model(Arc::new(DeepSeekClient::reasoner(&deepseek_api_key)?));\n",
1580 );
1581 } else {
1582 code.push_str(
1583 " .model(Arc::new(DeepSeekClient::chat(&deepseek_api_key)?));\n",
1584 );
1585 }
1586 }
1587 "groq" => {
1588 code.push_str(&format!(
1589 " .model(Arc::new(GroqClient::new(GroqConfig::new(&groq_api_key, \"{}\"))?));\n",
1590 model
1591 ));
1592 }
1593 "ollama" => {
1594 code.push_str(&format!(
1595 " .model(Arc::new(OllamaModel::new(OllamaConfig::new(\"{}\"))?));\n",
1596 model
1597 ));
1598 }
1599 "fireworks" => {
1600 code.push_str(&format!(
1601 " .model(Arc::new(FireworksClient::new(FireworksConfig::new(&fireworks_api_key, \"{}\"))?));\n",
1602 model
1603 ));
1604 }
1605 "together" => {
1606 code.push_str(&format!(
1607 " .model(Arc::new(TogetherClient::new(TogetherConfig::new(&together_api_key, \"{}\"))?));\n",
1608 model
1609 ));
1610 }
1611 "mistral" => {
1612 code.push_str(&format!(
1613 " .model(Arc::new(MistralClient::new(MistralConfig::new(&mistral_api_key, \"{}\"))?));\n",
1614 model
1615 ));
1616 }
1617 "perplexity" => {
1618 code.push_str(&format!(
1619 " .model(Arc::new(PerplexityClient::new(PerplexityConfig::new(&perplexity_api_key, \"{}\"))?));\n",
1620 model
1621 ));
1622 }
1623 "cerebras" => {
1624 code.push_str(&format!(
1625 " .model(Arc::new(CerebrasClient::new(CerebrasConfig::new(&cerebras_api_key, \"{}\"))?));\n",
1626 model
1627 ));
1628 }
1629 "sambanova" => {
1630 code.push_str(&format!(
1631 " .model(Arc::new(SambaNovaClient::new(SambaNovaConfig::new(&sambanova_api_key, \"{}\"))?));\n",
1632 model
1633 ));
1634 }
1635 "bedrock" => {
1636 code.push_str(&format!(
1637 " .model(Arc::new(BedrockClient::new(BedrockConfig::new(&bedrock_region, \"{}\")).await?));\n",
1638 model
1639 ));
1640 }
1641 "azure-ai" => {
1642 code.push_str(&format!(
1643 " .model(Arc::new(AzureAIClient::new(AzureAIConfig::new(&azure_ai_endpoint, &azure_ai_api_key, \"{}\"))?));\n",
1644 model
1645 ));
1646 }
1647 _ => {
1648 code.push_str(&format!(
1650 " .model(Arc::new(GeminiModel::new(&gemini_api_key, \"{}\")?));\n",
1651 model
1652 ));
1653 }
1654 }
1655
1656 if !agent.instruction.is_empty() {
1657 let instruction_clean = strip_template_variables(&agent.instruction);
1661 let escaped = instruction_clean
1662 .replace('\\', "\\\\")
1663 .replace('"', "\\\"")
1664 .replace('\n', "\\n");
1665 code.push_str(&format!(
1666 " {}_builder = {}_builder.instruction(\"{}\");\n",
1667 id, id, escaped
1668 ));
1669 }
1670
1671 for (idx, mcp_tool) in mcp_tools.iter().enumerate() {
1673 let tool_id = format!("{}_{}", id, mcp_tool);
1674 if project.tool_configs.contains_key(&tool_id) {
1675 let var_suffix = if idx == 0 {
1676 "mcp".to_string()
1677 } else {
1678 format!("mcp_{}", idx + 1)
1679 };
1680 code.push_str(&format!(" for tool in {}_{}_tools {{\n", id, var_suffix));
1681 code.push_str(&format!(
1682 " {}_builder = {}_builder.tool(tool);\n",
1683 id, id
1684 ));
1685 code.push_str(" }\n");
1686 }
1687 }
1688
1689 for tool_type in &agent.tools {
1690 if tool_type.starts_with("function") {
1691 let tool_id = format!("{}_{}", id, tool_type);
1692 if let Some(ToolConfig::Function(config)) = project.tool_configs.get(&tool_id) {
1693 let struct_name = to_pascal_case(&config.name);
1694 code.push_str(&format!(" {}_builder = {}_builder.tool(Arc::new(FunctionTool::new(\"{}\", \"{}\", {}_fn).with_parameters_schema::<{}Args>()));\n",
1695 id, id, config.name, config.description.replace('"', "\\\""), config.name, struct_name));
1696 }
1697 } else if !tool_type.starts_with("mcp") {
1698 match tool_type.as_str() {
1699 "google_search" => code.push_str(&format!(
1700 " {}_builder = {}_builder.tool(Arc::new(GoogleSearchTool::new()));\n",
1701 id, id
1702 )),
1703 "exit_loop" => code.push_str(&format!(
1704 " {}_builder = {}_builder.tool(Arc::new(ExitLoopTool::new()));\n",
1705 id, id
1706 )),
1707 "load_artifact" => code.push_str(&format!(
1708 " {}_builder = {}_builder.tool(Arc::new(LoadArtifactsTool::new()));\n",
1709 id, id
1710 )),
1711 "browser" => {
1712 code.push_str(" for tool in browser_toolset.tools(Arc::new(MinimalContext::new())).await? {\n");
1713 code.push_str(&format!(
1714 " {}_builder = {}_builder.tool(tool);\n",
1715 id, id
1716 ));
1717 code.push_str(" }\n");
1718 }
1719 _ => {}
1720 }
1721 }
1722 }
1723
1724 if let Some(temp) = agent.temperature {
1726 code.push_str(&format!(
1727 " {}_builder = {}_builder.temperature({:.1});\n",
1728 id, id, temp
1729 ));
1730 }
1731 if let Some(top_p) = agent.top_p {
1732 code.push_str(&format!(
1733 " {}_builder = {}_builder.top_p({:.2});\n",
1734 id, id, top_p
1735 ));
1736 }
1737 if let Some(top_k) = agent.top_k {
1738 code.push_str(&format!(
1739 " {}_builder = {}_builder.top_k({});\n",
1740 id, id, top_k
1741 ));
1742 }
1743 if let Some(max_tokens) = agent.max_output_tokens {
1744 code.push_str(&format!(
1745 " {}_builder = {}_builder.max_output_tokens({});\n",
1746 id, id, max_tokens
1747 ));
1748 }
1749
1750 code.push_str(&format!(
1751 " let {}_llm = Arc::new({}_builder.build()?);\n\n",
1752 id, id
1753 ));
1754
1755 code.push_str(&format!(
1756 " let {}_node = AgentNode::new({}_llm)\n",
1757 id, id
1758 ));
1759 code.push_str(" .with_input_mapper(|state| {\n");
1760
1761 let is_first = predecessor == Some("START") || predecessor.is_none();
1763
1764 if is_first {
1765 code.push_str(" // First node: read from original message\n");
1766 code.push_str(" let msg = state.get(\"message\").and_then(|v| v.as_str()).unwrap_or(\"\");\n");
1767 } else {
1768 code.push_str(&format!(
1769 " // Predecessor: {} - read from response\n",
1770 predecessor.unwrap_or("unknown")
1771 ));
1772 code.push_str(" let msg = state.get(\"response\").and_then(|v| v.as_str())\n");
1773 code.push_str(" .or_else(|| state.get(\"message\").and_then(|v| v.as_str())).unwrap_or(\"\");\n");
1774 }
1775
1776 let mut inject_vars: Vec<String> = Vec::new();
1780
1781 let var_refs: Vec<&str> = agent
1783 .instruction
1784 .match_indices("{{")
1785 .filter_map(|(start, _)| {
1786 let rest = &agent.instruction[start + 2..];
1787 rest.find("}}").map(|end| &rest[..end])
1788 })
1789 .collect();
1790 for var in &var_refs {
1791 if *var != "message" && *var != "response" && !inject_vars.contains(&var.to_string()) {
1792 inject_vars.push(var.to_string());
1793 }
1794 }
1795
1796 {
1800 use crate::codegen::action_nodes::ActionNodeConfig;
1801 let mut queue: Vec<&str> = if let Some(pred) = predecessor {
1802 vec![pred]
1803 } else {
1804 vec![]
1805 };
1806 if let Some(preds) = predecessor_map.get(id) {
1808 for p in preds {
1809 if !queue.contains(p) {
1810 queue.push(p);
1811 }
1812 }
1813 }
1814 let mut visited_preds = std::collections::HashSet::new();
1815 while let Some(pred_id) = queue.pop() {
1816 if pred_id == "START" || visited_preds.contains(pred_id) {
1817 continue;
1818 }
1819 visited_preds.insert(pred_id);
1820 if let Some(action_node) = project.action_nodes.get(pred_id) {
1821 match action_node {
1822 ActionNodeConfig::Set(set_config) => {
1823 for var in &set_config.variables {
1824 if !inject_vars.contains(&var.key) {
1825 inject_vars.push(var.key.clone());
1826 }
1827 }
1828 }
1829 ActionNodeConfig::Transform(transform_config) => {
1830 let out_key = &transform_config.standard.mapping.output_key;
1831 if !inject_vars.contains(out_key) {
1832 inject_vars.push(out_key.clone());
1833 }
1834 }
1835 ActionNodeConfig::Merge(merge_config) => {
1836 let out_key = if merge_config.standard.mapping.output_key.is_empty() {
1838 "merged".to_string()
1839 } else {
1840 merge_config.standard.mapping.output_key.clone()
1841 };
1842 if !inject_vars.contains(&out_key) {
1843 inject_vars.push(out_key);
1844 }
1845 }
1846 _ => {
1847 let out_key = &action_node.standard().mapping.output_key;
1850 if !out_key.is_empty() && !inject_vars.contains(out_key) {
1851 inject_vars.push(out_key.clone());
1852 }
1853 }
1854 }
1855 }
1856 if let Some(preds) = predecessor_map.get(pred_id) {
1858 for p in preds {
1859 if !visited_preds.contains(*p) {
1860 queue.push(p);
1861 }
1862 }
1863 }
1864 }
1865 }
1866
1867 if !inject_vars.is_empty() {
1868 code.push_str(
1869 " // Include state variables from Set nodes and instruction references\n",
1870 );
1871 code.push_str(" let mut full_msg = msg.to_string();\n");
1872 code.push_str(" let mut context_parts: Vec<String> = Vec::new();\n");
1873 for var in &inject_vars {
1874 code.push_str(&format!(
1875 " if let Some(v) = state.get(\"{}\") {{\n",
1876 var
1877 ));
1878 code.push_str(&format!(" context_parts.push(format!(\"{}: {{}}\", v.as_str().unwrap_or(&v.to_string())));\n", var));
1879 code.push_str(" }\n");
1880 }
1881 code.push_str(" if !context_parts.is_empty() {\n");
1882 code.push_str(" full_msg = format!(\"{}\\n\\nContext:\\n{}\", full_msg, context_parts.join(\"\\n\"));\n");
1883 code.push_str(" }\n");
1884 code.push_str(" adk_core::Content::new(\"user\").with_text(full_msg)\n");
1885 } else {
1886 code.push_str(" adk_core::Content::new(\"user\").with_text(msg.to_string())\n");
1887 }
1888 code.push_str(" })\n");
1889 code.push_str(" .with_output_mapper(|events| {\n");
1890 code.push_str(" let mut updates = std::collections::HashMap::new();\n");
1891 code.push_str(" let mut full_text = String::new();\n");
1892 code.push_str(" for event in events {\n");
1893 code.push_str(" if let Some(content) = event.content() {\n");
1894 code.push_str(" for part in &content.parts {\n");
1895 code.push_str(" if let Some(text) = part.text() {\n");
1896 code.push_str(" full_text.push_str(text);\n");
1897 code.push_str(" }\n");
1898 code.push_str(" }\n");
1899 code.push_str(" }\n");
1900 code.push_str(" }\n");
1901 code.push_str(" if !full_text.is_empty() {\n");
1902 if is_parallel_branch {
1903 code.push_str(&format!(
1905 " updates.insert(\"{}_response\".to_string(), json!(full_text));\n",
1906 id
1907 ));
1908 } else {
1909 code.push_str(
1910 " updates.insert(\"response\".to_string(), json!(full_text));\n",
1911 );
1912 }
1913 code.push_str(" }\n");
1914 code.push_str(" updates\n");
1915 code.push_str(" });\n\n");
1916
1917 code
1918}
1919
1920fn generate_container_node(id: &str, agent: &AgentSchema, project: &ProjectSchema) -> String {
1921 let mut code = String::new();
1922
1923 for sub_id in &agent.sub_agents {
1925 if let Some(sub) = project.agents.get(sub_id) {
1926 let model = sub.model.as_deref().unwrap_or("gemini-3.1-flash-lite-preview");
1927 let has_tools = !sub.tools.is_empty();
1928 let has_instruction = !sub.instruction.is_empty();
1929 let mut_kw = if has_tools || has_instruction {
1930 "mut "
1931 } else {
1932 ""
1933 };
1934
1935 for tool_type in &sub.tools {
1937 let tool_id = format!("{}_{}", sub_id, tool_type);
1938 if tool_type.starts_with("mcp") {
1939 if let Some(ToolConfig::Mcp(config)) = project.tool_configs.get(&tool_id) {
1940 let var_suffix = tool_type.replace("mcp_", "mcp");
1941 code.push_str(&format!(
1942 " let mut {}_{}_cmd = Command::new(\"{}\");\n",
1943 sub_id, var_suffix, config.server_command
1944 ));
1945 for arg in &config.server_args {
1946 code.push_str(&format!(
1947 " {}_{}_cmd.arg(\"{}\");\n",
1948 sub_id, var_suffix, arg
1949 ));
1950 }
1951 code.push_str(&format!(
1952 " let {}_{}_client = tokio::time::timeout(\n",
1953 sub_id, var_suffix
1954 ));
1955 code.push_str(" std::time::Duration::from_secs(10),\n");
1956 code.push_str(&format!(
1957 " ().serve(TokioChildProcess::new({}_{}_cmd)?)\n",
1958 sub_id, var_suffix
1959 ));
1960 code.push_str(&format!(" ).await.map_err(|_| anyhow::anyhow!(\"MCP server '{}' failed to start within 10s\"))??;\n", config.server_command));
1961 code.push_str(&format!(
1962 " let {}_{}_toolset = McpToolset::new({}_{}_client);\n",
1963 sub_id, var_suffix, sub_id, var_suffix
1964 ));
1965 code.push_str(&format!(" let {}_{}_tools = {}_{}_toolset.tools(Arc::new(MinimalContext::new())).await?;\n", sub_id, var_suffix, sub_id, var_suffix));
1966 code.push_str(&format!(" eprintln!(\"Loaded {{}} tools from MCP server '{}'\", {}_{}_tools.len());\n\n", config.server_command, sub_id, var_suffix));
1967 }
1968 }
1969 }
1970
1971 code.push_str(&format!(
1973 " let {}{}_builder = LlmAgentBuilder::new(\"{}\")\n",
1974 mut_kw, sub_id, sub_id
1975 ));
1976
1977 let provider = detect_provider(model);
1979 match provider {
1980 "openai" => {
1981 code.push_str(&format!(
1982 " .model(Arc::new(OpenAIClient::new(OpenAIConfig::new(&openai_api_key, \"{}\"))?));\n",
1983 model
1984 ));
1985 }
1986 "anthropic" => {
1987 code.push_str(&format!(
1988 " .model(Arc::new(AnthropicClient::new(AnthropicConfig::new(&anthropic_api_key, \"{}\"))?));\n",
1989 model
1990 ));
1991 }
1992 "deepseek" => {
1993 let m = model.to_lowercase();
1994 if m.contains("reasoner") || m.contains("r1") {
1995 code.push_str(
1996 " .model(Arc::new(DeepSeekClient::reasoner(&deepseek_api_key)?));\n",
1997 );
1998 } else {
1999 code.push_str(
2000 " .model(Arc::new(DeepSeekClient::chat(&deepseek_api_key)?));\n",
2001 );
2002 }
2003 }
2004 "groq" => {
2005 code.push_str(&format!(
2006 " .model(Arc::new(GroqClient::new(GroqConfig::new(&groq_api_key, \"{}\"))?));\n",
2007 model
2008 ));
2009 }
2010 "ollama" => {
2011 code.push_str(&format!(
2012 " .model(Arc::new(OllamaModel::new(OllamaConfig::new(\"{}\"))?));\n",
2013 model
2014 ));
2015 }
2016 _ => {
2017 code.push_str(&format!(
2019 " .model(Arc::new(GeminiModel::new(&gemini_api_key, \"{}\")?))",
2020 model
2021 ));
2022 code.push_str(";\n");
2023 }
2024 }
2025
2026 if !sub.instruction.is_empty() {
2028 let escaped = sub
2029 .instruction
2030 .replace('\\', "\\\\")
2031 .replace('"', "\\\"")
2032 .replace('\n', "\\n");
2033 code.push_str(&format!(
2034 " {}_builder = {}_builder.instruction(\"{}\");\n",
2035 sub_id, sub_id, escaped
2036 ));
2037 }
2038
2039 for tool_type in &sub.tools {
2041 let tool_id = format!("{}_{}", sub_id, tool_type);
2042 if tool_type.starts_with("function") {
2043 if let Some(ToolConfig::Function(config)) = project.tool_configs.get(&tool_id) {
2044 let fn_name = &config.name;
2045 let struct_name = to_pascal_case(fn_name);
2046 code.push_str(&format!(" {}_builder = {}_builder.tool(Arc::new(FunctionTool::new(\"{}\", \"{}\", {}_fn).with_parameters_schema::<{}Args>()));\n",
2047 sub_id, sub_id, fn_name, config.description.replace('"', "\\\""), fn_name, struct_name));
2048 }
2049 } else if tool_type.starts_with("mcp") {
2050 let tool_id = format!("{}_{}", sub_id, tool_type);
2052 if project.tool_configs.contains_key(&tool_id) {
2053 let var_suffix = tool_type.replace("mcp_", "mcp");
2054 code.push_str(&format!(
2055 " for tool in {}_{}_tools {{\n",
2056 sub_id, var_suffix
2057 ));
2058 code.push_str(&format!(
2059 " {}_builder = {}_builder.tool(tool);\n",
2060 sub_id, sub_id
2061 ));
2062 code.push_str(" }\n");
2063 }
2064 } else if tool_type == "google_search" {
2065 code.push_str(&format!(
2066 " {}_builder = {}_builder.tool(Arc::new(GoogleSearchTool::new()));\n",
2067 sub_id, sub_id
2068 ));
2069 } else if tool_type == "exit_loop" {
2070 code.push_str(&format!(
2071 " {}_builder = {}_builder.tool(Arc::new(ExitLoopTool::new()));\n",
2072 sub_id, sub_id
2073 ));
2074 } else if tool_type == "load_artifact" {
2075 code.push_str(&format!(
2076 " {}_builder = {}_builder.tool(Arc::new(LoadArtifactsTool::new()));\n",
2077 sub_id, sub_id
2078 ));
2079 }
2080 }
2081
2082 if let Some(temp) = sub.temperature {
2084 code.push_str(&format!(
2085 " {}_builder = {}_builder.temperature({:.1});\n",
2086 sub_id, sub_id, temp
2087 ));
2088 }
2089 if let Some(top_p) = sub.top_p {
2090 code.push_str(&format!(
2091 " {}_builder = {}_builder.top_p({:.2});\n",
2092 sub_id, sub_id, top_p
2093 ));
2094 }
2095 if let Some(top_k) = sub.top_k {
2096 code.push_str(&format!(
2097 " {}_builder = {}_builder.top_k({});\n",
2098 sub_id, sub_id, top_k
2099 ));
2100 }
2101 if let Some(max_tokens) = sub.max_output_tokens {
2102 code.push_str(&format!(
2103 " {}_builder = {}_builder.max_output_tokens({});\n",
2104 sub_id, sub_id, max_tokens
2105 ));
2106 }
2107
2108 code.push_str(&format!(
2109 " let {}_agent = {}_builder.build()?;\n\n",
2110 sub_id, sub_id
2111 ));
2112 }
2113 }
2114
2115 let subs: Vec<_> = agent
2117 .sub_agents
2118 .iter()
2119 .map(|s| format!("Arc::new({}_agent)", s))
2120 .collect();
2121 let container_type = match agent.agent_type {
2122 AgentType::Sequential => "adk_agent::SequentialAgent",
2123 AgentType::Loop => "adk_agent::LoopAgent",
2124 AgentType::Parallel => "adk_agent::ParallelAgent",
2125 _ => "adk_agent::SequentialAgent",
2126 };
2127
2128 code.push_str(&format!(
2129 " // Container: {} ({:?})\n",
2130 id, agent.agent_type
2131 ));
2132 if agent.agent_type == AgentType::Loop {
2133 let max_iter = agent.max_iterations.unwrap_or(3);
2134 code.push_str(&format!(
2135 " let {}_container = {}::new(\"{}\", vec![{}]).with_max_iterations({});\n\n",
2136 id,
2137 container_type,
2138 id,
2139 subs.join(", "),
2140 max_iter
2141 ));
2142 } else {
2143 code.push_str(&format!(
2144 " let {}_container = {}::new(\"{}\", vec![{}]);\n\n",
2145 id,
2146 container_type,
2147 id,
2148 subs.join(", ")
2149 ));
2150 }
2151
2152 code.push_str(&format!(
2154 " let {}_node = AgentNode::new(Arc::new({}_container))\n",
2155 id, id
2156 ));
2157 code.push_str(" .with_input_mapper(|state| {\n");
2158 code.push_str(
2159 " let msg = state.get(\"message\").and_then(|v| v.as_str()).unwrap_or(\"\");\n",
2160 );
2161 code.push_str(" adk_core::Content::new(\"user\").with_text(msg.to_string())\n");
2162 code.push_str(" })\n");
2163 code.push_str(" .with_output_mapper(|events| {\n");
2164 code.push_str(" let mut updates = std::collections::HashMap::new();\n");
2165 code.push_str(" let mut full_text = String::new();\n");
2166 code.push_str(" for event in events {\n");
2167 code.push_str(" if let Some(content) = event.content() {\n");
2168 code.push_str(" for part in &content.parts {\n");
2169 code.push_str(" if let Some(text) = part.text() {\n");
2170 code.push_str(" full_text.push_str(text);\n");
2171 code.push_str(" }\n");
2172 code.push_str(" }\n");
2173 code.push_str(" }\n");
2174 code.push_str(" }\n");
2175 code.push_str(" // Filter out tool call artifacts\n");
2176 code.push_str(" let full_text = full_text.replace(\"exit_loop\", \"\");\n");
2177 code.push_str(" if !full_text.is_empty() {\n");
2178 code.push_str(" updates.insert(\"response\".to_string(), json!(full_text));\n");
2179 code.push_str(" }\n");
2180 code.push_str(" updates\n");
2181 code.push_str(" });\n\n");
2182
2183 code
2184}
2185
2186fn generate_function_tool(config: &crate::schema::FunctionToolConfig) -> String {
2187 let mut code = String::new();
2188 let fn_name = &config.name;
2189
2190 code.push_str(&format!("async fn {}_fn(_ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value, adk_core::AdkError> {{\n", fn_name));
2191
2192 for param in &config.parameters {
2194 let extract = match param.param_type {
2195 crate::schema::ParamType::String => format!(
2196 " let {} = args[\"{}\"].as_str().unwrap_or(\"\");\n",
2197 param.name, param.name
2198 ),
2199 crate::schema::ParamType::Number => format!(
2200 " let {} = args[\"{}\"].as_f64().unwrap_or(0.0);\n",
2201 param.name, param.name
2202 ),
2203 crate::schema::ParamType::Boolean => format!(
2204 " let {} = args[\"{}\"].as_bool().unwrap_or(false);\n",
2205 param.name, param.name
2206 ),
2207 };
2208 code.push_str(&extract);
2209 }
2210
2211 code.push('\n');
2212
2213 if config.code.is_empty() {
2215 let param_json = config
2217 .parameters
2218 .iter()
2219 .map(|p| format!(" \"{}\": {}", p.name, p.name))
2220 .collect::<Vec<_>>()
2221 .join(",\n");
2222 code.push_str(" // TODO: Add function implementation\n");
2223 code.push_str(" Ok(json!({\n");
2224 code.push_str(&format!(" \"function\": \"{}\",\n", fn_name));
2225 if !param_json.is_empty() {
2226 code.push_str(¶m_json);
2227 code.push_str(",\n");
2228 }
2229 code.push_str(" \"status\": \"not_implemented\"\n");
2230 code.push_str(" }))\n");
2231 } else {
2232 code.push_str(" // User-defined implementation\n");
2234 for line in config.code.lines() {
2235 code.push_str(&format!(" {}\n", line));
2236 }
2237 }
2238
2239 code.push_str("}\n\n");
2240 code
2241}
2242
2243fn generate_function_schema(config: &crate::schema::FunctionToolConfig) -> String {
2244 let mut code = String::new();
2245 let struct_name = to_pascal_case(&config.name);
2246
2247 code.push_str("#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)]\n");
2248 code.push_str(&format!("struct {}Args {{\n", struct_name));
2249
2250 for param in &config.parameters {
2251 if !param.description.is_empty() {
2252 code.push_str(&format!(" /// {}\n", param.description));
2253 }
2254 let rust_type = match param.param_type {
2255 crate::schema::ParamType::String => "String",
2256 crate::schema::ParamType::Number => "f64",
2257 crate::schema::ParamType::Boolean => "bool",
2258 };
2259 code.push_str(&format!(" {}: {},\n", param.name, rust_type));
2260 }
2261
2262 code.push_str("}\n\n");
2263 code
2264}
2265
2266fn to_pascal_case(s: &str) -> String {
2267 s.split('_')
2268 .map(|word| {
2269 let mut chars = word.chars();
2270 match chars.next() {
2271 None => String::new(),
2272 Some(c) => c.to_uppercase().chain(chars).collect(),
2273 }
2274 })
2275 .collect()
2276}
2277
2278fn generate_json_path_extraction(code: &mut String, json_path: &str, output_key: &str) {
2283 let path = json_path.strip_prefix("$.").unwrap_or(json_path);
2285 let segments: Vec<&str> = path.split('.').collect();
2286
2287 if segments.is_empty() || (segments.len() == 1 && segments[0].is_empty()) {
2288 code.push_str(&format!(
2290 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"status\": status, \"body\": body }})))\n",
2291 output_key
2292 ));
2293 return;
2294 }
2295
2296 code.push_str(" let mut extracted = body.clone();\n");
2297 for seg in &segments {
2298 code.push_str(&format!(
2299 " extracted = extracted.get(\"{}\").cloned().unwrap_or(json!(null));\n",
2300 seg.replace('"', "\\\"")
2301 ));
2302 }
2303 code.push_str(&format!(
2304 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"status\": status, \"body\": extracted }})))\n",
2305 output_key
2306 ));
2307}
2308
2309fn generate_condition_check(code: &mut String, op: &str, value_str: &str, port: &str, field: &str) {
2311 let escaped_val = value_str.replace('"', "\\\"");
2312 match op {
2313 "equals" | "==" | "eq" => {
2314 code.push_str(&format!(
2315 " if field_val == \"{}\" {{ matched_branch = Some(\"{}\"); }}\n",
2316 escaped_val, port
2317 ));
2318 }
2319 "not_equals" | "!=" | "neq" => {
2320 code.push_str(&format!(
2321 " if field_val != \"{}\" {{ matched_branch = Some(\"{}\"); }}\n",
2322 escaped_val, port
2323 ));
2324 }
2325 "contains" => {
2326 code.push_str(&format!(
2327 " if field_val.contains(\"{}\") {{ matched_branch = Some(\"{}\"); }}\n",
2328 escaped_val, port
2329 ));
2330 }
2331 "greater_than" | ">" | "gt" => {
2332 code.push_str(&format!(
2333 " if field_val > \"{}\" {{ matched_branch = Some(\"{}\"); }}\n",
2334 escaped_val, port
2335 ));
2336 }
2337 "less_than" | "<" | "lt" => {
2338 code.push_str(&format!(
2339 " if field_val < \"{}\" {{ matched_branch = Some(\"{}\"); }}\n",
2340 escaped_val, port
2341 ));
2342 }
2343 "gte" | ">=" => {
2344 code.push_str(&format!(
2345 " if field_val >= \"{}\" {{ matched_branch = Some(\"{}\"); }}\n",
2346 escaped_val, port
2347 ));
2348 }
2349 "lte" | "<=" => {
2350 code.push_str(&format!(
2351 " if field_val <= \"{}\" {{ matched_branch = Some(\"{}\"); }}\n",
2352 escaped_val, port
2353 ));
2354 }
2355 "startsWith" => {
2356 code.push_str(&format!(
2357 " if field_val.starts_with(\"{}\") {{ matched_branch = Some(\"{}\"); }}\n",
2358 escaped_val, port
2359 ));
2360 }
2361 "endsWith" => {
2362 code.push_str(&format!(
2363 " if field_val.ends_with(\"{}\") {{ matched_branch = Some(\"{}\"); }}\n",
2364 escaped_val, port
2365 ));
2366 }
2367 "empty" => {
2368 code.push_str(&format!(
2369 " if field_val.is_empty() {{ matched_branch = Some(\"{}\"); }}\n",
2370 port
2371 ));
2372 }
2373 "exists" => {
2374 code.push_str(&format!(
2375 " if ctx.state.contains_key(\"{}\") {{ matched_branch = Some(\"{}\"); }}\n",
2376 field, port
2377 ));
2378 }
2379 _ => {
2380 code.push_str(&format!(
2381 " if field_val == \"{}\" {{ matched_branch = Some(\"{}\"); }}\n",
2382 escaped_val, port
2383 ));
2384 }
2385 }
2386}
2387
2388fn generate_all_match_condition_check(
2390 code: &mut String,
2391 op: &str,
2392 value_str: &str,
2393 port: &str,
2394 field: &str,
2395) {
2396 let escaped_val = value_str.replace('"', "\\\"");
2397 match op {
2398 "equals" | "==" | "eq" => {
2399 code.push_str(&format!(
2400 " if field_val == \"{}\" {{ matched_branches.push(\"{}\".to_string()); }}\n",
2401 escaped_val, port
2402 ));
2403 }
2404 "not_equals" | "!=" | "neq" => {
2405 code.push_str(&format!(
2406 " if field_val != \"{}\" {{ matched_branches.push(\"{}\".to_string()); }}\n",
2407 escaped_val, port
2408 ));
2409 }
2410 "contains" => {
2411 code.push_str(&format!(
2412 " if field_val.contains(\"{}\") {{ matched_branches.push(\"{}\".to_string()); }}\n",
2413 escaped_val, port
2414 ));
2415 }
2416 "greater_than" | ">" | "gt" => {
2417 code.push_str(&format!(
2418 " if field_val > \"{}\" {{ matched_branches.push(\"{}\".to_string()); }}\n",
2419 escaped_val, port
2420 ));
2421 }
2422 "less_than" | "<" | "lt" => {
2423 code.push_str(&format!(
2424 " if field_val < \"{}\" {{ matched_branches.push(\"{}\".to_string()); }}\n",
2425 escaped_val, port
2426 ));
2427 }
2428 "gte" | ">=" => {
2429 code.push_str(&format!(
2430 " if field_val >= \"{}\" {{ matched_branches.push(\"{}\".to_string()); }}\n",
2431 escaped_val, port
2432 ));
2433 }
2434 "lte" | "<=" => {
2435 code.push_str(&format!(
2436 " if field_val <= \"{}\" {{ matched_branches.push(\"{}\".to_string()); }}\n",
2437 escaped_val, port
2438 ));
2439 }
2440 "startsWith" => {
2441 code.push_str(&format!(
2442 " if field_val.starts_with(\"{}\") {{ matched_branches.push(\"{}\".to_string()); }}\n",
2443 escaped_val, port
2444 ));
2445 }
2446 "endsWith" => {
2447 code.push_str(&format!(
2448 " if field_val.ends_with(\"{}\") {{ matched_branches.push(\"{}\".to_string()); }}\n",
2449 escaped_val, port
2450 ));
2451 }
2452 "empty" => {
2453 code.push_str(&format!(
2454 " if field_val.is_empty() {{ matched_branches.push(\"{}\".to_string()); }}\n",
2455 port
2456 ));
2457 }
2458 "exists" => {
2459 code.push_str(&format!(
2460 " if ctx.state.contains_key(\"{}\") {{ matched_branches.push(\"{}\".to_string()); }}\n",
2461 field, port
2462 ));
2463 }
2464 _ => {
2465 code.push_str(&format!(
2466 " if field_val == \"{}\" {{ matched_branches.push(\"{}\".to_string()); }}\n",
2467 escaped_val, port
2468 ));
2469 }
2470 }
2471}
2472
2473fn generate_action_node_function(
2474 node_id: &str,
2475 node: &crate::codegen::action_nodes::ActionNodeConfig,
2476 predecessor_map: &std::collections::HashMap<&str, Vec<&str>>,
2477 parallel_branch_agents: &std::collections::HashSet<String>,
2478) -> String {
2479 use crate::codegen::action_nodes::ActionNodeConfig;
2480
2481 let mut code = String::new();
2482
2483 let parallel_predecessor: Option<&str> = predecessor_map
2486 .get(node_id)
2487 .and_then(|preds| preds.iter().find(|p| parallel_branch_agents.contains(**p)))
2488 .copied();
2489
2490 match node {
2491 ActionNodeConfig::Set(config) => {
2492 code.push_str(&format!(
2493 " // Action Node: {} (Set)\n",
2494 config.standard.name
2495 ));
2496 code.push_str(&format!(" let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |ctx| async move {{\n", node_id, node_id));
2497 code.push_str(" let mut output = NodeOutput::new();\n");
2498
2499 for var in &config.variables {
2501 let key = &var.key;
2502 match var.value_type.as_str() {
2503 "expression" => {
2504 let raw_expr = var.value.as_str().unwrap_or("");
2506 let expr = if let Some(pred_id) = parallel_predecessor {
2508 raw_expr
2509 .replace("{{response}}", &format!("{{{{{}_response}}}}", pred_id))
2510 } else {
2511 raw_expr.to_string()
2512 };
2513 code.push_str(&format!(" // Set {} from expression\n", key));
2515 code.push_str(&format!(
2516 " let mut {}_value = \"{}\".to_string();\n",
2517 key,
2518 expr.replace('"', "\\\"")
2519 ));
2520 code.push_str(" // Interpolate variables from state\n");
2521 code.push_str(" for (k, v) in ctx.state.iter() {\n");
2522 code.push_str(" let pattern = format!(\"{{{{{}}}}}\", k);\n");
2523 code.push_str(" if let Some(s) = v.as_str() {\n");
2524 code.push_str(&format!(
2525 " {}_value = {}_value.replace(&pattern, s);\n",
2526 key, key
2527 ));
2528 code.push_str(" } else {\n");
2529 code.push_str(&format!(" {}_value = {}_value.replace(&pattern, &v.to_string());\n", key, key));
2530 code.push_str(" }\n");
2531 code.push_str(" }\n");
2532 code.push_str(&format!(
2533 " output = output.with_update(\"{}\", json!({}_value));\n",
2534 key, key
2535 ));
2536 }
2537 "json" => {
2538 code.push_str(&format!(
2540 " output = output.with_update(\"{}\", json!({}));\n",
2541 key, var.value
2542 ));
2543 }
2544 _ => {
2545 let raw_val = var.value.as_str().unwrap_or("");
2547 if raw_val.contains("{{") {
2548 let val = if let Some(pred_id) = parallel_predecessor {
2550 raw_val.replace(
2551 "{{response}}",
2552 &format!("{{{{{}_response}}}}", pred_id),
2553 )
2554 } else {
2555 raw_val.to_string()
2556 };
2557 code.push_str(&format!(
2558 " // Set {} from string template\n",
2559 key
2560 ));
2561 code.push_str(&format!(
2562 " let mut {}_value = \"{}\".to_string();\n",
2563 key,
2564 val.replace('"', "\\\"")
2565 ));
2566 code.push_str(" for (k, v) in ctx.state.iter() {\n");
2567 code.push_str(
2568 " let pattern = format!(\"{{{{{}}}}}\", k);\n",
2569 );
2570 code.push_str(" if let Some(s) = v.as_str() {\n");
2571 code.push_str(&format!(
2572 " {}_value = {}_value.replace(&pattern, s);\n",
2573 key, key
2574 ));
2575 code.push_str(" } else {\n");
2576 code.push_str(&format!(" {}_value = {}_value.replace(&pattern, &v.to_string());\n", key, key));
2577 code.push_str(" }\n");
2578 code.push_str(" }\n");
2579 code.push_str(&format!(
2580 " output = output.with_update(\"{}\", json!({}_value));\n",
2581 key, key
2582 ));
2583 } else {
2584 code.push_str(&format!(
2585 " output = output.with_update(\"{}\", json!({}));\n",
2586 key, var.value
2587 ));
2588 }
2589 }
2590 }
2591 }
2592
2593 code.push_str(" Ok(output)\n");
2594 code.push_str(" });\n\n");
2595 }
2596 ActionNodeConfig::Transform(config) => {
2597 code.push_str(&format!(
2598 " // Action Node: {} (Transform)\n",
2599 config.standard.name
2600 ));
2601 code.push_str(&format!(" let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |ctx| async move {{\n", node_id, node_id));
2602
2603 let expr = &config.expression;
2605 code.push_str(&format!(
2606 " let mut result = \"{}\".to_string();\n",
2607 expr.replace('"', "\\\"")
2608 ));
2609 code.push_str(" for (k, v) in ctx.state.iter() {\n");
2610 code.push_str(" let pattern = format!(\"{{{{{}}}}}\", k);\n");
2611 code.push_str(" if let Some(s) = v.as_str() {\n");
2612 code.push_str(" result = result.replace(&pattern, s);\n");
2613 code.push_str(" } else {\n");
2614 code.push_str(" result = result.replace(&pattern, &v.to_string());\n");
2615 code.push_str(" }\n");
2616 code.push_str(" }\n");
2617
2618 let output_key = &config.standard.mapping.output_key;
2619 code.push_str(&format!(
2620 " Ok(NodeOutput::new().with_update(\"{}\", json!(result)))\n",
2621 output_key
2622 ));
2623 code.push_str(" });\n\n");
2624 }
2625 ActionNodeConfig::Switch(config) => {
2626 code.push_str(&format!(
2627 " // Action Node: {} (Switch)\n",
2628 config.standard.name
2629 ));
2630 code.push_str(&format!(" let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |ctx| async move {{\n", node_id, node_id));
2631
2632 let output_key = if config.standard.mapping.output_key.is_empty() {
2633 "branch"
2634 } else {
2635 &config.standard.mapping.output_key
2636 };
2637
2638 match config.evaluation_mode {
2639 crate::codegen::action_nodes::EvaluationMode::FirstMatch => {
2640 code.push_str(" let mut matched_branch: Option<&str> = None;\n");
2642
2643 for condition in &config.conditions {
2644 let field = &condition.field;
2645 let op = &condition.operator;
2646 let port = &condition.output_port;
2647 let value_str = condition
2648 .value
2649 .as_ref()
2650 .map(|v| match v {
2651 serde_json::Value::String(s) => s.clone(),
2652 other => other.to_string(),
2653 })
2654 .unwrap_or_default();
2655
2656 code.push_str(&format!(
2657 " // Condition: {} {} {}\n",
2658 field, op, value_str
2659 ));
2660 code.push_str(" if matched_branch.is_none() {\n");
2661 code.push_str(&format!(
2662 " let field_val = ctx.state.get(\"{}\").and_then(|v| v.as_str()).unwrap_or(\"\");\n",
2663 field
2664 ));
2665
2666 generate_condition_check(&mut code, op, &value_str, port, field);
2667 code.push_str(" }\n");
2668 }
2669
2670 if let Some(default) = &config.default_branch {
2672 code.push_str(&format!(
2673 " let branch = matched_branch.unwrap_or(\"{}\");\n",
2674 default
2675 ));
2676 } else {
2677 code.push_str(
2678 " let branch = matched_branch.unwrap_or(\"default\");\n",
2679 );
2680 }
2681
2682 code.push_str(&format!(
2683 " Ok(NodeOutput::new().with_update(\"{}\", json!(branch)))\n",
2684 output_key
2685 ));
2686 }
2687 crate::codegen::action_nodes::EvaluationMode::AllMatch => {
2688 code.push_str(
2691 " let mut matched_branches: Vec<String> = Vec::new();\n\n",
2692 );
2693
2694 for condition in &config.conditions {
2695 let field = &condition.field;
2696 let op = &condition.operator;
2697 let port = &condition.output_port;
2698 let value_str = condition
2699 .value
2700 .as_ref()
2701 .map(|v| match v {
2702 serde_json::Value::String(s) => s.clone(),
2703 other => other.to_string(),
2704 })
2705 .unwrap_or_default();
2706
2707 code.push_str(&format!(
2708 " // Condition: {} {} {}\n",
2709 field, op, value_str
2710 ));
2711 code.push_str(" {\n");
2712 code.push_str(&format!(
2713 " let field_val = ctx.state.get(\"{}\").and_then(|v| v.as_str()).unwrap_or(\"\");\n",
2714 field
2715 ));
2716
2717 generate_all_match_condition_check(&mut code, op, &value_str, port, field);
2718 code.push_str(" }\n");
2719 }
2720
2721 code.push_str(&format!(
2723 "\n Ok(NodeOutput::new().with_update(\"{}\", json!(matched_branches)))\n",
2724 output_key
2725 ));
2726 }
2727 }
2728
2729 code.push_str(" });\n\n");
2730 }
2731 ActionNodeConfig::Loop(config) => {
2732 code.push_str(&format!(
2733 " // Action Node: {} (Loop)\n",
2734 config.standard.name
2735 ));
2736 code.push_str(&format!(" let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |ctx| async move {{\n", node_id, node_id));
2737 code.push_str(" let mut output = NodeOutput::new();\n");
2738
2739 match config.loop_type {
2740 crate::codegen::action_nodes::LoopType::ForEach => {
2741 let source = config
2742 .for_each
2743 .as_ref()
2744 .map(|f| f.source_array.as_str())
2745 .unwrap_or("items");
2746 let item_var = config
2747 .for_each
2748 .as_ref()
2749 .map(|f| f.item_var.as_str())
2750 .unwrap_or("item");
2751 let index_var = config
2752 .for_each
2753 .as_ref()
2754 .map(|f| f.index_var.as_str())
2755 .unwrap_or("index");
2756 let counter_key = format!("{}_loop_index", node_id);
2757 let done_key = format!("{}_loop_done", node_id);
2758
2759 code.push_str(&format!(" // forEach: iterate over '{}'\n", source));
2760 code.push_str(&format!(" let source_arr = ctx.state.get(\"{}\").and_then(|v| v.as_array()).cloned().unwrap_or_default();\n", source));
2761 code.push_str(&format!(" let idx = ctx.state.get(\"{}\").and_then(|v| v.as_u64()).unwrap_or(0) as usize;\n", counter_key));
2762 code.push_str(" if idx < source_arr.len() {\n");
2763 code.push_str(" let current_item = source_arr[idx].clone();\n");
2764 code.push_str(&format!(
2765 " output = output.with_update(\"{}\", current_item);\n",
2766 item_var
2767 ));
2768 code.push_str(&format!(
2769 " output = output.with_update(\"{}\", json!(idx));\n",
2770 index_var
2771 ));
2772 code.push_str(&format!(
2773 " output = output.with_update(\"{}\", json!(idx + 1));\n",
2774 counter_key
2775 ));
2776 code.push_str(&format!(
2777 " output = output.with_update(\"{}\", json!(false));\n",
2778 done_key
2779 ));
2780
2781 if config.results.collect {
2783 let agg_key = config
2784 .results
2785 .aggregation_key
2786 .as_deref()
2787 .unwrap_or("loop_results");
2788 code.push_str(" // Collect previous iteration result if any\n");
2789 code.push_str(&format!(" let mut results = ctx.state.get(\"{}\").and_then(|v| v.as_array()).cloned().unwrap_or_default();\n", agg_key));
2790 code.push_str(" if idx > 0 {\n");
2791 code.push_str(
2792 " // Capture the response from the previous iteration\n",
2793 );
2794 code.push_str(
2795 " if let Some(resp) = ctx.state.get(\"response\") {\n",
2796 );
2797 code.push_str(" results.push(resp.clone());\n");
2798 code.push_str(" }\n");
2799 code.push_str(" }\n");
2800 code.push_str(&format!(
2801 " output = output.with_update(\"{}\", json!(results));\n",
2802 agg_key
2803 ));
2804 }
2805
2806 code.push_str(" } else {\n");
2807 code.push_str(&format!(
2808 " output = output.with_update(\"{}\", json!(true));\n",
2809 done_key
2810 ));
2811
2812 if config.results.collect {
2814 let agg_key = config
2815 .results
2816 .aggregation_key
2817 .as_deref()
2818 .unwrap_or("loop_results");
2819 code.push_str(&format!(" let mut results = ctx.state.get(\"{}\").and_then(|v| v.as_array()).cloned().unwrap_or_default();\n", agg_key));
2820 code.push_str(
2821 " if let Some(resp) = ctx.state.get(\"response\") {\n",
2822 );
2823 code.push_str(" results.push(resp.clone());\n");
2824 code.push_str(" }\n");
2825 code.push_str(&format!(
2826 " output = output.with_update(\"{}\", json!(results));\n",
2827 agg_key
2828 ));
2829 }
2830
2831 code.push_str(" }\n");
2832 }
2833 crate::codegen::action_nodes::LoopType::Times => {
2834 let count = config
2835 .times
2836 .as_ref()
2837 .map(|t| match &t.count {
2838 serde_json::Value::Number(n) => n.as_u64().unwrap_or(3) as usize,
2839 _ => 3,
2840 })
2841 .unwrap_or(3);
2842 let counter_key = format!("{}_loop_index", node_id);
2843 let done_key = format!("{}_loop_done", node_id);
2844
2845 code.push_str(&format!(" // times: repeat {} times\n", count));
2846 code.push_str(&format!(" let idx = ctx.state.get(\"{}\").and_then(|v| v.as_u64()).unwrap_or(0) as usize;\n", counter_key));
2847 code.push_str(&format!(" if idx < {} {{\n", count));
2848 code.push_str(
2849 " output = output.with_update(\"index\", json!(idx));\n",
2850 );
2851 code.push_str(&format!(
2852 " output = output.with_update(\"{}\", json!(idx + 1));\n",
2853 counter_key
2854 ));
2855 code.push_str(&format!(
2856 " output = output.with_update(\"{}\", json!(false));\n",
2857 done_key
2858 ));
2859
2860 if config.results.collect {
2861 let agg_key = config
2862 .results
2863 .aggregation_key
2864 .as_deref()
2865 .unwrap_or("loop_results");
2866 code.push_str(&format!(" let mut results = ctx.state.get(\"{}\").and_then(|v| v.as_array()).cloned().unwrap_or_default();\n", agg_key));
2867 code.push_str(" if idx > 0 {\n");
2868 code.push_str(
2869 " if let Some(resp) = ctx.state.get(\"response\") {\n",
2870 );
2871 code.push_str(" results.push(resp.clone());\n");
2872 code.push_str(" }\n");
2873 code.push_str(" }\n");
2874 code.push_str(&format!(
2875 " output = output.with_update(\"{}\", json!(results));\n",
2876 agg_key
2877 ));
2878 }
2879
2880 code.push_str(" } else {\n");
2881 code.push_str(&format!(
2882 " output = output.with_update(\"{}\", json!(true));\n",
2883 done_key
2884 ));
2885
2886 if config.results.collect {
2887 let agg_key = config
2888 .results
2889 .aggregation_key
2890 .as_deref()
2891 .unwrap_or("loop_results");
2892 code.push_str(&format!(" let mut results = ctx.state.get(\"{}\").and_then(|v| v.as_array()).cloned().unwrap_or_default();\n", agg_key));
2893 code.push_str(
2894 " if let Some(resp) = ctx.state.get(\"response\") {\n",
2895 );
2896 code.push_str(" results.push(resp.clone());\n");
2897 code.push_str(" }\n");
2898 code.push_str(&format!(
2899 " output = output.with_update(\"{}\", json!(results));\n",
2900 agg_key
2901 ));
2902 }
2903
2904 code.push_str(" }\n");
2905 }
2906 crate::codegen::action_nodes::LoopType::While => {
2907 let condition_field = config
2908 .while_config
2909 .as_ref()
2910 .map(|w| w.condition.as_str())
2911 .unwrap_or("should_continue");
2912 let counter_key = format!("{}_loop_index", node_id);
2913 let done_key = format!("{}_loop_done", node_id);
2914
2915 code.push_str(&format!(
2916 " // while: loop while '{}' is truthy\n",
2917 condition_field
2918 ));
2919 code.push_str(&format!(" let idx = ctx.state.get(\"{}\").and_then(|v| v.as_u64()).unwrap_or(0) as usize;\n", counter_key));
2920 code.push_str(&format!(
2921 " let cond_val = ctx.state.get(\"{}\");\n",
2922 condition_field
2923 ));
2924 code.push_str(" let should_continue = match cond_val {\n");
2925 code.push_str(
2926 " Some(v) if v.is_boolean() => v.as_bool().unwrap_or(false),\n",
2927 );
2928 code.push_str(" Some(v) if v.is_string() => !v.as_str().unwrap_or(\"\").is_empty() && v.as_str() != Some(\"false\"),\n");
2929 code.push_str(" Some(v) if v.is_number() => v.as_f64().unwrap_or(0.0) != 0.0,\n");
2930 code.push_str(" Some(v) if v.is_null() => false,\n");
2931 code.push_str(" Some(_) => true,\n");
2932 code.push_str(" None => false,\n");
2933 code.push_str(" };\n");
2934 code.push_str(" if should_continue {\n");
2935 code.push_str(
2936 " output = output.with_update(\"index\", json!(idx));\n",
2937 );
2938 code.push_str(&format!(
2939 " output = output.with_update(\"{}\", json!(idx + 1));\n",
2940 counter_key
2941 ));
2942 code.push_str(&format!(
2943 " output = output.with_update(\"{}\", json!(false));\n",
2944 done_key
2945 ));
2946 code.push_str(" } else {\n");
2947 code.push_str(&format!(
2948 " output = output.with_update(\"{}\", json!(true));\n",
2949 done_key
2950 ));
2951 code.push_str(" }\n");
2952 }
2953 }
2954
2955 code.push_str(" Ok(output)\n");
2956 code.push_str(" });\n\n");
2957 }
2958 ActionNodeConfig::Merge(config) => {
2959 code.push_str(&format!(
2960 " // Action Node: {} (Merge)\n",
2961 config.standard.name
2962 ));
2963 code.push_str(&format!(" let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |ctx| async move {{\n", node_id, node_id));
2964 code.push_str(" let mut output = NodeOutput::new();\n");
2965
2966 let output_key = if config.standard.mapping.output_key.is_empty() {
2967 "merged"
2968 } else {
2969 &config.standard.mapping.output_key
2970 };
2971
2972 let branch_keys = config.branch_keys.as_deref().unwrap_or(&[]);
2974
2975 match config.combine_strategy {
2976 crate::codegen::action_nodes::CombineStrategy::Array => {
2977 code.push_str(" // Combine strategy: array — collect branch outputs into an array\n");
2978 code.push_str(
2979 " let mut results: Vec<serde_json::Value> = Vec::new();\n",
2980 );
2981 if branch_keys.is_empty() {
2982 code.push_str(" for (k, v) in ctx.state.iter() {\n");
2984 code.push_str(
2985 " if k != \"message\" && k != \"classification\" {\n",
2986 );
2987 code.push_str(" results.push(v.clone());\n");
2988 code.push_str(" }\n");
2989 code.push_str(" }\n");
2990 } else {
2991 for key in branch_keys {
2992 code.push_str(&format!(
2993 " if let Some(v) = ctx.state.get(\"{}\") {{\n",
2994 key
2995 ));
2996 code.push_str(" results.push(v.clone());\n");
2997 code.push_str(" }\n");
2998 }
2999 }
3000 code.push_str(&format!(
3001 " output = output.with_update(\"{}\", json!(results));\n",
3002 output_key
3003 ));
3004 }
3005 crate::codegen::action_nodes::CombineStrategy::Object => {
3006 code.push_str(" // Combine strategy: object — merge branch outputs into an object\n");
3007 code.push_str(" let mut merged = serde_json::Map::new();\n");
3008 if branch_keys.is_empty() {
3009 code.push_str(" for (k, v) in ctx.state.iter() {\n");
3010 code.push_str(
3011 " if k != \"message\" && k != \"classification\" {\n",
3012 );
3013 code.push_str(" merged.insert(k.clone(), v.clone());\n");
3014 code.push_str(" }\n");
3015 code.push_str(" }\n");
3016 } else {
3017 for key in branch_keys {
3018 code.push_str(&format!(
3019 " if let Some(v) = ctx.state.get(\"{}\") {{\n",
3020 key
3021 ));
3022 code.push_str(&format!(
3023 " merged.insert(\"{}\".to_string(), v.clone());\n",
3024 key
3025 ));
3026 code.push_str(" }\n");
3027 }
3028 }
3029 code.push_str(&format!(
3030 " output = output.with_update(\"{}\", json!(merged));\n",
3031 output_key
3032 ));
3033 }
3034 crate::codegen::action_nodes::CombineStrategy::First => {
3035 code.push_str(
3036 " // Combine strategy: first — use first available branch output\n",
3037 );
3038 if branch_keys.is_empty() {
3039 code.push_str(" let first_val = ctx.state.get(\"response\").cloned().unwrap_or(json!(null));\n");
3040 } else {
3041 code.push_str(" let first_val = None\n");
3042 for key in branch_keys {
3043 code.push_str(&format!(
3044 " .or_else(|| ctx.state.get(\"{}\").cloned())\n",
3045 key
3046 ));
3047 }
3048 code.push_str(" .unwrap_or(json!(null));\n");
3049 }
3050 code.push_str(&format!(
3051 " output = output.with_update(\"{}\", first_val);\n",
3052 output_key
3053 ));
3054 }
3055 crate::codegen::action_nodes::CombineStrategy::Last => {
3056 code.push_str(
3057 " // Combine strategy: last — use last available branch output\n",
3058 );
3059 if branch_keys.is_empty() {
3060 code.push_str(" let last_val = ctx.state.get(\"response\").cloned().unwrap_or(json!(null));\n");
3061 } else {
3062 code.push_str(" let last_val = None\n");
3063 for key in branch_keys.iter().rev() {
3064 code.push_str(&format!(
3065 " .or_else(|| ctx.state.get(\"{}\").cloned())\n",
3066 key
3067 ));
3068 }
3069 code.push_str(" .unwrap_or(json!(null));\n");
3070 }
3071 code.push_str(&format!(
3072 " output = output.with_update(\"{}\", last_val);\n",
3073 output_key
3074 ));
3075 }
3076 }
3077
3078 code.push_str(" Ok(output)\n");
3079 code.push_str(" });\n\n");
3080 }
3081 ActionNodeConfig::Http(config) => {
3082 code.push_str(&format!(
3083 " // Action Node: {} (HTTP)\n",
3084 config.standard.name
3085 ));
3086 code.push_str(&format!(" let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |ctx| async move {{\n", node_id, node_id));
3087
3088 let output_key = if config.standard.mapping.output_key.is_empty() {
3089 "httpResult"
3090 } else {
3091 &config.standard.mapping.output_key
3092 };
3093
3094 let raw_url = &config.url;
3096 let url = if let Some(pred_id) = parallel_predecessor {
3097 raw_url.replace("{{response}}", &format!("{{{{{}_response}}}}", pred_id))
3098 } else {
3099 raw_url.to_string()
3100 };
3101
3102 code.push_str(&format!(
3103 " let mut url = \"{}\".to_string();\n",
3104 url.replace('"', "\\\"")
3105 ));
3106 code.push_str(" for (k, v) in ctx.state.iter() {\n");
3107 code.push_str(" let pattern = format!(\"{{{{{}}}}}\", k);\n");
3108 code.push_str(" if let Some(s) = v.as_str() {\n");
3109 code.push_str(" url = url.replace(&pattern, s);\n");
3110 code.push_str(" } else {\n");
3111 code.push_str(" url = url.replace(&pattern, &v.to_string());\n");
3112 code.push_str(" }\n");
3113 code.push_str(" }\n\n");
3114
3115 let method_fn = match config.method {
3117 crate::codegen::action_node_types::HttpMethod::Get => "get",
3118 crate::codegen::action_node_types::HttpMethod::Post => "post",
3119 crate::codegen::action_node_types::HttpMethod::Put => "put",
3120 crate::codegen::action_node_types::HttpMethod::Patch => "patch",
3121 crate::codegen::action_node_types::HttpMethod::Delete => "delete",
3122 };
3123
3124 code.push_str(" let client = reqwest::Client::new();\n");
3125
3126 let needs_mut = !config.headers.is_empty()
3128 || config.auth.auth_type != "none"
3129 || config.body.body_type != "none";
3130 if needs_mut {
3131 code.push_str(&format!(
3132 " let mut req = client.{}(&url);\n\n",
3133 method_fn
3134 ));
3135 } else {
3136 code.push_str(&format!(
3137 " let req = client.{}(&url);\n\n",
3138 method_fn
3139 ));
3140 }
3141
3142 for (key, value) in &config.headers {
3144 let val = if let Some(pred_id) = parallel_predecessor {
3145 value.replace("{{response}}", &format!("{{{{{}_response}}}}", pred_id))
3146 } else {
3147 value.to_string()
3148 };
3149 if val.contains("{{") {
3150 code.push_str(&format!(
3152 " let mut hdr_val = \"{}\".to_string();\n",
3153 val.replace('"', "\\\"")
3154 ));
3155 code.push_str(" for (k, v) in ctx.state.iter() {\n");
3156 code.push_str(" let pattern = format!(\"{{{{{}}}}}\", k);\n");
3157 code.push_str(" if let Some(s) = v.as_str() {\n");
3158 code.push_str(" hdr_val = hdr_val.replace(&pattern, s);\n");
3159 code.push_str(" } else {\n");
3160 code.push_str(
3161 " hdr_val = hdr_val.replace(&pattern, &v.to_string());\n",
3162 );
3163 code.push_str(" }\n");
3164 code.push_str(" }\n");
3165 code.push_str(&format!(
3166 " req = req.header(\"{}\", hdr_val);\n",
3167 key.replace('"', "\\\"")
3168 ));
3169 } else {
3170 code.push_str(&format!(
3171 " req = req.header(\"{}\", \"{}\");\n",
3172 key.replace('"', "\\\""),
3173 val.replace('"', "\\\"")
3174 ));
3175 }
3176 }
3177
3178 match config.auth.auth_type.as_str() {
3180 "bearer" => {
3181 if let Some(bearer) = &config.auth.bearer {
3182 let token = &bearer.token;
3183 if token.contains("{{") {
3184 code.push_str(&format!(
3185 " let mut bearer_token = \"{}\".to_string();\n",
3186 token.replace('"', "\\\"")
3187 ));
3188 code.push_str(" for (k, v) in ctx.state.iter() {\n");
3189 code.push_str(
3190 " let pattern = format!(\"{{{{{}}}}}\", k);\n",
3191 );
3192 code.push_str(" if let Some(s) = v.as_str() {\n");
3193 code.push_str(" bearer_token = bearer_token.replace(&pattern, s);\n");
3194 code.push_str(" }\n");
3195 code.push_str(" }\n");
3196 code.push_str(" req = req.bearer_auth(&bearer_token);\n");
3197 } else {
3198 code.push_str(&format!(
3199 " req = req.bearer_auth(\"{}\");\n",
3200 token.replace('"', "\\\"")
3201 ));
3202 }
3203 }
3204 }
3205 "basic" => {
3206 if let Some(basic) = &config.auth.basic {
3207 code.push_str(&format!(
3208 " req = req.basic_auth(\"{}\", Some(\"{}\"));\n",
3209 basic.username.replace('"', "\\\""),
3210 basic.password.replace('"', "\\\"")
3211 ));
3212 }
3213 }
3214 "api_key" => {
3215 if let Some(api_key) = &config.auth.api_key {
3216 let val = &api_key.value;
3217 if val.contains("{{") {
3218 code.push_str(&format!(
3219 " let mut api_key_val = \"{}\".to_string();\n",
3220 val.replace('"', "\\\"")
3221 ));
3222 code.push_str(" for (k, v) in ctx.state.iter() {\n");
3223 code.push_str(
3224 " let pattern = format!(\"{{{{{}}}}}\", k);\n",
3225 );
3226 code.push_str(" if let Some(s) = v.as_str() {\n");
3227 code.push_str(
3228 " api_key_val = api_key_val.replace(&pattern, s);\n",
3229 );
3230 code.push_str(" }\n");
3231 code.push_str(" }\n");
3232 code.push_str(&format!(
3233 " req = req.header(\"{}\", api_key_val);\n",
3234 api_key.header_name.replace('"', "\\\"")
3235 ));
3236 } else {
3237 code.push_str(&format!(
3238 " req = req.header(\"{}\", \"{}\");\n",
3239 api_key.header_name.replace('"', "\\\""),
3240 val.replace('"', "\\\"")
3241 ));
3242 }
3243 }
3244 }
3245 _ => {} }
3247
3248 match config.body.body_type.as_str() {
3250 "json" => {
3251 if let Some(content) = &config.body.content {
3252 let body_str = content.to_string();
3253 if body_str.contains("{{") {
3254 code.push_str(&format!(
3256 " let mut body_str = r#\"{}\"#.to_string();\n",
3257 body_str.replace('"', "\\\"")
3258 ));
3259 code.push_str(" for (k, v) in ctx.state.iter() {\n");
3260 code.push_str(
3261 " let pattern = format!(\"{{{{{}}}}}\", k);\n",
3262 );
3263 code.push_str(" if let Some(s) = v.as_str() {\n");
3264 code.push_str(
3265 " body_str = body_str.replace(&pattern, s);\n",
3266 );
3267 code.push_str(" } else {\n");
3268 code.push_str(" body_str = body_str.replace(&pattern, &v.to_string());\n");
3269 code.push_str(" }\n");
3270 code.push_str(" }\n");
3271 code.push_str(" let body_json: serde_json::Value = serde_json::from_str(&body_str).unwrap_or(json!(body_str));\n");
3272 code.push_str(" req = req.json(&body_json);\n");
3273 } else {
3274 code.push_str(&format!(
3275 " req = req.json(&json!({}));\n",
3276 body_str
3277 ));
3278 }
3279 }
3280 }
3281 "form" => {
3282 if let Some(content) = &config.body.content {
3283 code.push_str(&format!(" req = req.form(&json!({}));\n", content));
3284 }
3285 }
3286 "raw" => {
3287 if let Some(content) = &config.body.content {
3288 let raw = content
3289 .as_str()
3290 .map(|s| s.to_string())
3291 .unwrap_or_else(|| content.to_string());
3292 code.push_str(&format!(
3293 " req = req.body(\"{}\");\n",
3294 raw.replace('"', "\\\"")
3295 ));
3296 }
3297 }
3298 _ => {} }
3300
3301 code.push_str("\n let resp = req.send().await;\n");
3303 code.push_str(" match resp {\n");
3304 code.push_str(" Ok(response) => {\n");
3305 code.push_str(" let status = response.status().as_u16();\n");
3306
3307 if let Some(validation) = &config.response.status_validation {
3309 if validation.contains('-') {
3310 let parts: Vec<&str> = validation.split('-').collect();
3311 if parts.len() == 2 {
3312 code.push_str(&format!(
3313 " if status < {} || status > {} {{\n",
3314 parts[0].trim(),
3315 parts[1].trim()
3316 ));
3317 code.push_str(" let body = response.text().await.unwrap_or_default();\n");
3318 code.push_str(&format!(
3319 " return Ok(NodeOutput::new().with_update(\"{}\", json!({{\n",
3320 output_key
3321 ));
3322 code.push_str(" \"error\": true, \"status\": status, \"body\": body\n");
3323 code.push_str(" })));\n");
3324 code.push_str(" }\n");
3325 }
3326 }
3327 }
3328
3329 match config.response.response_type.as_str() {
3331 "text" => {
3332 code.push_str(
3333 " let body = response.text().await.unwrap_or_default();\n",
3334 );
3335 if let Some(json_path) = &config.response.json_path {
3336 code.push_str(" let parsed: serde_json::Value = serde_json::from_str(&body).unwrap_or(json!(body));\n");
3338 code.push_str(&format!(
3339 " // JSONPath extraction: {}\n",
3340 json_path
3341 ));
3342 generate_json_path_extraction(&mut code, json_path, output_key);
3343 } else {
3344 code.push_str(&format!(
3345 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"status\": status, \"body\": body }})))\n",
3346 output_key
3347 ));
3348 }
3349 }
3350 _ => {
3351 code.push_str(
3353 " let text = response.text().await.unwrap_or_default();\n",
3354 );
3355 code.push_str(" let body: serde_json::Value = serde_json::from_str(&text).unwrap_or_else(|_| json!(text));\n");
3356 if let Some(json_path) = &config.response.json_path {
3357 code.push_str(&format!(
3358 " // JSONPath extraction: {}\n",
3359 json_path
3360 ));
3361 generate_json_path_extraction(&mut code, json_path, output_key);
3362 } else {
3363 code.push_str(&format!(
3364 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"status\": status, \"body\": body }})))\n",
3365 output_key
3366 ));
3367 }
3368 }
3369 }
3370
3371 code.push_str(" }\n");
3372 code.push_str(" Err(e) => {\n");
3373 code.push_str(&format!(
3374 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": e.to_string() }})))\n",
3375 output_key
3376 ));
3377 code.push_str(" }\n");
3378 code.push_str(" }\n");
3379 code.push_str(" });\n\n");
3380 }
3381 ActionNodeConfig::Wait(config) => {
3382 code.push_str(&format!(
3383 " // Action Node: {} (Wait)\n",
3384 config.standard.name
3385 ));
3386 code.push_str(&format!(" let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |_ctx| async move {{\n", node_id, node_id));
3387
3388 let output_key = if config.standard.mapping.output_key.is_empty() {
3389 "wait_result"
3390 } else {
3391 &config.standard.mapping.output_key
3392 };
3393
3394 match config.wait_type {
3395 crate::codegen::action_nodes::WaitType::Fixed => {
3396 if let Some(fixed) = &config.fixed {
3397 let ms = match fixed.unit.as_str() {
3398 "ms" => fixed.duration,
3399 "s" => fixed.duration * 1000,
3400 "m" => fixed.duration * 60 * 1000,
3401 "h" => fixed.duration * 60 * 60 * 1000,
3402 _ => fixed.duration,
3403 };
3404 code.push_str(&format!(
3405 " // Fixed wait: {} {}\n",
3406 fixed.duration, fixed.unit
3407 ));
3408 code.push_str(&format!(" tokio::time::sleep(std::time::Duration::from_millis({})).await;\n", ms));
3409 code.push_str(&format!(" Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"waited\": true, \"duration_ms\": {} }})))\n", output_key, ms));
3410 } else {
3411 code.push_str(" // Fixed wait: default 1s\n");
3413 code.push_str(" tokio::time::sleep(std::time::Duration::from_millis(1000)).await;\n");
3414 code.push_str(&format!(" Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"waited\": true, \"duration_ms\": 1000 }})))\n", output_key));
3415 }
3416 }
3417 crate::codegen::action_nodes::WaitType::Until => {
3418 if let Some(until) = &config.until {
3419 code.push_str(" // Wait until timestamp\n");
3420 code.push_str(&format!(
3421 " let target = chrono::DateTime::parse_from_rfc3339(\"{}\")\n",
3422 until.timestamp
3423 ));
3424 code.push_str(
3425 " .unwrap_or_else(|_| chrono::Utc::now().fixed_offset());\n",
3426 );
3427 code.push_str(" let now = chrono::Utc::now();\n");
3428 code.push_str(" if target > now {\n");
3429 code.push_str(" let duration = (target - now).to_std().unwrap_or_default();\n");
3430 code.push_str(" tokio::time::sleep(duration).await;\n");
3431 code.push_str(" }\n");
3432 code.push_str(&format!(" Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"waited\": true }})))\n", output_key));
3433 } else {
3434 code.push_str(&format!(" Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"waited\": false, \"reason\": \"no timestamp configured\" }})))\n", output_key));
3435 }
3436 }
3437 crate::codegen::action_nodes::WaitType::Condition => {
3438 if let Some(condition) = &config.condition {
3439 code.push_str(" // Poll until condition is met\n");
3440 code.push_str(&format!(
3441 " let poll_interval = std::time::Duration::from_millis({});\n",
3442 condition.poll_interval
3443 ));
3444 code.push_str(&format!(
3445 " let max_wait = std::time::Duration::from_millis({});\n",
3446 condition.max_wait
3447 ));
3448 code.push_str(" let start = std::time::Instant::now();\n");
3449 code.push_str(" loop {\n");
3450 code.push_str(" if start.elapsed() >= max_wait {\n");
3451 code.push_str(&format!(" break Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"waited\": true, \"timed_out\": true }})));\n", output_key));
3452 code.push_str(" }\n");
3453 code.push_str(" tokio::time::sleep(poll_interval).await;\n");
3454 code.push_str(" }\n");
3455 } else {
3456 code.push_str(&format!(" Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"waited\": false }})))\n", output_key));
3457 }
3458 }
3459 crate::codegen::action_nodes::WaitType::Webhook => {
3460 if let Some(webhook) = &config.webhook {
3462 code.push_str(" // Webhook wait (simplified): sleep for timeout as placeholder\n");
3463 code.push_str(&format!(" tokio::time::sleep(std::time::Duration::from_millis({})).await;\n", webhook.timeout.min(30000)));
3464 code.push_str(&format!(" Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"waited\": true, \"webhook\": \"{}\" }})))\n", output_key, webhook.path.replace('"', "\\\"")));
3465 } else {
3466 code.push_str(&format!(" Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"waited\": false }})))\n", output_key));
3467 }
3468 }
3469 }
3470
3471 code.push_str(" });\n\n");
3472 }
3473 ActionNodeConfig::Database(config) => {
3474 code.push_str(&format!(
3475 " // Action Node: {} (Database - {:?})\n",
3476 config.standard.name, config.db_type
3477 ));
3478 code.push_str(&format!(" let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |ctx| async move {{\n", node_id, node_id));
3479
3480 let output_key = if config.standard.mapping.output_key.is_empty() {
3481 "dbResult"
3482 } else {
3483 &config.standard.mapping.output_key
3484 };
3485
3486 let conn_str = &config.connection.connection_string;
3488 if conn_str.contains("{{") {
3489 code.push_str(&format!(
3490 " let mut conn_str = \"{}\".to_string();\n",
3491 conn_str.replace('"', "\\\"")
3492 ));
3493 code.push_str(" for (k, v) in ctx.state.iter() {\n");
3494 code.push_str(" let pattern = format!(\"{{{{{}}}}}\", k);\n");
3495 code.push_str(" if let Some(s) = v.as_str() {\n");
3496 code.push_str(" conn_str = conn_str.replace(&pattern, s);\n");
3497 code.push_str(" } else {\n");
3498 code.push_str(
3499 " conn_str = conn_str.replace(&pattern, &v.to_string());\n",
3500 );
3501 code.push_str(" }\n");
3502 code.push_str(" }\n");
3503 } else {
3504 code.push_str(&format!(
3505 " let conn_str = \"{}\".to_string();\n",
3506 conn_str.replace('"', "\\\"")
3507 ));
3508 }
3509
3510 if let Some(cred_ref) = &config.connection.credential_ref {
3512 if !cred_ref.is_empty() {
3513 code.push_str(&format!(
3514 " let conn_str = ctx.state.get(\"{}\").and_then(|v| v.as_str()).map(|s| s.to_string()).unwrap_or(conn_str);\n",
3515 cred_ref
3516 ));
3517 }
3518 }
3519
3520 match config.db_type {
3521 crate::codegen::action_node_types::DatabaseType::Postgresql
3522 | crate::codegen::action_node_types::DatabaseType::Mysql
3523 | crate::codegen::action_node_types::DatabaseType::Sqlite => {
3524 if let Some(sql) = &config.sql {
3526 let query = &sql.query;
3527 if query.contains("{{") {
3529 code.push_str(&format!(
3530 " let mut query_str = \"{}\".to_string();\n",
3531 query.replace('"', "\\\"")
3532 ));
3533 code.push_str(" for (k, v) in ctx.state.iter() {\n");
3534 code.push_str(
3535 " let pattern = format!(\"{{{{{}}}}}\", k);\n",
3536 );
3537 code.push_str(" if let Some(s) = v.as_str() {\n");
3538 code.push_str(
3539 " query_str = query_str.replace(&pattern, s);\n",
3540 );
3541 code.push_str(" } else {\n");
3542 code.push_str(" query_str = query_str.replace(&pattern, &v.to_string());\n");
3543 code.push_str(" }\n");
3544 code.push_str(" }\n");
3545 } else {
3546 code.push_str(&format!(
3547 " let query_str = \"{}\".to_string();\n",
3548 query.replace('"', "\\\"")
3549 ));
3550 }
3551
3552 match config.db_type {
3553 crate::codegen::action_node_types::DatabaseType::Sqlite => {
3554 code.push_str(" let pool = sqlx::SqlitePool::connect(&conn_str).await\n");
3555 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"SQLite connection failed: {}\", e) })?;\n");
3556 }
3557 crate::codegen::action_node_types::DatabaseType::Mysql => {
3558 code.push_str(" let pool = sqlx::MySqlPool::connect(&conn_str).await\n");
3559 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"MySQL connection failed: {}\", e) })?;\n");
3560 }
3561 _ => {
3562 code.push_str(
3564 " let pool = sqlx::PgPool::connect(&conn_str).await\n",
3565 );
3566 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"PostgreSQL connection failed: {}\", e) })?;\n");
3567 }
3568 }
3569
3570 match sql.operation.as_str() {
3571 "query" => {
3572 code.push_str(" use sqlx::Row;\n");
3575 code.push_str(" use sqlx::Column;\n");
3576 code.push_str(" let raw_rows = sqlx::query(&query_str)\n");
3577 code.push_str(" .fetch_all(&pool).await\n");
3578 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Query failed: {}\", e) })?;\n");
3579 code.push_str(" let rows: Vec<serde_json::Value> = raw_rows.iter().map(|row| {\n");
3580 code.push_str(
3581 " let mut obj = serde_json::Map::new();\n",
3582 );
3583 code.push_str(" for col in row.columns() {\n");
3584 code.push_str(
3585 " let name = col.name().to_string();\n",
3586 );
3587 code.push_str(" let val: serde_json::Value = row.try_get::<String, _>(col.name())\n");
3588 code.push_str(" .map(|s| json!(s))\n");
3589 code.push_str(" .or_else(|_| row.try_get::<i64, _>(col.name()).map(|n| json!(n)))\n");
3590 code.push_str(" .or_else(|_| row.try_get::<f64, _>(col.name()).map(|n| json!(n)))\n");
3591 code.push_str(" .or_else(|_| row.try_get::<bool, _>(col.name()).map(|b| json!(b)))\n");
3592 code.push_str(" .unwrap_or(json!(null));\n");
3593 code.push_str(" obj.insert(name, val);\n");
3594 code.push_str(" }\n");
3595 code.push_str(" serde_json::Value::Object(obj)\n");
3596 code.push_str(" }).collect();\n");
3597 code.push_str(" let row_count = rows.len();\n");
3598 code.push_str(&format!(
3599 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"rows\": rows, \"count\": row_count }})))\n",
3600 output_key
3601 ));
3602 }
3603 _ => {
3604 code.push_str(" let result = sqlx::query(&query_str)\n");
3606 code.push_str(" .execute(&pool).await\n");
3607 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Query failed: {}\", e) })?;\n");
3608 code.push_str(&format!(
3609 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"affected\": result.rows_affected(), \"operation\": \"{}\" }})))\n",
3610 output_key, sql.operation
3611 ));
3612 }
3613 }
3614 } else {
3615 code.push_str(&format!(
3616 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": \"No SQL query configured\" }})))\n",
3617 output_key
3618 ));
3619 }
3620 }
3621 crate::codegen::action_node_types::DatabaseType::Mongodb => {
3622 if let Some(mongo) = &config.mongodb {
3623 code.push_str(
3624 " let client = mongodb::Client::with_uri_str(&conn_str).await\n",
3625 );
3626 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"MongoDB connection failed: {}\", e) })?;\n");
3627 code.push_str(" let db_name = conn_str.rsplit('/').next()\n");
3629 code.push_str(" .and_then(|s| s.split('?').next())\n");
3630 code.push_str(" .filter(|s| !s.is_empty())\n");
3631 code.push_str(" .unwrap_or(\"test\");\n");
3632 code.push_str(" let db = client.database(db_name);\n");
3633 code.push_str(&format!(
3634 " let collection = db.collection::<mongodb::bson::Document>(\"{}\");\n",
3635 mongo.collection.replace('"', "\\\"")
3636 ));
3637
3638 let filter_str = mongo
3639 .filter
3640 .as_ref()
3641 .map(|f| f.to_string())
3642 .unwrap_or_else(|| "{}".to_string());
3643
3644 match mongo.operation.as_str() {
3645 "find" => {
3646 code.push_str(&format!(
3647 " let filter_json: serde_json::Value = serde_json::from_str(r#\"{}\"#).unwrap_or(json!({{}}));\n",
3648 filter_str.replace('"', "\\\"")
3649 ));
3650 code.push_str(" let filter_doc = mongodb::bson::to_document(&filter_json).unwrap_or_default();\n");
3651 code.push_str(" use futures::TryStreamExt;\n");
3652 code.push_str(
3653 " let mut cursor = collection.find(filter_doc).await\n",
3654 );
3655 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"MongoDB find failed: {}\", e) })?;\n");
3656 code.push_str(
3657 " let mut docs: Vec<serde_json::Value> = Vec::new();\n",
3658 );
3659 code.push_str(
3660 " while let Some(doc) = cursor.try_next().await\n",
3661 );
3662 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Cursor error: {}\", e) })? {\n");
3663 code.push_str(" if let Ok(json) = mongodb::bson::from_document::<serde_json::Value>(doc) {\n");
3664 code.push_str(" docs.push(json);\n");
3665 code.push_str(" }\n");
3666 code.push_str(" }\n");
3667 code.push_str(&format!(
3668 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"docs\": docs, \"count\": docs.len() }})))\n",
3669 output_key
3670 ));
3671 }
3672 "findOne" => {
3673 code.push_str(&format!(
3674 " let filter_json: serde_json::Value = serde_json::from_str(r#\"{}\"#).unwrap_or(json!({{}}));\n",
3675 filter_str.replace('"', "\\\"")
3676 ));
3677 code.push_str(" let filter_doc = mongodb::bson::to_document(&filter_json).unwrap_or_default();\n");
3678 code.push_str(
3679 " let result = collection.find_one(filter_doc).await\n",
3680 );
3681 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"MongoDB findOne failed: {}\", e) })?;\n");
3682 code.push_str(" let doc_json = result.map(|d| mongodb::bson::from_document::<serde_json::Value>(d).unwrap_or(json!(null))).unwrap_or(json!(null));\n");
3683 code.push_str(&format!(
3684 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"doc\": doc_json }})))\n",
3685 output_key
3686 ));
3687 }
3688 "insert" => {
3689 let doc_str = mongo
3690 .document
3691 .as_ref()
3692 .map(|d| d.to_string())
3693 .unwrap_or_else(|| "{}".to_string());
3694 code.push_str(&format!(
3695 " let doc_json: serde_json::Value = serde_json::from_str(r#\"{}\"#).unwrap_or(json!({{}}));\n",
3696 doc_str.replace('"', "\\\"")
3697 ));
3698 code.push_str(" let doc = mongodb::bson::to_document(&doc_json).unwrap_or_default();\n");
3699 code.push_str(
3700 " let result = collection.insert_one(doc).await\n",
3701 );
3702 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"MongoDB insert failed: {}\", e) })?;\n");
3703 code.push_str(&format!(
3704 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"inserted_id\": result.inserted_id.to_string() }})))\n",
3705 output_key
3706 ));
3707 }
3708 "update" => {
3709 code.push_str(&format!(
3710 " let filter_json: serde_json::Value = serde_json::from_str(r#\"{}\"#).unwrap_or(json!({{}}));\n",
3711 filter_str.replace('"', "\\\"")
3712 ));
3713 code.push_str(" let filter_doc = mongodb::bson::to_document(&filter_json).unwrap_or_default();\n");
3714 let doc_str = mongo
3715 .document
3716 .as_ref()
3717 .map(|d| d.to_string())
3718 .unwrap_or_else(|| "{}".to_string());
3719 code.push_str(&format!(
3720 " let update_json: serde_json::Value = serde_json::from_str(r#\"{}\"#).unwrap_or(json!({{}}));\n",
3721 doc_str.replace('"', "\\\"")
3722 ));
3723 code.push_str(" let update_doc = mongodb::bson::to_document(&update_json).unwrap_or_default();\n");
3724 code.push_str(" let result = collection.update_many(filter_doc, update_doc).await\n");
3725 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"MongoDB update failed: {}\", e) })?;\n");
3726 code.push_str(&format!(
3727 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"matched\": result.matched_count, \"modified\": result.modified_count }})))\n",
3728 output_key
3729 ));
3730 }
3731 "delete" => {
3732 code.push_str(&format!(
3733 " let filter_json: serde_json::Value = serde_json::from_str(r#\"{}\"#).unwrap_or(json!({{}}));\n",
3734 filter_str.replace('"', "\\\"")
3735 ));
3736 code.push_str(" let filter_doc = mongodb::bson::to_document(&filter_json).unwrap_or_default();\n");
3737 code.push_str(" let result = collection.delete_many(filter_doc).await\n");
3738 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"MongoDB delete failed: {}\", e) })?;\n");
3739 code.push_str(&format!(
3740 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"deleted\": result.deleted_count }})))\n",
3741 output_key
3742 ));
3743 }
3744 _ => {
3745 code.push_str(&format!(
3746 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": \"Unknown MongoDB operation\" }})))\n",
3747 output_key
3748 ));
3749 }
3750 }
3751 } else {
3752 code.push_str(&format!(
3753 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": \"No MongoDB config\" }})))\n",
3754 output_key
3755 ));
3756 }
3757 }
3758 crate::codegen::action_node_types::DatabaseType::Redis => {
3759 if let Some(redis_cfg) = &config.redis {
3760 code.push_str(
3761 " let client = redis::Client::open(conn_str.as_str())\n",
3762 );
3763 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Redis connection failed: {}\", e) })?;\n");
3764 code.push_str(" let mut con = client.get_multiplexed_async_connection().await\n");
3765 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Redis connect failed: {}\", e) })?;\n");
3766
3767 let key = &redis_cfg.key;
3769 if key.contains("{{") {
3770 code.push_str(&format!(
3771 " let mut redis_key = \"{}\".to_string();\n",
3772 key.replace('"', "\\\"")
3773 ));
3774 code.push_str(" for (k, v) in ctx.state.iter() {\n");
3775 code.push_str(
3776 " let pattern = format!(\"{{{{{}}}}}\", k);\n",
3777 );
3778 code.push_str(" if let Some(s) = v.as_str() {\n");
3779 code.push_str(
3780 " redis_key = redis_key.replace(&pattern, s);\n",
3781 );
3782 code.push_str(" }\n");
3783 code.push_str(" }\n");
3784 } else {
3785 code.push_str(&format!(
3786 " let redis_key = \"{}\".to_string();\n",
3787 key.replace('"', "\\\"")
3788 ));
3789 }
3790
3791 code.push_str(" use redis::AsyncCommands;\n");
3792
3793 match redis_cfg.operation.as_str() {
3794 "get" => {
3795 code.push_str(
3796 " let val: Option<String> = con.get(&redis_key).await\n",
3797 );
3798 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Redis GET failed: {}\", e) })?;\n");
3799 code.push_str(&format!(
3800 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"value\": val }})))\n",
3801 output_key
3802 ));
3803 }
3804 "set" => {
3805 let val_str = redis_cfg
3806 .value
3807 .as_ref()
3808 .map(|v| match v {
3809 serde_json::Value::String(s) => s.clone(),
3810 other => other.to_string(),
3811 })
3812 .unwrap_or_default();
3813 code.push_str(&format!(
3814 " let val = \"{}\";\n",
3815 val_str.replace('"', "\\\"")
3816 ));
3817 if let Some(ttl) = redis_cfg.ttl {
3818 code.push_str(&format!(
3819 " let _: () = con.set_ex(&redis_key, val, {}).await\n",
3820 ttl
3821 ));
3822 } else {
3823 code.push_str(
3824 " let _: () = con.set(&redis_key, val).await\n",
3825 );
3826 }
3827 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Redis SET failed: {}\", e) })?;\n");
3828 code.push_str(&format!(
3829 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"set\": true, \"key\": redis_key }})))\n",
3830 output_key
3831 ));
3832 }
3833 "del" => {
3834 code.push_str(
3835 " let deleted: i64 = con.del(&redis_key).await\n",
3836 );
3837 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Redis DEL failed: {}\", e) })?;\n");
3838 code.push_str(&format!(
3839 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"deleted\": deleted }})))\n",
3840 output_key
3841 ));
3842 }
3843 "hget" => {
3844 let field = redis_cfg
3846 .value
3847 .as_ref()
3848 .and_then(|v| v.as_str())
3849 .unwrap_or("field");
3850 code.push_str(&format!(
3851 " let val: Option<String> = con.hget(&redis_key, \"{}\").await\n",
3852 field.replace('"', "\\\"")
3853 ));
3854 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Redis HGET failed: {}\", e) })?;\n");
3855 code.push_str(&format!(
3856 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"value\": val }})))\n",
3857 output_key
3858 ));
3859 }
3860 "hset" => {
3861 let val_str = redis_cfg
3862 .value
3863 .as_ref()
3864 .map(|v| v.to_string())
3865 .unwrap_or_else(|| "{}".to_string());
3866 code.push_str(&format!(
3867 " let fields: serde_json::Value = serde_json::from_str(r#\"{}\"#).unwrap_or(json!({{}}));\n",
3868 val_str.replace('"', "\\\"")
3869 ));
3870 code.push_str(" if let Some(obj) = fields.as_object() {\n");
3871 code.push_str(" for (field, val) in obj {\n");
3872 code.push_str(" let val_str = val.as_str().map(|s| s.to_string()).unwrap_or_else(|| val.to_string());\n");
3873 code.push_str(" let _: () = con.hset(&redis_key, field, &val_str).await\n");
3874 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Redis HSET failed: {}\", e) })?;\n");
3875 code.push_str(" }\n");
3876 code.push_str(" }\n");
3877 code.push_str(&format!(
3878 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"set\": true, \"key\": redis_key }})))\n",
3879 output_key
3880 ));
3881 }
3882 "lpush" => {
3883 let val_str = redis_cfg
3884 .value
3885 .as_ref()
3886 .map(|v| match v {
3887 serde_json::Value::String(s) => s.clone(),
3888 other => other.to_string(),
3889 })
3890 .unwrap_or_default();
3891 code.push_str(&format!(
3892 " let len: i64 = con.lpush(&redis_key, \"{}\").await\n",
3893 val_str.replace('"', "\\\"")
3894 ));
3895 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Redis LPUSH failed: {}\", e) })?;\n");
3896 code.push_str(&format!(
3897 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"length\": len }})))\n",
3898 output_key
3899 ));
3900 }
3901 "rpop" => {
3902 code.push_str(" let val: Option<String> = con.rpop(&redis_key, None).await\n");
3903 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"db\".into(), message: format!(\"Redis RPOP failed: {}\", e) })?;\n");
3904 code.push_str(&format!(
3905 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"value\": val }})))\n",
3906 output_key
3907 ));
3908 }
3909 _ => {
3910 code.push_str(&format!(
3911 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": \"Unknown Redis operation\" }})))\n",
3912 output_key
3913 ));
3914 }
3915 }
3916 } else {
3917 code.push_str(&format!(
3918 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": \"No Redis config\" }})))\n",
3919 output_key
3920 ));
3921 }
3922 }
3923 }
3924
3925 code.push_str(" });\n\n");
3926 }
3927 ActionNodeConfig::Email(config) => {
3928 code.push_str(&format!(
3929 " // Action Node: {} (Email - {:?})\n",
3930 config.standard.name, config.mode
3931 ));
3932 code.push_str(&format!(" let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |ctx| async move {{\n", node_id, node_id));
3933
3934 let output_key = if config.standard.mapping.output_key.is_empty() {
3935 "emailResult"
3936 } else {
3937 &config.standard.mapping.output_key
3938 };
3939
3940 match config.mode {
3941 crate::codegen::action_node_types::EmailMode::Send => {
3942 if let (Some(smtp), Some(recipients), Some(content)) =
3943 (&config.smtp, &config.recipients, &config.content)
3944 {
3945 code.push_str(
3947 " // Build email content with variable interpolation\n",
3948 );
3949
3950 let subject = &content.subject;
3952 if subject.contains("{{") {
3953 code.push_str(&format!(
3954 " let mut subject = \"{}\".to_string();\n",
3955 subject.replace('"', "\\\"")
3956 ));
3957 code.push_str(" for (k, v) in ctx.state.iter() {\n");
3958 code.push_str(
3959 " let pattern = format!(\"{{{{{}}}}}\", k);\n",
3960 );
3961 code.push_str(" if let Some(s) = v.as_str() {\n");
3962 code.push_str(
3963 " subject = subject.replace(&pattern, s);\n",
3964 );
3965 code.push_str(" } else {\n");
3966 code.push_str(" subject = subject.replace(&pattern, &v.to_string());\n");
3967 code.push_str(" }\n");
3968 code.push_str(" }\n");
3969 } else {
3970 code.push_str(&format!(
3971 " let subject = \"{}\".to_string();\n",
3972 subject.replace('"', "\\\"")
3973 ));
3974 }
3975
3976 let body = &content.body;
3978 if body.contains("{{") {
3979 code.push_str(&format!(
3980 " let mut body = \"{}\".to_string();\n",
3981 body.replace('"', "\\\"").replace('\n', "\\n")
3982 ));
3983 code.push_str(" for (k, v) in ctx.state.iter() {\n");
3984 code.push_str(
3985 " let pattern = format!(\"{{{{{}}}}}\", k);\n",
3986 );
3987 code.push_str(" if let Some(s) = v.as_str() {\n");
3988 code.push_str(" body = body.replace(&pattern, s);\n");
3989 code.push_str(" } else {\n");
3990 code.push_str(
3991 " body = body.replace(&pattern, &v.to_string());\n",
3992 );
3993 code.push_str(" }\n");
3994 code.push_str(" }\n");
3995 } else {
3996 code.push_str(&format!(
3997 " let body = \"{}\".to_string();\n",
3998 body.replace('"', "\\\"").replace('\n', "\\n")
3999 ));
4000 }
4001
4002 let from_addr = if let Some(ref name) = smtp.from_name {
4004 format!(
4005 "{} <{}>",
4006 name.replace('"', "\\\""),
4007 smtp.from_email.replace('"', "\\\"")
4008 )
4009 } else {
4010 smtp.from_email.replace('"', "\\\"")
4011 };
4012
4013 code.push_str(&format!(
4014 " let from = \"{}\".parse::<lettre::message::Mailbox>()\n",
4015 from_addr
4016 ));
4017 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"Invalid from address: {}\", e) })?;\n");
4018
4019 code.push_str(&format!(
4021 " let to_addrs = \"{}\".to_string();\n",
4022 recipients.to.replace('"', "\\\"")
4023 ));
4024
4025 code.push_str(
4026 " let mut email_builder = lettre::Message::builder()\n",
4027 );
4028 code.push_str(" .from(from)\n");
4029 code.push_str(" .subject(&subject);\n");
4030
4031 code.push_str(" for addr in to_addrs.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {\n");
4033 code.push_str(" if let Ok(mbox) = addr.parse::<lettre::message::Mailbox>() {\n");
4034 code.push_str(" email_builder = email_builder.to(mbox);\n");
4035 code.push_str(" }\n");
4036 code.push_str(" }\n");
4037
4038 if let Some(ref cc) = recipients.cc {
4040 if !cc.is_empty() {
4041 code.push_str(&format!(
4042 " for addr in \"{}\".split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {{\n",
4043 cc.replace('"', "\\\"")
4044 ));
4045 code.push_str(" if let Ok(mbox) = addr.parse::<lettre::message::Mailbox>() {\n");
4046 code.push_str(
4047 " email_builder = email_builder.cc(mbox);\n",
4048 );
4049 code.push_str(" }\n");
4050 code.push_str(" }\n");
4051 }
4052 }
4053
4054 if let Some(ref bcc) = recipients.bcc {
4056 if !bcc.is_empty() {
4057 code.push_str(&format!(
4058 " for addr in \"{}\".split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {{\n",
4059 bcc.replace('"', "\\\"")
4060 ));
4061 code.push_str(" if let Ok(mbox) = addr.parse::<lettre::message::Mailbox>() {\n");
4062 code.push_str(
4063 " email_builder = email_builder.bcc(mbox);\n",
4064 );
4065 code.push_str(" }\n");
4066 code.push_str(" }\n");
4067 }
4068 }
4069
4070 match content.body_type {
4072 crate::codegen::action_node_types::EmailBodyType::Html => {
4073 code.push_str(" let email = email_builder\n");
4074 code.push_str(" .header(lettre::message::header::ContentType::TEXT_HTML)\n");
4075 code.push_str(" .body(body.clone())\n");
4076 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"Failed to build email: {}\", e) })?;\n");
4077 }
4078 _ => {
4079 code.push_str(" let email = email_builder\n");
4080 code.push_str(" .header(lettre::message::header::ContentType::TEXT_PLAIN)\n");
4081 code.push_str(" .body(body.clone())\n");
4082 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"Failed to build email: {}\", e) })?;\n");
4083 }
4084 }
4085
4086 let host = smtp.host.replace('"', "\\\"");
4088 let username = smtp.username.replace('"', "\\\"");
4089 let password = smtp.password.replace('"', "\\\"");
4090
4091 if password.contains("{{") {
4093 code.push_str(&format!(
4094 " let mut smtp_password = \"{}\".to_string();\n",
4095 password
4096 ));
4097 code.push_str(" for (k, v) in ctx.state.iter() {\n");
4098 code.push_str(
4099 " let pattern = format!(\"{{{{{}}}}}\", k);\n",
4100 );
4101 code.push_str(" if let Some(s) = v.as_str() {\n");
4102 code.push_str(" smtp_password = smtp_password.replace(&pattern, s);\n");
4103 code.push_str(" }\n");
4104 code.push_str(" }\n");
4105 } else {
4106 code.push_str(&format!(
4107 " let smtp_password = \"{}\".to_string();\n",
4108 password
4109 ));
4110 }
4111
4112 code.push_str(" use lettre::Transport;\n");
4113
4114 if smtp.secure {
4115 code.push_str(&format!(
4116 " let transport = lettre::SmtpTransport::relay(\"{}\")\n",
4117 host
4118 ));
4119 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"SMTP relay failed: {}\", e) })?\n");
4120 code.push_str(&format!(" .port({})\n", smtp.port));
4121 code.push_str(&format!(
4122 " .credentials(lettre::transport::smtp::authentication::Credentials::new(\"{}\".to_string(), smtp_password))\n",
4123 username
4124 ));
4125 code.push_str(" .build();\n");
4126 } else {
4127 code.push_str(&format!(
4128 " let transport = lettre::SmtpTransport::builder_dangerous(\"{}\")\n",
4129 host
4130 ));
4131 code.push_str(&format!(" .port({})\n", smtp.port));
4132 code.push_str(&format!(
4133 " .credentials(lettre::transport::smtp::authentication::Credentials::new(\"{}\".to_string(), smtp_password))\n",
4134 username
4135 ));
4136 code.push_str(" .build();\n");
4137 }
4138
4139 code.push_str(" match transport.send(&email) {\n");
4141 code.push_str(" Ok(response) => {\n");
4142 code.push_str(&format!(
4143 " Ok(NodeOutput::new().with_update(\"{}\", json!({{\n",
4144 output_key
4145 ));
4146 code.push_str(" \"sent\": true,\n");
4147 code.push_str(" \"message\": format!(\"Email sent successfully: {:?}\", response),\n");
4148 code.push_str(&format!(
4149 " \"to\": \"{}\",\n",
4150 recipients.to.replace('"', "\\\"")
4151 ));
4152 code.push_str(" \"subject\": subject\n");
4153 code.push_str(" })))\n");
4154 code.push_str(" }\n");
4155 code.push_str(" Err(e) => {\n");
4156 code.push_str(&format!(
4157 " Ok(NodeOutput::new().with_update(\"{}\", json!({{\n",
4158 output_key
4159 ));
4160 code.push_str(" \"sent\": false,\n");
4161 code.push_str(" \"error\": format!(\"Failed to send email: {}\", e)\n");
4162 code.push_str(" })))\n");
4163 code.push_str(" }\n");
4164 code.push_str(" }\n");
4165 } else {
4166 code.push_str(&format!(
4167 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": \"Email send mode requires smtp, recipients, and content configuration\" }})))\n",
4168 output_key
4169 ));
4170 }
4171 }
4172 crate::codegen::action_node_types::EmailMode::Monitor => {
4173 if let Some(imap_cfg) = &config.imap {
4175 let host = imap_cfg.host.replace('"', "\\\"");
4176 let username = imap_cfg.username.replace('"', "\\\"");
4177 let password = imap_cfg.password.replace('"', "\\\"");
4178 let folder = imap_cfg.folder.replace('"', "\\\"");
4179
4180 if password.contains("{{") {
4182 code.push_str(&format!(
4183 " let mut imap_password = \"{}\".to_string();\n",
4184 password
4185 ));
4186 code.push_str(" for (k, v) in ctx.state.iter() {\n");
4187 code.push_str(
4188 " let pattern = format!(\"{{{{{}}}}}\", k);\n",
4189 );
4190 code.push_str(" if let Some(s) = v.as_str() {\n");
4191 code.push_str(" imap_password = imap_password.replace(&pattern, s);\n");
4192 code.push_str(" }\n");
4193 code.push_str(" }\n");
4194 } else {
4195 code.push_str(&format!(
4196 " let imap_password = \"{}\".to_string();\n",
4197 password
4198 ));
4199 }
4200
4201 if imap_cfg.secure {
4203 code.push_str(
4204 " let tls = native_tls::TlsConnector::builder().build()\n",
4205 );
4206 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"TLS error: {}\", e) })?;\n");
4207 code.push_str(&format!(
4208 " let client = imap::ClientBuilder::new(\"{}\", {}).native_tls(tls)\n",
4209 host, imap_cfg.port
4210 ));
4211 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"IMAP connection failed: {}\", e) })?;\n");
4212 } else {
4213 code.push_str(&format!(
4214 " let client = imap::ClientBuilder::new(\"{}\", {}).connect()\n",
4215 host, imap_cfg.port
4216 ));
4217 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"IMAP connection failed: {}\", e) })?;\n");
4218 }
4219
4220 code.push_str(&format!(
4222 " let mut session = client.login(\"{}\", &imap_password)\n",
4223 username
4224 ));
4225 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"IMAP login failed: {:?}\", e.0) })?;\n");
4226
4227 code.push_str(&format!(" session.select(\"{}\")\n", folder));
4229 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"Folder select failed: {}\", e) })?;\n");
4230
4231 let mut search_criteria = Vec::new();
4233 if let Some(ref filters) = config.filters {
4234 if filters.unread_only {
4235 search_criteria.push("UNSEEN".to_string());
4236 }
4237 if let Some(ref from) = filters.from {
4238 if !from.is_empty() {
4239 search_criteria.push(format!(
4240 "FROM \\\"{}\\\"",
4241 from.replace('"', "\\\\\\\"")
4242 ));
4243 }
4244 }
4245 if let Some(ref subject) = filters.subject {
4246 if !subject.is_empty() {
4247 search_criteria.push(format!(
4248 "SUBJECT \\\"{}\\\"",
4249 subject.replace('"', "\\\\\\\"")
4250 ));
4251 }
4252 }
4253 }
4254 let search_str = if search_criteria.is_empty() {
4255 "ALL".to_string()
4256 } else {
4257 search_criteria.join(" ")
4258 };
4259
4260 code.push_str(&format!(
4261 " let messages = session.search(\"{}\")\n",
4262 search_str
4263 ));
4264 code.push_str(" .map_err(|e| adk_graph::GraphError::NodeExecutionFailed { node: \"email\".into(), message: format!(\"IMAP search failed: {}\", e) })?;\n");
4265
4266 code.push_str(
4268 " let mut emails: Vec<serde_json::Value> = Vec::new();\n",
4269 );
4270 code.push_str(" if !messages.is_empty() {\n");
4271 code.push_str(" let seq_set: Vec<String> = messages.iter().map(|id| id.to_string()).collect();\n");
4272 code.push_str(" let fetch_range = seq_set.join(\",\");\n");
4273 code.push_str(" if let Ok(fetched) = session.fetch(&fetch_range, \"(RFC822.HEADER BODY[TEXT])\") {\n");
4274 code.push_str(" for msg in fetched.iter() {\n");
4275 code.push_str(" let header = msg.header().map(|h| String::from_utf8_lossy(h).to_string()).unwrap_or_default();\n");
4276 code.push_str(" let body_text = msg.text().map(|b| String::from_utf8_lossy(b).to_string()).unwrap_or_default();\n");
4277 code.push_str(" // Parse basic headers\n");
4278 code.push_str(" let from_line = header.lines().find(|l| l.starts_with(\"From:\")).map(|l| l[5..].trim().to_string()).unwrap_or_default();\n");
4279 code.push_str(" let subject_line = header.lines().find(|l| l.starts_with(\"Subject:\")).map(|l| l[8..].trim().to_string()).unwrap_or_default();\n");
4280 code.push_str(" let date_line = header.lines().find(|l| l.starts_with(\"Date:\")).map(|l| l[5..].trim().to_string()).unwrap_or_default();\n");
4281 code.push_str(" emails.push(json!({\n");
4282 code.push_str(" \"from\": from_line,\n");
4283 code.push_str(" \"subject\": subject_line,\n");
4284 code.push_str(" \"date\": date_line,\n");
4285 code.push_str(" \"body\": body_text,\n");
4286 code.push_str(" \"uid\": msg.message\n");
4287 code.push_str(" }));\n");
4288 code.push_str(" }\n");
4289 code.push_str(" }\n");
4290
4291 if imap_cfg.mark_as_read {
4293 code.push_str(" // Mark fetched messages as read\n");
4294 code.push_str(" let _ = session.store(&fetch_range, \"+FLAGS (\\\\Seen)\");\n");
4295 }
4296
4297 code.push_str(" }\n");
4298 code.push_str(" let _ = session.logout();\n");
4299
4300 code.push_str(" let email_count = emails.len();\n");
4301 code.push_str(&format!(
4302 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"emails\": emails, \"count\": email_count }})))\n",
4303 output_key
4304 ));
4305 } else {
4306 code.push_str(&format!(
4307 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": \"Email monitor mode requires IMAP configuration\" }})))\n",
4308 output_key
4309 ));
4310 }
4311 }
4312 }
4313
4314 code.push_str(" });\n\n");
4315 }
4316 ActionNodeConfig::Code(config) => {
4317 code.push_str(&format!(
4318 " // Action Node: {} (Code - {:?})\n",
4319 config.standard.name, config.language
4320 ));
4321 code.push_str(&format!(" let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |ctx| async move {{\n", node_id, node_id));
4322
4323 let output_key = if config.standard.mapping.output_key.is_empty() {
4324 "codeResult"
4325 } else {
4326 &config.standard.mapping.output_key
4327 };
4328
4329 if config.code.is_empty() {
4330 code.push_str(&format!(
4331 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": \"No code provided\" }})))\n",
4332 output_key
4333 ));
4334 } else {
4335 match config.language {
4336 crate::codegen::action_node_types::CodeLanguage::Rust => {
4337 code.push_str(
4342 " // Rust-first: execute authored Rust body directly\n",
4343 );
4344 code.push_str(" let mut input_obj = serde_json::Map::new();\n");
4345 code.push_str(" for (k, v) in ctx.state.iter() {\n");
4346 code.push_str(" input_obj.insert(k.clone(), v.clone());\n");
4347 code.push_str(" }\n");
4348 code.push_str(
4349 " let input = serde_json::Value::Object(input_obj);\n\n",
4350 );
4351 code.push_str(&format!(" let result = {}_run(input);\n", node_id));
4352 code.push_str(&format!(
4353 " Ok(NodeOutput::new().with_update(\"{}\", result))\n",
4354 output_key
4355 ));
4356 }
4357 _ => {
4358 code.push_str(
4360 " // Secondary scripting: execute JS/TS via boa_engine\n",
4361 );
4362 code.push_str(" let mut input_obj = serde_json::Map::new();\n");
4363 code.push_str(" for (k, v) in ctx.state.iter() {\n");
4364 code.push_str(" input_obj.insert(k.clone(), v.clone());\n");
4365 code.push_str(" }\n");
4366 code.push_str(
4367 " let input_json = serde_json::Value::Object(input_obj);\n\n",
4368 );
4369
4370 let escaped_code = config.code.replace('\\', "\\\\");
4371 let escaped_code = escaped_code.replace('"', "\\\"");
4372 let escaped_code = escaped_code.replace('\n', "\\n");
4373 let escaped_code = escaped_code.replace('\r', "");
4374 let escaped_code = escaped_code.replace('\t', "\\t");
4375
4376 code.push_str(" let js_result = std::thread::spawn(move || -> Result<serde_json::Value, String> {\n");
4377 code.push_str(
4378 " use boa_engine::{Context, Source, JsValue, JsError};\n",
4379 );
4380 code.push_str(" use std::time::Instant;\n\n");
4381
4382 let time_limit = config.sandbox.time_limit;
4383 code.push_str(&format!(
4384 " let time_limit = std::time::Duration::from_millis({});\n",
4385 time_limit
4386 ));
4387 code.push_str(" let start = Instant::now();\n\n");
4388 code.push_str(" let mut context = Context::default();\n\n");
4389 code.push_str(" let input_str = serde_json::to_string(&input_json).unwrap_or_else(|_| \"{}\".to_string());\n");
4390 code.push_str(
4391 " let setup_code = format!(\"var input = {};\", input_str);\n",
4392 );
4393 code.push_str(
4394 " context.eval(Source::from_bytes(&setup_code))\n",
4395 );
4396 code.push_str(" .map_err(|e| format!(\"Failed to inject input: {:?}\", e))?;\n\n");
4397
4398 code.push_str(&format!(
4399 " let user_code = \"{}\";\n",
4400 escaped_code
4401 ));
4402 code.push_str(
4403 " let wrapped = format!(\"(function() {{ {} }})()\", user_code);\n",
4404 );
4405 code.push_str(
4406 " let result = context.eval(Source::from_bytes(&wrapped));\n\n",
4407 );
4408
4409 code.push_str(" if start.elapsed() > time_limit {\n");
4410 code.push_str(" return Err(format!(\"Code execution exceeded time limit of {}ms\", time_limit.as_millis()));\n");
4411 code.push_str(" }\n\n");
4412
4413 code.push_str(" match result {\n");
4414 code.push_str(" Ok(val) => {\n");
4415 code.push_str(
4416 " let json_val = js_value_to_json(&val, &mut context);\n",
4417 );
4418 code.push_str(" Ok(json_val)\n");
4419 code.push_str(" }\n");
4420 code.push_str(" Err(e) => {\n");
4421 code.push_str(
4422 " Err(format!(\"JavaScript error: {:?}\", e))\n",
4423 );
4424 code.push_str(" }\n");
4425 code.push_str(" }\n");
4426 code.push_str(" }).join().unwrap_or_else(|_| Err(\"Code execution panicked\".to_string()));\n\n");
4427
4428 code.push_str(" match js_result {\n");
4429 code.push_str(" Ok(value) => {\n");
4430 code.push_str(&format!(
4431 " Ok(NodeOutput::new().with_update(\"{}\", value))\n",
4432 output_key
4433 ));
4434 code.push_str(" }\n");
4435 code.push_str(" Err(err) => {\n");
4436 code.push_str(&format!(
4437 " Ok(NodeOutput::new().with_update(\"{}\", json!({{ \"error\": true, \"message\": err }})))\n",
4438 output_key
4439 ));
4440 code.push_str(" }\n");
4441 code.push_str(" }\n");
4442 }
4443 }
4444 }
4445
4446 code.push_str(" });\n\n");
4447
4448 if !config.code.is_empty()
4450 && matches!(
4451 config.language,
4452 crate::codegen::action_node_types::CodeLanguage::Rust
4453 )
4454 {
4455 code.push_str(&format!(
4456 "// Authored Rust body for Code node \"{}\"\n",
4457 config.standard.name
4458 ));
4459 code.push_str(&format!(
4460 "fn {}_run(input: serde_json::Value) -> serde_json::Value {{\n",
4461 node_id
4462 ));
4463 code.push_str(&config.code);
4464 code.push_str("\n}\n\n");
4465 }
4466 }
4467 _ => {
4469 let standard = node.standard();
4471 code.push_str(&format!(
4472 " // Action Node: {} ({})\n",
4473 standard.name,
4474 node.node_type()
4475 ));
4476 code.push_str(&format!(" let {}_node = adk_graph::node::FunctionNode::new(\"{}\", |_ctx| async move {{\n", node_id, node_id));
4477 code.push_str(" Ok(NodeOutput::new())\n");
4478 code.push_str(" });\n\n");
4479 }
4480 }
4481
4482 code
4483}
4484
4485fn generate_cargo_toml(project: &ProjectSchema) -> String {
4486 let mut name = project
4487 .name
4488 .to_lowercase()
4489 .replace(' ', "_")
4490 .replace(|c: char| !c.is_alphanumeric() && c != '_', "");
4491 if name.is_empty()
4493 || name
4494 .chars()
4495 .next()
4496 .map(|c| c.is_ascii_digit())
4497 .unwrap_or(false)
4498 {
4499 name = format!("project_{}", name);
4500 }
4501
4502 let adk_version = project
4504 .settings
4505 .adk_version
4506 .as_deref()
4507 .unwrap_or(DEFAULT_ADK_VERSION);
4508 let rust_edition = project.settings.rust_edition.as_deref().unwrap_or("2024");
4509
4510 let code_uses = |pattern: &str| -> bool {
4512 project.tool_configs.values().any(|tc| {
4513 if let ToolConfig::Function(fc) = tc {
4514 fc.code.contains(pattern)
4515 } else {
4516 false
4517 }
4518 })
4519 };
4520
4521 let needs_reqwest = code_uses("reqwest::") || {
4522 use crate::codegen::action_nodes::ActionNodeConfig;
4523 project
4524 .action_nodes
4525 .values()
4526 .any(|n| matches!(n, ActionNodeConfig::Http(_)))
4527 };
4528 let needs_lettre = code_uses("lettre::");
4529 let needs_base64 = code_uses("base64::");
4530 let needs_imap = code_uses("imap::");
4531 let needs_native_tls = code_uses("native_tls::");
4532 let needs_boa = {
4533 use crate::codegen::action_node_types::CodeLanguage;
4534 use crate::codegen::action_nodes::ActionNodeConfig;
4535 project.action_nodes.values().any(|n| {
4536 matches!(
4537 n,
4538 ActionNodeConfig::Code(cfg) if matches!(cfg.language, CodeLanguage::Javascript | CodeLanguage::Typescript)
4539 )
4540 })
4541 };
4542
4543 let (needs_sqlx_pg, needs_sqlx_mysql, needs_sqlx_sqlite, needs_mongodb, needs_redis) = {
4545 use crate::codegen::action_node_types::DatabaseType;
4546 use crate::codegen::action_nodes::ActionNodeConfig;
4547 let mut pg = false;
4548 let mut mysql = false;
4549 let mut sqlite = false;
4550 let mut mongo = false;
4551 let mut redis = false;
4552 for node in project.action_nodes.values() {
4553 if let ActionNodeConfig::Database(cfg) = node {
4554 match cfg.db_type {
4555 DatabaseType::Postgresql => pg = true,
4556 DatabaseType::Mysql => mysql = true,
4557 DatabaseType::Sqlite => sqlite = true,
4558 DatabaseType::Mongodb => mongo = true,
4559 DatabaseType::Redis => redis = true,
4560 }
4561 }
4562 }
4563 (pg, mysql, sqlite, mongo, redis)
4564 };
4565
4566 let providers = collect_providers(project);
4568 let mut model_features: Vec<&str> = Vec::new();
4569 if providers.contains("gemini") {
4570 model_features.push("gemini");
4571 }
4572 if providers.contains("openai") {
4573 model_features.push("openai");
4574 }
4575 if providers.contains("anthropic") {
4576 model_features.push("anthropic");
4577 }
4578 if providers.contains("deepseek") {
4579 model_features.push("deepseek");
4580 }
4581 if providers.contains("groq") {
4582 model_features.push("groq");
4583 }
4584 if providers.contains("ollama") {
4585 model_features.push("ollama");
4586 }
4587 if providers.contains("fireworks") {
4588 model_features.push("fireworks");
4589 }
4590 if providers.contains("together") {
4591 model_features.push("together");
4592 }
4593 if providers.contains("mistral") {
4594 model_features.push("mistral");
4595 }
4596 if providers.contains("perplexity") {
4597 model_features.push("perplexity");
4598 }
4599 if providers.contains("cerebras") {
4600 model_features.push("cerebras");
4601 }
4602 if providers.contains("sambanova") {
4603 model_features.push("sambanova");
4604 }
4605 if providers.contains("bedrock") {
4606 model_features.push("bedrock");
4607 }
4608 if providers.contains("azure-ai") {
4609 model_features.push("azure-ai");
4610 }
4611 if model_features.is_empty() {
4613 model_features.push("gemini");
4614 }
4615 let features_str = model_features
4616 .iter()
4617 .map(|f| format!("\"{}\"", f))
4618 .collect::<Vec<_>>()
4619 .join(", ");
4620
4621 let has_action_nodes = !project.action_nodes.is_empty();
4623 let graph_features_str = if has_action_nodes {
4624 ", features = [\"action-full\"]"
4625 } else {
4626 ""
4627 };
4628 let action_dep = if has_action_nodes {
4629 format!("\nadk-action = \"{}\"", adk_version)
4630 } else {
4631 String::new()
4632 };
4633
4634 let adk_deps = format!(
4635 r#"adk-agent = "{}"
4636adk-core = "{}"
4637adk-model = {{ version = "{}", default-features = false, features = [{}] }}
4638adk-tool = "{}"
4639adk-graph = {{ version = "{}"{} }}{}"#,
4640 adk_version, adk_version, adk_version, features_str, adk_version, adk_version, graph_features_str, action_dep
4641 );
4642
4643 let mut deps = format!(
4644 r#"[package]
4645name = "{}"
4646version = "0.1.0"
4647edition = "{}"
4648
4649[dependencies]
4650{}
4651tokio = {{ version = "1", features = ["full", "macros"] }}
4652tokio-stream = "0.1"
4653anyhow = "1"
4654serde = {{ version = "1", features = ["derive"] }}
4655serde_json = "1"
4656schemars = "0.8"
4657tracing-subscriber = {{ version = "0.3", features = ["json", "env-filter"] }}
4658uuid = {{ version = "1", features = ["v4"] }}
4659"#,
4660 name, rust_edition, adk_deps
4661 );
4662
4663 if needs_reqwest {
4664 deps.push_str("reqwest = { version = \"0.12\", features = [\"json\"] }\n");
4665 }
4666 if needs_lettre {
4667 deps.push_str("lettre = \"0.11\"\n");
4668 }
4669 if needs_base64 {
4670 deps.push_str("base64 = \"0.21\"\n");
4671 }
4672 if needs_imap {
4673 deps.push_str("imap = \"3\"\n");
4674 }
4675 if needs_native_tls {
4676 deps.push_str("native-tls = \"0.2\"\n");
4677 }
4678 if needs_boa {
4679 deps.push_str("boa_engine = { version = \"0.20\", features = [\"annex-b\"] }\n");
4680 }
4681
4682 let needs_sqlx = needs_sqlx_pg || needs_sqlx_mysql || needs_sqlx_sqlite;
4684 if needs_sqlx {
4685 let mut sqlx_features = vec!["runtime-tokio"];
4686 if needs_sqlx_pg {
4687 sqlx_features.push("postgres");
4688 }
4689 if needs_sqlx_mysql {
4690 sqlx_features.push("mysql");
4691 }
4692 if needs_sqlx_sqlite {
4693 sqlx_features.push("sqlite");
4694 }
4695 let features_str = sqlx_features
4696 .iter()
4697 .map(|f| format!("\"{}\"", f))
4698 .collect::<Vec<_>>()
4699 .join(", ");
4700 deps.push_str(&format!(
4701 "sqlx = {{ version = \"0.8\", features = [{}] }}\n",
4702 features_str
4703 ));
4704 }
4705 if needs_mongodb {
4706 deps.push_str("mongodb = \"3\"\nfutures = \"0.3\"\n");
4707 }
4708 if needs_redis {
4709 deps.push_str("redis = { version = \"0.27\", features = [\"tokio-comp\"] }\n");
4710 }
4711
4712 let uses_mcp = project
4714 .agents
4715 .values()
4716 .any(|a| a.tools.iter().any(|t| t == "mcp" || t.starts_with("mcp_")));
4717 if uses_mcp {
4718 deps.push_str(
4719 "rmcp = { version = \"1.3\", features = [\"client\", \"transport-child-process\"] }\n",
4720 );
4721 deps.push_str("async-trait = \"0.1\"\n");
4722 }
4723
4724 let uses_browser = project
4726 .agents
4727 .values()
4728 .any(|a| a.tools.contains(&"browser".to_string()));
4729 if uses_browser {
4730 deps.push_str(&format!("adk-browser = \"{}\"\n", adk_version));
4731 if !uses_mcp {
4733 deps.push_str("async-trait = \"0.1\"\n");
4734 }
4735 }
4736
4737 deps
4738}