ceylon_next/runner/
runner.rs

1use anyhow::{Context, Result};
2use crate::{
3    agent::Agent,
4    llm::LLMConfig,
5    memory::{FileStore, InMemoryStore, Memory, SqliteStore, StorageFormat},
6    tasks::{OutputData, TaskRequest},
7};
8use colored::Colorize;
9use log::{debug, info};
10use rustyline::{error::ReadlineError, DefaultEditor};
11use std::path::Path;
12use std::sync::Arc;
13
14use super::config::CeylonConfig;
15
16/// Agent runner that executes agents based on ceylon.toml configuration
17pub struct AgentRunner {
18    config: CeylonConfig,
19    base_dir: std::path::PathBuf,
20}
21
22impl AgentRunner {
23    /// Create a new AgentRunner from a configuration file
24    pub fn new<P: AsRef<Path>>(config_path: P) -> Result<Self> {
25        let config_path = config_path.as_ref();
26        let config = CeylonConfig::load(config_path)
27            .context("Failed to load Ceylon configuration")?;
28
29        let base_dir = config_path
30            .parent()
31            .unwrap_or_else(|| Path::new("."))
32            .to_path_buf();
33
34        Ok(Self { config, base_dir })
35    }
36
37    /// Create a new AgentRunner from the current directory
38    pub fn from_current_dir() -> Result<Self> {
39        Self::new("./ceylon.toml")
40    }
41
42    /// Create a new AgentRunner from a specific directory
43    pub fn from_dir<P: AsRef<Path>>(dir: P) -> Result<Self> {
44        let config_path = dir.as_ref().join("ceylon.toml");
45        Self::new(config_path)
46    }
47
48    /// Run the agent with a single prompt and exit
49    pub async fn run(&mut self, prompt: &str) -> Result<()> {
50        println!(
51            "{}",
52            format!("🤖 Initializing agent '{}'...", self.config.agent.name).cyan()
53        );
54
55        let mut agent = self.create_agent().await?;
56
57        println!("{}", format!("📝 Processing: {}", prompt).yellow());
58        println!();
59
60        let task = TaskRequest::new(prompt);
61        let response = agent.run(task).await;
62
63        self.display_output(&response.result());
64
65        println!();
66        println!("{}", "✅ Done!".green());
67
68        Ok(())
69    }
70
71    /// Run the agent in interactive mode
72    pub async fn run_interactive(&mut self) -> Result<()> {
73        println!(
74            "{}",
75            format!(
76                "🤖 Starting interactive session with '{}'",
77                self.config.agent.name
78            )
79            .cyan()
80        );
81        println!(
82            "{}",
83            format!("📦 Model: {}", self.config.agent.model).cyan()
84        );
85        println!();
86        println!(
87            "{}",
88            "Type 'exit', 'quit', or press Ctrl+D to end the session".dimmed()
89        );
90        println!("{}", "Type 'clear' to clear conversation history".dimmed());
91        println!();
92
93        let mut agent = self.create_agent().await?;
94
95        let mut rl = DefaultEditor::new().context("Failed to initialize readline")?;
96
97        loop {
98            let readline = rl.readline(&format!("{} ", "You:".green().bold()));
99            match readline {
100                Ok(line) => {
101                    let input = line.trim();
102
103                    if input.is_empty() {
104                        continue;
105                    }
106
107                    // Handle special commands
108                    match input.to_lowercase().as_str() {
109                        "exit" | "quit" => {
110                            println!("{}", "👋 Goodbye!".cyan());
111                            break;
112                        }
113                        "clear" => {
114                            println!("{}", "🗑️  Conversation history cleared".yellow());
115                            agent = self.create_agent().await?;
116                            continue;
117                        }
118                        _ => {}
119                    }
120
121                    rl.add_history_entry(input)
122                        .context("Failed to add history entry")?;
123
124                    // Process the input
125                    print!(
126                        "{}",
127                        format!("{} ", format!("{}:", self.config.agent.name).cyan().bold())
128                    );
129                    let task = TaskRequest::new(input);
130                    let response = agent.run(task).await;
131                    self.display_output(&response.result());
132                    println!();
133                }
134                Err(ReadlineError::Interrupted) => {
135                    debug!("Received CTRL-C");
136                    println!("{}", "👋 Goodbye!".cyan());
137                    break;
138                }
139                Err(ReadlineError::Eof) => {
140                    debug!("Received CTRL-D");
141                    println!("{}", "👋 Goodbye!".cyan());
142                    break;
143                }
144                Err(err) => {
145                    return Err(err.into());
146                }
147            }
148        }
149
150        Ok(())
151    }
152
153    /// Create an agent based on the configuration
154    async fn create_agent(&self) -> Result<Agent> {
155        debug!("Creating agent with model: {}", self.config.agent.model);
156
157        // Create LLM config
158        let mut llm_config = LLMConfig::new(&self.config.agent.model);
159
160        if let Some(temp) = self.config.agent.temperature {
161            llm_config = llm_config.with_temperature(temp);
162        }
163
164        if let Some(tokens) = self.config.agent.max_tokens {
165            llm_config = llm_config.with_max_tokens(tokens);
166        }
167
168        // Create agent
169        let mut agent = Agent::new_with_config(&self.config.agent.name, llm_config)
170            .map_err(|e| anyhow::anyhow!("Failed to create agent: {}", e))?;
171
172        // Set system prompt if configured
173        if let Some(prompt) = self.config.read_system_prompt(&self.base_dir)? {
174            agent.with_system_prompt(&prompt);
175        }
176
177        // Configure memory backend
178        let memory_backend = self.create_memory_backend().await?;
179        agent.with_memory(memory_backend);
180
181        // Configure agent settings
182        let mut agent_config = crate::agent::AgentConfig::default();
183        if self.config.agent.analyze_goals {
184            agent_config.with_goal_analysis(true);
185        }
186        agent.with_config(agent_config);
187
188        info!("Agent '{}' created successfully", self.config.agent.name);
189        Ok(agent)
190    }
191
192    /// Create memory backend based on configuration
193    async fn create_memory_backend(&self) -> Result<Arc<dyn Memory>> {
194        let backend: Arc<dyn Memory> = match self.config.memory.backend.as_str() {
195            "in-memory" => {
196                debug!("Using in-memory storage");
197                Arc::new(InMemoryStore::new())
198            }
199            "file" => {
200                let path = self
201                    .config
202                    .memory_path(&self.base_dir)
203                    .context("Memory path required for file backend")?;
204
205                debug!("Using file storage at: {:?}", path);
206
207                // Create parent directory if it doesn't exist
208                if let Some(parent) = path.parent() {
209                    std::fs::create_dir_all(parent)
210                        .context("Failed to create memory directory")?;
211                }
212
213                Arc::new(
214                    FileStore::new(
215                        path.to_str().context("Invalid memory path")?,
216                        StorageFormat::Json,
217                    )
218                    .await
219                    .map_err(|e| anyhow::anyhow!("Failed to create file memory: {}", e))?,
220                )
221            }
222            "sqlite" => {
223                let path = self
224                    .config
225                    .memory_path(&self.base_dir)
226                    .context("Memory path required for sqlite backend")?;
227
228                debug!("Using SQLite storage at: {:?}", path);
229
230                // Create parent directory if it doesn't exist
231                if let Some(parent) = path.parent() {
232                    std::fs::create_dir_all(parent)
233                        .context("Failed to create memory directory")?;
234                }
235
236                Arc::new(
237                    SqliteStore::new(path.to_str().context("Invalid memory path")?)
238                        .await
239                        .map_err(|e| anyhow::anyhow!("Failed to create SQLite memory: {}", e))?,
240                )
241            }
242            backend => {
243                anyhow::bail!("Unknown memory backend: {}", backend);
244            }
245        };
246
247        Ok(backend)
248    }
249
250    /// Display output based on its type
251    fn display_output(&self, output: &OutputData) {
252        match output {
253            OutputData::Text(text) => {
254                println!("{}", text);
255            }
256            OutputData::File(_) => {
257                println!("{}", "📁 Received file data".blue().bold());
258            }
259            OutputData::Image(_) => {
260                println!("{}", "🖼️  Received image data".magenta().bold());
261            }
262            OutputData::Audio(_) => {
263                println!("{}", "🔊 Received audio data".cyan().bold());
264            }
265            OutputData::Video(_) => {
266                println!("{}", "🎬 Received video data".purple().bold());
267            }
268            OutputData::Raw(_) => {
269                println!("{}", "📦 Received raw binary data".dimmed());
270            }
271        }
272    }
273
274    /// Get the configuration
275    pub fn config(&self) -> &CeylonConfig {
276        &self.config
277    }
278}