universal_bot_core/
bot.rs

1//! Bot implementation with AI model orchestration
2//!
3//! This module provides the main Bot struct that orchestrates AI interactions
4//! through various providers and manages the conversation lifecycle.
5
6use std::sync::Arc;
7
8use anyhow::{Context as _, Result};
9use parking_lot::RwLock;
10use tracing::{debug, info, instrument, warn};
11
12use crate::{
13    config::BotConfig,
14    context::ContextManager,
15    message::{Message, Response},
16    pipeline::MessagePipeline,
17    plugin::PluginRegistry,
18};
19
20/// The main Bot struct that handles all AI interactions
21///
22/// The Bot coordinates between different components:
23/// - Message pipeline for processing
24/// - Context manager for state
25/// - Plugin registry for extensions
26/// - AI providers for generation
27#[derive(Clone)]
28pub struct Bot {
29    config: Arc<BotConfig>,
30    pipeline: Arc<MessagePipeline>,
31    context_manager: Arc<ContextManager>,
32    plugin_registry: Arc<RwLock<PluginRegistry>>,
33    metrics: Arc<BotMetrics>,
34}
35
36impl Bot {
37    /// Create a new Bot instance with the given configuration
38    ///
39    /// # Errors
40    ///
41    /// Returns an error if initialization fails, such as:
42    /// - Invalid configuration
43    /// - Failed to connect to AI provider
44    /// - Plugin initialization failure
45    ///
46    /// # Example
47    ///
48    /// ```rust
49    /// # use universal_bot_core::{Bot, BotConfig};
50    /// # async fn example() -> anyhow::Result<()> {
51    /// let config = BotConfig::default();
52    /// let bot = Bot::new(config).await?;
53    /// # Ok(())
54    /// # }
55    /// ```
56    #[instrument(skip(config))]
57    pub async fn new(config: BotConfig) -> Result<Self> {
58        info!("Initializing Universal Bot v{}", crate::VERSION);
59
60        // Validate configuration
61        config.validate().context("Invalid bot configuration")?;
62
63        // Initialize components
64        let pipeline = MessagePipeline::new(&config)
65            .await
66            .context("Failed to create message pipeline")?;
67
68        let context_manager = ContextManager::new(config.context_config.clone())
69            .await
70            .context("Failed to create context manager")?;
71
72        let plugin_registry = PluginRegistry::new();
73
74        let metrics = BotMetrics::new();
75
76        let bot = Self {
77            config: Arc::new(config),
78            pipeline: Arc::new(pipeline),
79            context_manager: Arc::new(context_manager),
80            plugin_registry: Arc::new(RwLock::new(plugin_registry)),
81            metrics: Arc::new(metrics),
82        };
83
84        // Load default plugins
85        bot.load_default_plugins();
86
87        info!("Bot initialized successfully");
88        Ok(bot)
89    }
90
91    /// Process a message and generate a response
92    ///
93    /// This is the main entry point for all bot interactions.
94    ///
95    /// # Errors
96    ///
97    /// Returns an error if processing fails at any stage.
98    ///
99    /// # Example
100    ///
101    /// ```rust
102    /// # use universal_bot_core::{Bot, BotConfig, Message};
103    /// # async fn example() -> anyhow::Result<()> {
104    /// # let bot = Bot::new(BotConfig::default()).await?;
105    /// let message = Message::text("Hello, bot!");
106    /// let response = bot.process(message).await?;
107    /// println!("Bot says: {}", response.content);
108    /// # Ok(())
109    /// # }
110    /// ```
111    #[allow(clippy::future_not_send)]
112    #[instrument(skip(self, message), fields(message_id = %message.id))]
113    pub async fn process(&self, message: Message) -> Result<Response> {
114        let start = std::time::Instant::now();
115        self.metrics.increment_requests();
116
117        debug!("Processing message: {:?}", message.message_type);
118
119        // Get or create context
120        let context = self
121            .context_manager
122            .get_or_create(&message.conversation_id)
123            .await
124            .context("Failed to get conversation context")?;
125
126        // Apply plugins pre-processing
127        let message = self.apply_plugins_pre(message).await?;
128
129        // Process through pipeline
130        let response = self
131            .pipeline
132            .process(message, context.clone())
133            .await
134            .context("Pipeline processing failed")?;
135
136        // Apply plugins post-processing
137        let response = self.apply_plugins_post(response).await?;
138
139        // Update context
140        self.context_manager
141            .update(&response.conversation_id, context)
142            .await
143            .context("Failed to update context")?;
144
145        // Record metrics
146        let duration = start.elapsed();
147        self.metrics.record_response_time(duration);
148
149        if response.error.is_some() {
150            self.metrics.increment_errors();
151            warn!("Response contains error: {:?}", response.error);
152        } else {
153            self.metrics.increment_success();
154        }
155
156        debug!("Message processed in {:?}", duration);
157        Ok(response)
158    }
159
160    /// Register a plugin with the bot
161    ///
162    /// # Errors
163    ///
164    /// Returns an error if plugin registration fails.
165    pub fn register_plugin<P>(&self, plugin: P) -> Result<()>
166    where
167        P: crate::plugin::Plugin + 'static,
168    {
169        self.plugin_registry.write().register(Box::new(plugin))?;
170        Ok(())
171    }
172
173    /// Get the current bot configuration
174    #[must_use]
175    pub fn config(&self) -> &BotConfig {
176        &self.config
177    }
178
179    /// Get metrics for monitoring
180    #[must_use]
181    pub fn metrics(&self) -> &BotMetrics {
182        &self.metrics
183    }
184
185    // Private helper methods
186
187    #[allow(clippy::unused_self)]
188    fn load_default_plugins(&self) {
189        debug!("Loading default plugins");
190        // Load built-in plugins based on configuration
191        // This would load various default plugins
192    }
193
194    #[allow(clippy::future_not_send, clippy::await_holding_lock)]
195    async fn apply_plugins_pre(&self, message: Message) -> Result<Message> {
196        let registry = self.plugin_registry.read();
197        registry.apply_pre_processing(message).await
198    }
199
200    #[allow(clippy::future_not_send, clippy::await_holding_lock)]
201    async fn apply_plugins_post(&self, response: Response) -> Result<Response> {
202        let registry = self.plugin_registry.read();
203        registry.apply_post_processing(response).await
204    }
205}
206
207/// Builder for creating Bot instances with custom configuration
208pub struct BotBuilder {
209    config: BotConfig,
210    plugins: Vec<Box<dyn crate::plugin::Plugin>>,
211}
212
213impl BotBuilder {
214    /// Create a new builder with default configuration
215    #[must_use]
216    pub fn new() -> Self {
217        Self {
218            config: BotConfig::default(),
219            plugins: Vec::new(),
220        }
221    }
222
223    /// Set the bot configuration
224    #[must_use]
225    pub fn config(mut self, config: BotConfig) -> Self {
226        self.config = config;
227        self
228    }
229
230    /// Add a plugin to be registered on initialization
231    #[must_use]
232    pub fn plugin<P>(mut self, plugin: P) -> Self
233    where
234        P: crate::plugin::Plugin + 'static,
235    {
236        self.plugins.push(Box::new(plugin));
237        self
238    }
239
240    /// Build the Bot instance
241    ///
242    /// # Errors
243    ///
244    /// Returns an error if bot creation fails.
245    pub async fn build(self) -> Result<Bot> {
246        let bot = Bot::new(self.config).await?;
247
248        for plugin in self.plugins {
249            let mut registry = bot.plugin_registry.write();
250            registry.register(plugin)?;
251        }
252
253        Ok(bot)
254    }
255}
256
257impl Default for BotBuilder {
258    fn default() -> Self {
259        Self::new()
260    }
261}
262
263/// Metrics for monitoring bot performance
264#[derive(Debug)]
265pub struct BotMetrics {
266    requests_total: Arc<RwLock<u64>>,
267    success_total: Arc<RwLock<u64>>,
268    errors_total: Arc<RwLock<u64>>,
269    response_times: Arc<RwLock<Vec<std::time::Duration>>>,
270}
271
272impl BotMetrics {
273    fn new() -> Self {
274        Self {
275            requests_total: Arc::new(RwLock::new(0)),
276            success_total: Arc::new(RwLock::new(0)),
277            errors_total: Arc::new(RwLock::new(0)),
278            response_times: Arc::new(RwLock::new(Vec::new())),
279        }
280    }
281
282    fn increment_requests(&self) {
283        *self.requests_total.write() += 1;
284    }
285
286    fn increment_success(&self) {
287        *self.success_total.write() += 1;
288    }
289
290    fn increment_errors(&self) {
291        *self.errors_total.write() += 1;
292    }
293
294    fn record_response_time(&self, duration: std::time::Duration) {
295        let mut times = self.response_times.write();
296        times.push(duration);
297        // Keep only last 1000 response times
298        if times.len() > 1000 {
299            times.remove(0);
300        }
301    }
302
303    /// Get the total number of requests
304    #[must_use]
305    pub fn requests_total(&self) -> u64 {
306        *self.requests_total.read()
307    }
308
309    /// Get the total number of successful responses
310    #[must_use]
311    pub fn success_total(&self) -> u64 {
312        *self.success_total.read()
313    }
314
315    /// Get the total number of errors
316    #[must_use]
317    pub fn errors_total(&self) -> u64 {
318        *self.errors_total.read()
319    }
320
321    /// Get the average response time
322    #[must_use]
323    #[allow(clippy::cast_possible_truncation)]
324    pub fn average_response_time(&self) -> Option<std::time::Duration> {
325        let times = self.response_times.read();
326        if times.is_empty() {
327            return None;
328        }
329
330        let total: std::time::Duration = times.iter().sum();
331        Some(total / times.len() as u32)
332    }
333
334    /// Get the success rate as a percentage
335    #[must_use]
336    #[allow(clippy::cast_precision_loss)]
337    pub fn success_rate(&self) -> f64 {
338        let requests = self.requests_total();
339        if requests == 0 {
340            return 100.0;
341        }
342
343        let success = self.success_total();
344        (success as f64 / requests as f64) * 100.0
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[tokio::test]
353    async fn test_bot_creation() {
354        let config = BotConfig::default();
355        let bot = Bot::new(config).await;
356        assert!(bot.is_ok());
357    }
358
359    #[tokio::test]
360    async fn test_bot_builder() {
361        let bot = BotBuilder::new().config(BotConfig::default()).build().await;
362        assert!(bot.is_ok());
363    }
364
365    #[test]
366    fn test_metrics() {
367        let metrics = BotMetrics::new();
368
369        assert_eq!(metrics.requests_total(), 0);
370        assert_eq!(metrics.success_total(), 0);
371        assert_eq!(metrics.errors_total(), 0);
372        assert!((metrics.success_rate() - 100.0).abs() < f64::EPSILON);
373
374        metrics.increment_requests();
375        metrics.increment_success();
376        assert_eq!(metrics.requests_total(), 1);
377        assert_eq!(metrics.success_total(), 1);
378        assert!((metrics.success_rate() - 100.0).abs() < f64::EPSILON);
379
380        metrics.increment_requests();
381        metrics.increment_errors();
382        assert_eq!(metrics.requests_total(), 2);
383        assert_eq!(metrics.errors_total(), 1);
384        assert!((metrics.success_rate() - 50.0).abs() < f64::EPSILON);
385    }
386
387    #[test]
388    fn test_metrics_response_time() {
389        let metrics = BotMetrics::new();
390
391        assert!(metrics.average_response_time().is_none());
392
393        metrics.record_response_time(std::time::Duration::from_millis(100));
394        metrics.record_response_time(std::time::Duration::from_millis(200));
395
396        let avg = metrics.average_response_time().unwrap();
397        assert_eq!(avg, std::time::Duration::from_millis(150));
398    }
399
400    #[cfg(feature = "property-testing")]
401    mod property_tests {
402        use super::*;
403        use proptest::prelude::*;
404
405        proptest! {
406            #[test]
407            fn test_metrics_success_rate_bounds(
408                requests in 0u64..1000,
409                success in 0u64..1000
410            ) {
411                let metrics = BotMetrics::new();
412
413                for _ in 0..requests {
414                    metrics.increment_requests();
415                }
416
417                for _ in 0..success.min(requests) {
418                    metrics.increment_success();
419                }
420
421                let rate = metrics.success_rate();
422                prop_assert!(rate >= 0.0);
423                prop_assert!(rate <= 100.0);
424            }
425        }
426    }
427}