|
| 1 | +//! Streaming agent CLI - Real-time terminal output mode |
| 2 | +//! |
| 3 | +//! This module provides streaming output to the terminal as the agent executes, |
| 4 | +//! without launching the chat UI. It's ideal for one-off queries where you want |
| 5 | +//! to see real-time progress and results directly in your terminal. |
| 6 | +//! |
| 7 | +//! Usage: `osvm query text here` - runs as a streaming agent query |
| 8 | +
|
| 9 | +use anyhow::{Context, Result}; |
| 10 | +use serde_json::Value; |
| 11 | +use std::collections::HashMap; |
| 12 | +use std::io::Write; |
| 13 | + |
| 14 | +use crate::services::{ |
| 15 | + ai_service::{AiService, PlannedTool, ToolPlan}, |
| 16 | + mcp_service::{McpService, McpTool}, |
| 17 | +}; |
| 18 | + |
| 19 | +/// Execute agent command with real-time streaming output to terminal |
| 20 | +pub async fn execute_streaming_agent(query: &str, verbose: u8) -> Result<()> { |
| 21 | + let start_time = std::time::Instant::now(); |
| 22 | + |
| 23 | + // Print header |
| 24 | + println!("\n🤖 OSVM Agent - Streaming Mode"); |
| 25 | + println!("{}", "━".repeat(60)); |
| 26 | + println!("📝 Query: {}", query); |
| 27 | + println!("{}", "━".repeat(60)); |
| 28 | + println!(); |
| 29 | + |
| 30 | + // Initialize services |
| 31 | + let ai_service = AiService::new_with_debug(verbose > 1); |
| 32 | + let mut mcp_service = McpService::new_with_debug(verbose > 1); |
| 33 | + |
| 34 | + // Load MCP configurations |
| 35 | + if let Err(e) = mcp_service.load_config() { |
| 36 | + if verbose > 0 { |
| 37 | + eprintln!("⚠️ Warning: Failed to load MCP configurations: {}", e); |
| 38 | + } |
| 39 | + } |
| 40 | + |
| 41 | + // Get available tools from MCP service |
| 42 | + let mut available_tools: HashMap<String, Vec<McpTool>> = HashMap::new(); |
| 43 | + |
| 44 | + // In a real implementation, would get tools from active MCP servers |
| 45 | + let mock_tools = vec![ |
| 46 | + McpTool { |
| 47 | + name: "get_balance".to_string(), |
| 48 | + description: Some("Get wallet balance from blockchain".to_string()), |
| 49 | + input_schema: serde_json::json!({"type": "object", "properties": {}}), |
| 50 | + }, |
| 51 | + McpTool { |
| 52 | + name: "get_transactions".to_string(), |
| 53 | + description: Some("Get recent transactions from wallet".to_string()), |
| 54 | + input_schema: serde_json::json!({"type": "object", "properties": {"limit": {"type": "number", "default": 10}}}), |
| 55 | + }, |
| 56 | + McpTool { |
| 57 | + name: "deploy_validator".to_string(), |
| 58 | + description: Some("Deploy a new validator node".to_string()), |
| 59 | + input_schema: serde_json::json!({"type": "object", "properties": {"network": {"type": "string"}}}), |
| 60 | + }, |
| 61 | + McpTool { |
| 62 | + name: "get_network_status".to_string(), |
| 63 | + description: Some("Get current network status and statistics".to_string()), |
| 64 | + input_schema: serde_json::json!({"type": "object", "properties": {}}), |
| 65 | + }, |
| 66 | + ]; |
| 67 | + |
| 68 | + if !mock_tools.is_empty() { |
| 69 | + available_tools.insert("blockchain_tools".to_string(), mock_tools); |
| 70 | + } |
| 71 | + |
| 72 | + // Step 1: AI Planning |
| 73 | + print!("🧠 AI Planning"); |
| 74 | + std::io::stdout().flush()?; |
| 75 | + |
| 76 | + let tool_plan = match ai_service.create_tool_plan(query, &available_tools).await { |
| 77 | + Ok(plan) => { |
| 78 | + println!(" ✅"); |
| 79 | + plan |
| 80 | + } |
| 81 | + Err(e) => { |
| 82 | + println!(" ❌"); |
| 83 | + eprintln!("❌ Failed to create AI plan: {}", e); |
| 84 | + return Err(e); |
| 85 | + } |
| 86 | + }; |
| 87 | + |
| 88 | + // Display plan details |
| 89 | + println!("\n📋 Plan Details:"); |
| 90 | + println!(" Reasoning: {}", tool_plan.reasoning); |
| 91 | + println!(" Expected Outcome: {}", tool_plan.expected_outcome); |
| 92 | + |
| 93 | + if !tool_plan.osvm_tools_to_use.is_empty() { |
| 94 | + println!("\n🔧 Tools to Execute:"); |
| 95 | + for (i, tool) in tool_plan.osvm_tools_to_use.iter().enumerate() { |
| 96 | + println!(" {}. {} (from {})", i + 1, tool.tool_name, tool.server_id); |
| 97 | + if verbose > 0 { |
| 98 | + println!(" Reason: {}", tool.reason); |
| 99 | + } |
| 100 | + } |
| 101 | + } else { |
| 102 | + println!("\n📌 No specific tools needed - will provide direct response"); |
| 103 | + } |
| 104 | + |
| 105 | + println!(); |
| 106 | + |
| 107 | + // Step 2: Execute Tools |
| 108 | + let mut tool_results = Vec::new(); |
| 109 | + |
| 110 | + for (i, planned_tool) in tool_plan.osvm_tools_to_use.iter().enumerate() { |
| 111 | + print!("⚙️ Executing tool [{}/{}]: {}", |
| 112 | + i + 1, |
| 113 | + tool_plan.osvm_tools_to_use.len(), |
| 114 | + planned_tool.tool_name); |
| 115 | + std::io::stdout().flush()?; |
| 116 | + |
| 117 | + let tool_start = std::time::Instant::now(); |
| 118 | + |
| 119 | + // Execute tool (mock for now) |
| 120 | + let (result, error) = execute_tool(&planned_tool).await; |
| 121 | + let elapsed = tool_start.elapsed().as_millis(); |
| 122 | + |
| 123 | + if error.is_some() { |
| 124 | + println!(" ❌ ({}ms)", elapsed); |
| 125 | + println!(" Error: {}", error.as_ref().unwrap()); |
| 126 | + } else { |
| 127 | + println!(" ✅ ({}ms)", elapsed); |
| 128 | + if let Some(ref result_value) = result { |
| 129 | + // Show key results |
| 130 | + print_tool_result(&planned_tool.tool_name, result_value); |
| 131 | + } |
| 132 | + } |
| 133 | + |
| 134 | + tool_results.push((planned_tool.clone(), result, error)); |
| 135 | + } |
| 136 | + |
| 137 | + println!(); |
| 138 | + |
| 139 | + // Step 3: Generate Response |
| 140 | + print!("💬 Generating Final Response"); |
| 141 | + std::io::stdout().flush()?; |
| 142 | + |
| 143 | + let tool_results_for_ai: Vec<(String, Value)> = tool_results |
| 144 | + .iter() |
| 145 | + .filter_map(|(tool, result, _error)| { |
| 146 | + result |
| 147 | + .as_ref() |
| 148 | + .map(|r| (tool.tool_name.clone(), r.clone())) |
| 149 | + }) |
| 150 | + .collect(); |
| 151 | + |
| 152 | + let final_response = if tool_results_for_ai.is_empty() && tool_plan.osvm_tools_to_use.is_empty() { |
| 153 | + // No tools - direct response |
| 154 | + match ai_service.query_with_debug(query, verbose > 1).await { |
| 155 | + Ok(resp) => { |
| 156 | + println!(" ✅"); |
| 157 | + resp |
| 158 | + } |
| 159 | + Err(e) => { |
| 160 | + println!(" ❌"); |
| 161 | + eprintln!("❌ Failed to generate response: {}", e); |
| 162 | + format!("Error: {}", e) |
| 163 | + } |
| 164 | + } |
| 165 | + } else { |
| 166 | + // Generate contextual response with tool results |
| 167 | + match ai_service |
| 168 | + .generate_contextual_response(query, &tool_results_for_ai, &tool_plan.expected_outcome) |
| 169 | + .await |
| 170 | + { |
| 171 | + Ok(resp) => { |
| 172 | + println!(" ✅"); |
| 173 | + resp |
| 174 | + } |
| 175 | + Err(e) => { |
| 176 | + println!(" ❌"); |
| 177 | + eprintln!("❌ Failed to generate response: {}", e); |
| 178 | + format!("Error generating response: {}", e) |
| 179 | + } |
| 180 | + } |
| 181 | + }; |
| 182 | + |
| 183 | + println!(); |
| 184 | + |
| 185 | + // Step 4: Display Results |
| 186 | + println!("{}", "━".repeat(60)); |
| 187 | + println!("📊 Result:"); |
| 188 | + println!("{}", "━".repeat(60)); |
| 189 | + println!(); |
| 190 | + println!("{}", final_response); |
| 191 | + println!(); |
| 192 | + |
| 193 | + // Display execution statistics |
| 194 | + let elapsed_ms = start_time.elapsed().as_millis(); |
| 195 | + println!("{}", "━".repeat(60)); |
| 196 | + println!("⏱️ Execution Time: {}ms", elapsed_ms); |
| 197 | + println!("✨ Done!"); |
| 198 | + println!(); |
| 199 | + |
| 200 | + Ok(()) |
| 201 | +} |
| 202 | + |
| 203 | +/// Execute a tool with streaming output |
| 204 | +async fn execute_tool(tool: &PlannedTool) -> (Option<Value>, Option<String>) { |
| 205 | + // Simulate tool execution with small delay |
| 206 | + tokio::time::sleep(std::time::Duration::from_millis(100)).await; |
| 207 | + |
| 208 | + match tool.tool_name.as_str() { |
| 209 | + "get_balance" => ( |
| 210 | + Some(serde_json::json!({ |
| 211 | + "balance": "2.5 SOL", |
| 212 | + "usd_value": 375.50, |
| 213 | + "address": "7nYzPUfXgvNgSPe5kEqKvGvSZg4rPKnrJdK5eTbDwRmQ", |
| 214 | + "last_activity": "2024-01-20T15:30:00Z" |
| 215 | + })), |
| 216 | + None, |
| 217 | + ), |
| 218 | + "get_transactions" => ( |
| 219 | + Some(serde_json::json!({ |
| 220 | + "transactions": [ |
| 221 | + { |
| 222 | + "signature": "3n8wF9tK2pL...", |
| 223 | + "amount": "0.1 SOL", |
| 224 | + "type": "transfer" |
| 225 | + }, |
| 226 | + { |
| 227 | + "signature": "5kP2xRvN9dW...", |
| 228 | + "amount": "1.0 SOL", |
| 229 | + "type": "receive" |
| 230 | + } |
| 231 | + ], |
| 232 | + "total": 2 |
| 233 | + })), |
| 234 | + None, |
| 235 | + ), |
| 236 | + "deploy_validator" => ( |
| 237 | + Some(serde_json::json!({ |
| 238 | + "status": "initiated", |
| 239 | + "validator_id": "val_abc12345", |
| 240 | + "network": tool.args.get("network").and_then(|v| v.as_str()).unwrap_or("mainnet"), |
| 241 | + "estimated_time": "5-10 minutes" |
| 242 | + })), |
| 243 | + None, |
| 244 | + ), |
| 245 | + "get_network_status" => ( |
| 246 | + Some(serde_json::json!({ |
| 247 | + "network": "mainnet-beta", |
| 248 | + "slot": 250000000, |
| 249 | + "epoch": 580, |
| 250 | + "tps": 3000, |
| 251 | + "validators": {"active": 1800, "delinquent": 12}, |
| 252 | + "status": "healthy" |
| 253 | + })), |
| 254 | + None, |
| 255 | + ), |
| 256 | + _ => ( |
| 257 | + None, |
| 258 | + Some(format!( |
| 259 | + "Tool '{}' is not available in the current context", |
| 260 | + tool.tool_name |
| 261 | + )), |
| 262 | + ), |
| 263 | + } |
| 264 | +} |
| 265 | + |
| 266 | +/// Print tool result with formatting |
| 267 | +fn print_tool_result(tool_name: &str, result: &Value) { |
| 268 | + match tool_name { |
| 269 | + "get_balance" => { |
| 270 | + if let Some(balance) = result.get("balance") { |
| 271 | + println!(" Balance: {}", balance); |
| 272 | + } |
| 273 | + } |
| 274 | + "get_transactions" => { |
| 275 | + if let Some(txs) = result.get("transactions").and_then(|v| v.as_array()) { |
| 276 | + println!(" Retrieved {} transactions", txs.len()); |
| 277 | + for (i, tx) in txs.iter().take(3).enumerate() { |
| 278 | + if let Some(sig) = tx.get("signature") { |
| 279 | + println!(" {}: {}", i + 1, sig); |
| 280 | + } |
| 281 | + } |
| 282 | + if txs.len() > 3 { |
| 283 | + println!(" ... and {} more", txs.len() - 3); |
| 284 | + } |
| 285 | + } |
| 286 | + } |
| 287 | + "deploy_validator" => { |
| 288 | + if let Some(status) = result.get("status") { |
| 289 | + println!(" Status: {}", status); |
| 290 | + } |
| 291 | + if let Some(id) = result.get("validator_id") { |
| 292 | + println!(" ID: {}", id); |
| 293 | + } |
| 294 | + } |
| 295 | + "get_network_status" => { |
| 296 | + if let Some(slot) = result.get("slot") { |
| 297 | + println!(" Current Slot: {}", slot); |
| 298 | + } |
| 299 | + if let Some(status) = result.get("status") { |
| 300 | + println!(" Network Status: {}", status); |
| 301 | + } |
| 302 | + } |
| 303 | + _ => { |
| 304 | + println!(" Result: {}", serde_json::to_string(result).unwrap_or_default()); |
| 305 | + } |
| 306 | + } |
| 307 | +} |
0 commit comments