ceylon_next/runner/
runner.rs1use 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
16pub struct AgentRunner {
18 config: CeylonConfig,
19 base_dir: std::path::PathBuf,
20}
21
22impl AgentRunner {
23 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 pub fn from_current_dir() -> Result<Self> {
39 Self::new("./ceylon.toml")
40 }
41
42 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 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 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 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 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 async fn create_agent(&self) -> Result<Agent> {
155 debug!("Creating agent with model: {}", self.config.agent.model);
156
157 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 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 if let Some(prompt) = self.config.read_system_prompt(&self.base_dir)? {
174 agent.with_system_prompt(&prompt);
175 }
176
177 let memory_backend = self.create_memory_backend().await?;
179 agent.with_memory(memory_backend);
180
181 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 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 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 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 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 pub fn config(&self) -> &CeylonConfig {
276 &self.config
277 }
278}