universal_bot_core/
bot.rs1use 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#[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 #[instrument(skip(config))]
57 pub async fn new(config: BotConfig) -> Result<Self> {
58 info!("Initializing Universal Bot v{}", crate::VERSION);
59
60 config.validate().context("Invalid bot configuration")?;
62
63 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 bot.load_default_plugins();
86
87 info!("Bot initialized successfully");
88 Ok(bot)
89 }
90
91 #[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 let context = self
121 .context_manager
122 .get_or_create(&message.conversation_id)
123 .await
124 .context("Failed to get conversation context")?;
125
126 let message = self.apply_plugins_pre(message).await?;
128
129 let response = self
131 .pipeline
132 .process(message, context.clone())
133 .await
134 .context("Pipeline processing failed")?;
135
136 let response = self.apply_plugins_post(response).await?;
138
139 self.context_manager
141 .update(&response.conversation_id, context)
142 .await
143 .context("Failed to update context")?;
144
145 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 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 #[must_use]
175 pub fn config(&self) -> &BotConfig {
176 &self.config
177 }
178
179 #[must_use]
181 pub fn metrics(&self) -> &BotMetrics {
182 &self.metrics
183 }
184
185 #[allow(clippy::unused_self)]
188 fn load_default_plugins(&self) {
189 debug!("Loading default plugins");
190 }
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
207pub struct BotBuilder {
209 config: BotConfig,
210 plugins: Vec<Box<dyn crate::plugin::Plugin>>,
211}
212
213impl BotBuilder {
214 #[must_use]
216 pub fn new() -> Self {
217 Self {
218 config: BotConfig::default(),
219 plugins: Vec::new(),
220 }
221 }
222
223 #[must_use]
225 pub fn config(mut self, config: BotConfig) -> Self {
226 self.config = config;
227 self
228 }
229
230 #[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 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#[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 if times.len() > 1000 {
299 times.remove(0);
300 }
301 }
302
303 #[must_use]
305 pub fn requests_total(&self) -> u64 {
306 *self.requests_total.read()
307 }
308
309 #[must_use]
311 pub fn success_total(&self) -> u64 {
312 *self.success_total.read()
313 }
314
315 #[must_use]
317 pub fn errors_total(&self) -> u64 {
318 *self.errors_total.read()
319 }
320
321 #[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 #[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}