adk_cli/
launcher.rs

1//! Simple launcher for ADK agents with CLI support.
2//!
3//! Provides a one-liner to run agents with console or web server modes,
4//! similar to adk-go's launcher pattern.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use adk_rust::prelude::*;
10//! use adk_rust::Launcher;
11//!
12//! #[tokio::main]
13//! async fn main() -> Result<()> {
14//!     let agent = /* create your agent */;
15//!
16//!     // Run with CLI support (console by default, or `serve` for web)
17//!     Launcher::new(agent).run().await
18//! }
19//! ```
20//!
21//! # CLI Usage
22//!
23//! ```bash
24//! # Interactive console (default)
25//! cargo run
26//!
27//! # Web server with UI
28//! cargo run -- serve
29//! cargo run -- serve --port 3000
30//! ```
31
32use adk_artifact::ArtifactService;
33use adk_core::{Agent, AgentLoader, Result, RunConfig, StreamingMode};
34use adk_server::{ServerConfig, create_app};
35use adk_session::InMemorySessionService;
36use clap::{Parser, Subcommand};
37use std::sync::Arc;
38
39/// CLI arguments for the launcher
40#[derive(Parser)]
41#[command(name = "agent")]
42#[command(about = "ADK Agent", long_about = None)]
43struct Cli {
44    #[command(subcommand)]
45    command: Option<Commands>,
46}
47
48#[derive(Subcommand)]
49enum Commands {
50    /// Run interactive console (default if no command specified)
51    Chat,
52    /// Start web server with UI
53    Serve {
54        /// Server port
55        #[arg(long, default_value_t = 8080)]
56        port: u16,
57    },
58}
59
60/// Single agent loader for the launcher
61pub struct SingleAgentLoader {
62    agent: Arc<dyn Agent>,
63}
64
65impl SingleAgentLoader {
66    /// Create a new single agent loader
67    pub fn new(agent: Arc<dyn Agent>) -> Self {
68        Self { agent }
69    }
70}
71
72#[async_trait::async_trait]
73impl AgentLoader for SingleAgentLoader {
74    async fn load_agent(&self, _name: &str) -> Result<Arc<dyn Agent>> {
75        Ok(self.agent.clone())
76    }
77
78    fn list_agents(&self) -> Vec<String> {
79        vec![self.agent.name().to_string()]
80    }
81
82    fn root_agent(&self) -> Arc<dyn Agent> {
83        self.agent.clone()
84    }
85}
86
87/// Launcher for running ADK agents with CLI support.
88///
89/// Provides console and web server modes out of the box.
90pub struct Launcher {
91    agent: Arc<dyn Agent>,
92    app_name: Option<String>,
93    artifact_service: Option<Arc<dyn ArtifactService>>,
94    run_config: Option<RunConfig>,
95}
96
97impl Launcher {
98    /// Create a new launcher with the given agent.
99    pub fn new(agent: Arc<dyn Agent>) -> Self {
100        Self { agent, app_name: None, artifact_service: None, run_config: None }
101    }
102
103    /// Set a custom application name (defaults to agent name).
104    pub fn app_name(mut self, name: impl Into<String>) -> Self {
105        self.app_name = Some(name.into());
106        self
107    }
108
109    /// Set a custom artifact service.
110    pub fn with_artifact_service(mut self, service: Arc<dyn ArtifactService>) -> Self {
111        self.artifact_service = Some(service);
112        self
113    }
114
115    /// Set streaming mode (defaults to SSE if not specified).
116    pub fn with_streaming_mode(mut self, mode: StreamingMode) -> Self {
117        self.run_config = Some(RunConfig { streaming_mode: mode });
118        self
119    }
120
121    /// Run the launcher, parsing CLI arguments.
122    ///
123    /// - No arguments or `chat`: Interactive console
124    /// - `serve [--port PORT]`: Web server with UI
125    pub async fn run(self) -> Result<()> {
126        let cli = Cli::parse();
127
128        match cli.command.unwrap_or(Commands::Chat) {
129            Commands::Chat => self.run_console().await,
130            Commands::Serve { port } => self.run_serve(port).await,
131        }
132    }
133
134    /// Run in interactive console mode.
135    async fn run_console(self) -> Result<()> {
136        use adk_runner::{Runner, RunnerConfig};
137        use adk_session::{CreateRequest, SessionService};
138        use futures::StreamExt;
139        use std::collections::HashMap;
140        use std::io::{self, BufRead, Write};
141
142        let app_name = self.app_name.unwrap_or_else(|| self.agent.name().to_string());
143        let user_id = "user".to_string();
144
145        let session_service = Arc::new(InMemorySessionService::new());
146
147        // Create session
148        let session = session_service
149            .create(CreateRequest {
150                app_name: app_name.clone(),
151                user_id: user_id.clone(),
152                session_id: None,
153                state: HashMap::new(),
154            })
155            .await?;
156
157        let session_id = session.id().to_string();
158
159        // Create runner
160        let runner = Runner::new(RunnerConfig {
161            app_name,
162            agent: self.agent,
163            session_service,
164            artifact_service: self.artifact_service,
165            memory_service: None,
166            run_config: self.run_config,
167        })?;
168
169        println!("šŸ¤– Agent ready! Type your questions (or 'exit' to quit).\n");
170
171        let stdin = io::stdin();
172        let mut stdout = io::stdout();
173
174        loop {
175            print!("You: ");
176            stdout.flush()?;
177
178            let mut input = String::new();
179            let bytes_read = stdin.lock().read_line(&mut input)?;
180
181            // EOF reached (e.g., piped input ended)
182            if bytes_read == 0 {
183                println!("\nšŸ‘‹ Goodbye!");
184                break;
185            }
186
187            let input = input.trim();
188
189            if input == "exit" || input == "quit" {
190                println!("šŸ‘‹ Goodbye!");
191                break;
192            }
193
194            if input.is_empty() {
195                continue;
196            }
197
198            let content = adk_core::Content::new("user").with_text(input);
199            let mut events = runner.run(user_id.clone(), session_id.clone(), content).await?;
200
201            print!("Assistant: ");
202            stdout.flush()?;
203
204            let mut current_agent = String::new();
205
206            while let Some(event) = events.next().await {
207                match event {
208                    Ok(evt) => {
209                        // Track which agent is responding
210                        if !evt.author.is_empty()
211                            && evt.author != "user"
212                            && evt.author != current_agent
213                        {
214                            if !current_agent.is_empty() {
215                                println!();
216                            }
217                            current_agent = evt.author.clone();
218                            println!("\n[Agent: {}]", current_agent);
219                            print!("Assistant: ");
220                            stdout.flush()?;
221                        }
222
223                        // Check for transfer action
224                        if let Some(target) = &evt.actions.transfer_to_agent {
225                            println!("\nšŸ”„ [Transfer requested to: {}]", target);
226                        }
227
228                        if let Some(content) = evt.llm_response.content {
229                            for part in content.parts {
230                                if let Some(text) = part.text() {
231                                    print!("{}", text);
232                                    stdout.flush()?;
233                                }
234                            }
235                        }
236                    }
237                    Err(e) => eprintln!("\nError: {}", e),
238                }
239            }
240            println!("\n");
241        }
242
243        Ok(())
244    }
245
246    /// Run web server with UI.
247    async fn run_serve(self, port: u16) -> Result<()> {
248        // Initialize telemetry with ADK-Go style exporter
249        let span_exporter = match adk_telemetry::init_with_adk_exporter("adk-server") {
250            Ok(exporter) => Some(exporter),
251            Err(e) => {
252                eprintln!("Warning: Failed to initialize telemetry: {}", e);
253                None
254            }
255        };
256
257        let session_service = Arc::new(InMemorySessionService::new());
258        let agent_loader = Arc::new(SingleAgentLoader::new(self.agent));
259
260        let mut config = ServerConfig::new(agent_loader, session_service)
261            .with_artifact_service_opt(self.artifact_service);
262
263        if let Some(exporter) = span_exporter {
264            config = config.with_span_exporter(exporter);
265        }
266
267        let app = create_app(config);
268
269        let addr = format!("0.0.0.0:{}", port);
270        let listener = tokio::net::TcpListener::bind(&addr).await?;
271
272        println!("šŸš€ ADK Server starting on http://localhost:{}", port);
273        println!("šŸ“± Open http://localhost:{} in your browser", port);
274        println!("Press Ctrl+C to stop\n");
275
276        axum::serve(listener, app).await?;
277
278        Ok(())
279    }
280}