llm_memory_graph/plugin/mod.rs
1//! Plugin system for extending LLM-Memory-Graph functionality
2//!
3//! This module provides a flexible plugin architecture that allows users to extend
4//! the core functionality of LLM-Memory-Graph with custom behavior. Plugins can hook
5//! into various operations to provide:
6//!
7//! - **Validation**: Content validation and rule enforcement
8//! - **Enrichment**: Automatic metadata enhancement
9//! - **Transformation**: Data transformation and normalization
10//! - **Auditing**: Custom audit logging and compliance tracking
11//! - **Integration**: External system integration
12//!
13//! # Architecture
14//!
15//! The plugin system is designed around these core concepts:
16//!
17//! - **Plugin Trait**: The main interface all plugins must implement
18//! - **Plugin Context**: Provides plugins with access to operation data
19//! - **Hook Points**: Specific points in the execution flow where plugins are called
20//! - **Plugin Manager**: Manages plugin lifecycle and execution
21//!
22//! # Example
23//!
24//! ```rust
25//! use llm_memory_graph::plugin::{Plugin, PluginBuilder, PluginContext, PluginError, PluginMetadata};
26//! use async_trait::async_trait;
27//!
28//! struct MyValidationPlugin {
29//! metadata: PluginMetadata,
30//! }
31//!
32//! impl MyValidationPlugin {
33//! pub fn new() -> Self {
34//! let metadata = PluginBuilder::new("my_validator", "1.0.0")
35//! .author("Your Name")
36//! .description("Custom validation plugin")
37//! .capability("validation")
38//! .build();
39//!
40//! Self { metadata }
41//! }
42//! }
43//!
44//! #[async_trait]
45//! impl Plugin for MyValidationPlugin {
46//! fn metadata(&self) -> &PluginMetadata {
47//! &self.metadata
48//! }
49//!
50//! async fn before_create_node(&self, context: &PluginContext) -> Result<(), PluginError> {
51//! // Custom validation logic
52//! Ok(())
53//! }
54//! }
55//! ```
56
57use async_trait::async_trait;
58use serde_json::Value;
59use std::collections::HashMap;
60use std::fmt;
61
62pub mod hooks;
63pub mod manager;
64pub mod registry;
65
66pub use hooks::{HookExecutor, HookPoint, HookRegistry};
67pub use manager::PluginManager;
68pub use registry::{PluginDiscovery, PluginRegistry};
69
70/// Plugin error type
71#[derive(Debug, thiserror::Error)]
72pub enum PluginError {
73 /// Plugin initialization failed
74 #[error("Plugin initialization failed: {0}")]
75 InitFailed(String),
76
77 /// Plugin hook execution failed
78 #[error("Plugin hook execution failed: {0}")]
79 HookFailed(String),
80
81 /// Plugin not found
82 #[error("Plugin not found: {0}")]
83 NotFound(String),
84
85 /// Plugin version incompatible
86 #[error("Plugin version incompatible: {0}")]
87 VersionMismatch(String),
88
89 /// Plugin configuration error
90 #[error("Plugin configuration error: {0}")]
91 ConfigError(String),
92
93 /// Plugin already registered
94 #[error("Plugin already registered: {0}")]
95 AlreadyRegistered(String),
96
97 /// Plugin disabled
98 #[error("Plugin disabled: {0}")]
99 Disabled(String),
100
101 /// General plugin error
102 #[error("Plugin error: {0}")]
103 General(String),
104}
105
106/// Plugin metadata
107///
108/// Contains information about a plugin, including its name, version,
109/// author, description, and capabilities.
110#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
111pub struct PluginMetadata {
112 /// Plugin name (unique identifier)
113 pub name: String,
114
115 /// Plugin version (semantic versioning)
116 pub version: String,
117
118 /// Plugin author
119 pub author: String,
120
121 /// Plugin description
122 pub description: String,
123
124 /// API version this plugin is compatible with
125 pub api_version: String,
126
127 /// List of capabilities this plugin provides
128 pub capabilities: Vec<String>,
129
130 /// Optional plugin configuration schema
131 pub config_schema: Option<Value>,
132}
133
134impl fmt::Display for PluginMetadata {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 write!(
137 f,
138 "{} v{} by {} - {} (API: {})",
139 self.name, self.version, self.author, self.description, self.api_version
140 )
141 }
142}
143
144/// Plugin context for hooks
145///
146/// Provides plugins with information about the current operation,
147/// including the operation type, data being processed, and metadata.
148#[derive(Debug, Clone)]
149pub struct PluginContext {
150 /// Operation being performed (e.g., "create_node", "create_session")
151 pub operation: String,
152
153 /// Data associated with the operation (JSON format)
154 pub data: Value,
155
156 /// Additional metadata (key-value pairs)
157 pub metadata: HashMap<String, String>,
158
159 /// Timestamp when the context was created
160 pub timestamp: chrono::DateTime<chrono::Utc>,
161}
162
163impl PluginContext {
164 /// Create a new plugin context
165 pub fn new(operation: impl Into<String>, data: Value) -> Self {
166 Self {
167 operation: operation.into(),
168 data,
169 metadata: HashMap::new(),
170 timestamp: chrono::Utc::now(),
171 }
172 }
173
174 /// Add metadata to the context
175 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
176 self.metadata.insert(key.into(), value.into());
177 self
178 }
179
180 /// Get a metadata value
181 pub fn get_metadata(&self, key: &str) -> Option<&String> {
182 self.metadata.get(key)
183 }
184
185 /// Set a metadata value
186 pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
187 self.metadata.insert(key.into(), value.into());
188 }
189
190 /// Get the operation type
191 pub fn operation(&self) -> &str {
192 &self.operation
193 }
194
195 /// Get the data
196 pub fn data(&self) -> &Value {
197 &self.data
198 }
199}
200
201/// Plugin trait - all plugins must implement this
202///
203/// Plugins can selectively implement hooks they're interested in.
204/// All hooks are async and return a Result to allow for error handling.
205#[async_trait]
206pub trait Plugin: Send + Sync {
207 /// Get plugin metadata
208 fn metadata(&self) -> &PluginMetadata;
209
210 /// Initialize plugin
211 ///
212 /// Called once when the plugin is registered and enabled.
213 /// Use this to set up any resources, connections, or state.
214 /// Plugins should use interior mutability (e.g., Mutex, RwLock) for any state changes.
215 async fn init(&self) -> Result<(), PluginError> {
216 Ok(())
217 }
218
219 /// Shutdown plugin
220 ///
221 /// Called when the plugin is being disabled or the system is shutting down.
222 /// Use this to clean up resources, close connections, etc.
223 /// Plugins should use interior mutability (e.g., Mutex, RwLock) for any state changes.
224 async fn shutdown(&self) -> Result<(), PluginError> {
225 Ok(())
226 }
227
228 /// Hook: Before node creation
229 ///
230 /// Called before a node is created in the graph.
231 /// Can be used for validation, transformation, or enrichment.
232 async fn before_create_node(&self, _context: &PluginContext) -> Result<(), PluginError> {
233 Ok(())
234 }
235
236 /// Hook: After node creation
237 ///
238 /// Called after a node is successfully created.
239 /// Can be used for logging, notifications, or follow-up actions.
240 async fn after_create_node(&self, _context: &PluginContext) -> Result<(), PluginError> {
241 Ok(())
242 }
243
244 /// Hook: Before session creation
245 ///
246 /// Called before a session is created.
247 /// Can be used for validation, quota checking, or initialization.
248 async fn before_create_session(&self, _context: &PluginContext) -> Result<(), PluginError> {
249 Ok(())
250 }
251
252 /// Hook: After session creation
253 ///
254 /// Called after a session is successfully created.
255 /// Can be used for registration, logging, or setup.
256 async fn after_create_session(&self, _context: &PluginContext) -> Result<(), PluginError> {
257 Ok(())
258 }
259
260 /// Hook: Before query execution
261 ///
262 /// Called before a query is executed.
263 /// Can be used for query validation, transformation, or access control.
264 async fn before_query(&self, _context: &PluginContext) -> Result<(), PluginError> {
265 Ok(())
266 }
267
268 /// Hook: After query execution
269 ///
270 /// Called after a query is successfully executed.
271 /// Can be used for result transformation, caching, or logging.
272 async fn after_query(&self, _context: &PluginContext) -> Result<(), PluginError> {
273 Ok(())
274 }
275
276 /// Hook: Before edge creation
277 ///
278 /// Called before an edge is created in the graph.
279 /// Can be used for relationship validation or enforcement.
280 async fn before_create_edge(&self, _context: &PluginContext) -> Result<(), PluginError> {
281 Ok(())
282 }
283
284 /// Hook: After edge creation
285 ///
286 /// Called after an edge is successfully created.
287 /// Can be used for graph analysis or notifications.
288 async fn after_create_edge(&self, _context: &PluginContext) -> Result<(), PluginError> {
289 Ok(())
290 }
291
292 /// Generic hook execution (before)
293 ///
294 /// Routes to the appropriate before hook based on the hook name.
295 async fn before_hook(
296 &self,
297 hook_name: &str,
298 context: &PluginContext,
299 ) -> Result<(), PluginError> {
300 match hook_name {
301 "before_create_node" => self.before_create_node(context).await,
302 "before_create_session" => self.before_create_session(context).await,
303 "before_query" => self.before_query(context).await,
304 "before_create_edge" => self.before_create_edge(context).await,
305 _ => Ok(()),
306 }
307 }
308
309 /// Generic hook execution (after)
310 ///
311 /// Routes to the appropriate after hook based on the hook name.
312 async fn after_hook(
313 &self,
314 hook_name: &str,
315 context: &PluginContext,
316 ) -> Result<(), PluginError> {
317 match hook_name {
318 "after_create_node" => self.after_create_node(context).await,
319 "after_create_session" => self.after_create_session(context).await,
320 "after_query" => self.after_query(context).await,
321 "after_create_edge" => self.after_create_edge(context).await,
322 _ => Ok(()),
323 }
324 }
325}
326
327/// Plugin builder for configuration
328///
329/// Provides a fluent API for building plugin metadata.
330///
331/// # Example
332///
333/// ```rust
334/// use llm_memory_graph::plugin::PluginBuilder;
335///
336/// let metadata = PluginBuilder::new("my_plugin", "1.0.0")
337/// .author("John Doe")
338/// .description("My custom plugin")
339/// .capability("validation")
340/// .capability("enrichment")
341/// .build();
342/// ```
343pub struct PluginBuilder {
344 metadata: PluginMetadata,
345}
346
347impl PluginBuilder {
348 /// Create a new plugin builder
349 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
350 Self {
351 metadata: PluginMetadata {
352 name: name.into(),
353 version: version.into(),
354 author: String::new(),
355 description: String::new(),
356 api_version: "1.0.0".to_string(),
357 capabilities: Vec::new(),
358 config_schema: None,
359 },
360 }
361 }
362
363 /// Set the plugin author
364 pub fn author(mut self, author: impl Into<String>) -> Self {
365 self.metadata.author = author.into();
366 self
367 }
368
369 /// Set the plugin description
370 pub fn description(mut self, description: impl Into<String>) -> Self {
371 self.metadata.description = description.into();
372 self
373 }
374
375 /// Set the API version
376 pub fn api_version(mut self, version: impl Into<String>) -> Self {
377 self.metadata.api_version = version.into();
378 self
379 }
380
381 /// Add a capability
382 pub fn capability(mut self, capability: impl Into<String>) -> Self {
383 self.metadata.capabilities.push(capability.into());
384 self
385 }
386
387 /// Set the configuration schema
388 pub fn config_schema(mut self, schema: Value) -> Self {
389 self.metadata.config_schema = Some(schema);
390 self
391 }
392
393 /// Build the plugin metadata
394 pub fn build(self) -> PluginMetadata {
395 self.metadata
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn test_plugin_builder() {
405 let metadata = PluginBuilder::new("test_plugin", "1.0.0")
406 .author("Test Author")
407 .description("Test plugin")
408 .capability("validation")
409 .capability("enrichment")
410 .build();
411
412 assert_eq!(metadata.name, "test_plugin");
413 assert_eq!(metadata.version, "1.0.0");
414 assert_eq!(metadata.author, "Test Author");
415 assert_eq!(metadata.capabilities.len(), 2);
416 }
417
418 #[test]
419 fn test_plugin_context() {
420 let context = PluginContext::new("test_operation", serde_json::json!({"key": "value"}))
421 .with_metadata("test_key", "test_value");
422
423 assert_eq!(context.operation(), "test_operation");
424 assert_eq!(
425 context.get_metadata("test_key"),
426 Some(&"test_value".to_string())
427 );
428 }
429}