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};
34use adk_server::{create_app, ServerConfig};
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}
95
96impl Launcher {
97    /// Create a new launcher with the given agent.
98    pub fn new(agent: Arc<dyn Agent>) -> Self {
99        Self { agent, app_name: None, artifact_service: None }
100    }
101
102    /// Set a custom application name (defaults to agent name).
103    pub fn app_name(mut self, name: impl Into<String>) -> Self {
104        self.app_name = Some(name.into());
105        self
106    }
107
108    /// Set a custom artifact service.
109    pub fn with_artifact_service(mut self, service: Arc<dyn ArtifactService>) -> Self {
110        self.artifact_service = Some(service);
111        self
112    }
113
114    /// Run the launcher, parsing CLI arguments.
115    ///
116    /// - No arguments or `chat`: Interactive console
117    /// - `serve [--port PORT]`: Web server with UI
118    pub async fn run(self) -> Result<()> {
119        let cli = Cli::parse();
120
121        match cli.command.unwrap_or(Commands::Chat) {
122            Commands::Chat => self.run_console().await,
123            Commands::Serve { port } => self.run_serve(port).await,
124        }
125    }
126
127    /// Run in interactive console mode.
128    async fn run_console(self) -> Result<()> {
129        use adk_runner::{Runner, RunnerConfig};
130        use adk_session::{CreateRequest, SessionService};
131        use futures::StreamExt;
132        use std::collections::HashMap;
133        use std::io::{self, BufRead, Write};
134
135        let app_name = self.app_name.unwrap_or_else(|| self.agent.name().to_string());
136        let user_id = "user".to_string();
137
138        let session_service = Arc::new(InMemorySessionService::new());
139
140        // Create session
141        let session = session_service
142            .create(CreateRequest {
143                app_name: app_name.clone(),
144                user_id: user_id.clone(),
145                session_id: None,
146                state: HashMap::new(),
147            })
148            .await?;
149
150        let session_id = session.id().to_string();
151
152        // Create runner
153        let runner = Runner::new(RunnerConfig {
154            app_name,
155            agent: self.agent,
156            session_service,
157            artifact_service: self.artifact_service,
158            memory_service: None,
159        })?;
160
161        println!("šŸ¤– Agent ready! Type your questions (or 'exit' to quit).\n");
162
163        let stdin = io::stdin();
164        let mut stdout = io::stdout();
165
166        loop {
167            print!("You: ");
168            stdout.flush()?;
169
170            let mut input = String::new();
171            let bytes_read = stdin.lock().read_line(&mut input)?;
172
173            // EOF reached (e.g., piped input ended)
174            if bytes_read == 0 {
175                println!("\nšŸ‘‹ Goodbye!");
176                break;
177            }
178
179            let input = input.trim();
180
181            if input == "exit" || input == "quit" {
182                println!("šŸ‘‹ Goodbye!");
183                break;
184            }
185
186            if input.is_empty() {
187                continue;
188            }
189
190            let content = adk_core::Content::new("user").with_text(input);
191            let mut events = runner.run(user_id.clone(), session_id.clone(), content).await?;
192
193            print!("Assistant: ");
194            stdout.flush()?;
195
196            let mut current_agent = String::new();
197
198            while let Some(event) = events.next().await {
199                match event {
200                    Ok(evt) => {
201                        // Track which agent is responding
202                        if !evt.author.is_empty()
203                            && evt.author != "user"
204                            && evt.author != current_agent
205                        {
206                            if !current_agent.is_empty() {
207                                println!();
208                            }
209                            current_agent = evt.author.clone();
210                            println!("\n[Agent: {}]", current_agent);
211                            print!("Assistant: ");
212                            stdout.flush()?;
213                        }
214
215                        // Check for transfer action
216                        if let Some(target) = &evt.actions.transfer_to_agent {
217                            println!("\nšŸ”„ [Transfer requested to: {}]", target);
218                        }
219
220                        if let Some(content) = evt.llm_response.content {
221                            for part in content.parts {
222                                if let Some(text) = part.text() {
223                                    print!("{}", text);
224                                    stdout.flush()?;
225                                }
226                            }
227                        }
228                    }
229                    Err(e) => eprintln!("\nError: {}", e),
230                }
231            }
232            println!("\n");
233        }
234
235        Ok(())
236    }
237
238    /// Run web server with UI.
239    async fn run_serve(self, port: u16) -> Result<()> {
240        // Initialize telemetry
241        if let Err(e) = adk_telemetry::init_telemetry("adk-server") {
242            eprintln!("Warning: Failed to initialize telemetry: {}", e);
243        }
244
245        let session_service = Arc::new(InMemorySessionService::new());
246        let agent_loader = Arc::new(SingleAgentLoader::new(self.agent));
247
248        let config = ServerConfig::new(agent_loader, session_service)
249            .with_artifact_service_opt(self.artifact_service);
250
251        let app = create_app(config);
252
253        let addr = format!("0.0.0.0:{}", port);
254        let listener = tokio::net::TcpListener::bind(&addr).await?;
255
256        println!("šŸš€ ADK Server starting on http://localhost:{}", port);
257        println!("šŸ“± Open http://localhost:{} in your browser", port);
258        println!("Press Ctrl+C to stop\n");
259
260        axum::serve(listener, app).await?;
261
262        Ok(())
263    }
264}