| … | … | @@ -0,0 +1,3 @@ |
| 1 | [submodule "ollamatools-project/ollama-matrix-client"] | |
| 2 | path = ollamatools-project/ollama-matrix-client | |
| 3 | url = https://github.com/arankwende/ollama-matrix-client.git |
| … | … | @@ -5,6 +5,81 @@ All notable changes to the OllamaTools project will be documented in this file. |
| 5 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), |
| 6 | 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). |
| 7 | 7 | |
| 8 | ## [1.4.0] - 2025-05-15 | |
| 9 | ||
| 10 | ### Added | |
| 11 | - Modular structure for enhanced Matrix client: | |
| 12 | - Split functionality into multiple modules for better maintainability: | |
| 13 | - `enhanced_matrix_core.py`: Core client functionality | |
| 14 | - `enhanced_matrix_search.py`: Search-related functionality | |
| 15 | - `enhanced_matrix_history.py`: Conversation history and analytics | |
| 16 | - `enhanced_matrix_tools.py`: Main module that integrates everything | |
| 17 | - Added new synchronous wrapper functions for enhanced client: | |
| 18 | - `join_enhanced_matrix_room` | |
| 19 | - `send_enhanced_matrix_message` | |
| 20 | - `listen_for_enhanced_matrix_messages` | |
| 21 | - `disconnect_enhanced_matrix` | |
| 22 | - `process_with_enhanced_ollama` | |
| 23 | - Improved error handling and resource cleanup | |
| 24 | - Better handling of optional dependencies with graceful degradation | |
| 25 | - Enhanced documentation with modular structure details | |
| 26 | - Updated examples to use the new modular structure | |
| 27 | ||
| 28 | ## [1.3.0] - 2025-05-10 | |
| 29 | ||
| 30 | ### Added | |
| 31 | - Enhanced Matrix client with modular agent functionality: | |
| 32 | - New `enhanced_matrix_tools.py` module with `EnhancedMatrixClient` class | |
| 33 | - Improved message response logic with better mention detection | |
| 34 | - Natural language understanding from modular agent | |
| 35 | - Search integration with OpenSearchNG | |
| 36 | - Conversation history tracking and summarization | |
| 37 | - Topic analysis for conversation insights | |
| 38 | - Performance metrics tracking | |
| 39 | - Additional commands: `/search`, `/history`, `/topics`, `/metrics` | |
| 40 | - Option to respond to all messages (not just mentions) | |
| 41 | - Enhanced configuration with `create_enhanced_matrix_config` | |
| 42 | - Updated example in `examples/enhanced_matrix_client_example.py` | |
| 43 | - Updated configuration template in `examples/config/enhanced_matrix_config.yaml` | |
| 44 | - Expanded documentation in `docs/matrix_integration.md` | |
| 45 | ||
| 46 | ## [1.2.0] - 2025-05-01 | |
| 47 | ||
| 48 | ### Added | |
| 49 | - Enhanced Matrix chat integration: | |
| 50 | - Model selection support with `/model` command | |
| 51 | - User context tracking for coherent conversations | |
| 52 | - Admin user management for privileged operations | |
| 53 | - Command system with `/help` and `/models` commands | |
| 54 | - Random greeting messages when joining rooms | |
| 55 | - Automatic model discovery from Ollama server | |
| 56 | - Enhanced example in `examples/enhanced_matrix_client_example.py` | |
| 57 | - Updated configuration template in `examples/config/enhanced_matrix_config.yaml` | |
| 58 | - Expanded documentation in `docs/matrix_integration.md` | |
| 59 | ||
| 60 | ## [1.1.0] - 2025-04-25 | |
| 61 | ||
| 62 | ### Added | |
| 63 | - Matrix chat integration: | |
| 64 | - New `matrix_tools.py` module for Matrix protocol support | |
| 65 | - `MatrixClient` class for connecting to Matrix homeservers | |
| 66 | - Functions for joining rooms, sending messages, and listening for events | |
| 67 | - Integration with Ollama for processing Matrix messages | |
| 68 | - Configuration management for Matrix settings | |
| 69 | - End-to-end encryption support via matrix-nio[e2e] | |
| 70 | - Example script in `examples/matrix_client_example.py` | |
| 71 | - Default configuration template in `examples/config/matrix_config.yaml` | |
| 72 | - Comprehensive documentation in `docs/matrix_integration.md` | |
| 73 | - Dependencies: | |
| 74 | - Added matrix-nio for Matrix protocol support | |
| 75 | - Added pyyaml for configuration handling | |
| 76 | - Made ollama a direct dependency instead of optional | |
| 77 | ||
| 78 | ### Changed | |
| 79 | - Updated `tools/__init__.py` to include Matrix tools | |
| 80 | - Updated requirements.txt with new dependencies | |
| 81 | - Added Matrix integration as an optional extra in setup.py | |
| 82 | ||
| 8 | 83 | ## [1.0.0] - 2025-04-20 |
| 9 | 84 | |
| 10 | 85 | ### Breaking Changes |
| … | … | @@ -0,0 +1,193 @@ |
| 1 | # Enhanced Matrix Client with Modular Agent Functionality - Completion Report | |
| 2 | ||
| 3 | ## Overview | |
| 4 | ||
| 5 | This report documents the implementation of the Enhanced Matrix Client with Modular Agent functionality in OllamaTools. The enhanced client extends the basic Matrix client with advanced features from the modular and extended agents, addressing issues with message response logic and adding powerful new capabilities. | |
| 6 | ||
| 7 | ## Latest Update: Modular Structure Implementation | |
| 8 | ||
| 9 | The Enhanced Matrix Client has been refactored into a modular structure to improve maintainability, extensibility, and to address file size and context limitations. The functionality has been split into multiple modules: | |
| 10 | ||
| 11 | 1. **enhanced_matrix_core.py**: Contains the core `EnhancedMatrixClient` class and basic functionality | |
| 12 | 2. **enhanced_matrix_search.py**: Contains search-related functionality | |
| 13 | 3. **enhanced_matrix_history.py**: Contains conversation history tracking and analytics | |
| 14 | 4. **enhanced_matrix_tools.py**: Main module that imports and re-exports everything | |
| 15 | ||
| 16 | This modular structure provides several benefits: | |
| 17 | - Better code organization and maintainability | |
| 18 | - Easier to extend specific functionality without affecting other parts | |
| 19 | - Reduced file sizes to avoid context limitations | |
| 20 | - Clearer separation of concerns | |
| 21 | - Improved error handling and resource cleanup | |
| 22 | - Better handling of optional dependencies with graceful degradation | |
| 23 | ||
| 24 | Additional synchronous wrapper functions have been added to provide a complete API: | |
| 25 | - `join_enhanced_matrix_room` | |
| 26 | - `send_enhanced_matrix_message` | |
| 27 | - `listen_for_enhanced_matrix_messages` | |
| 28 | - `disconnect_enhanced_matrix` | |
| 29 | - `process_with_enhanced_ollama` | |
| 30 | ||
| 31 | The documentation has been updated to reflect the new modular structure, and the example code has been updated to use the new API. | |
| 32 | ||
| 33 | ## Completed Enhancements | |
| 34 | ||
| 35 | ### 1. Improved Message Response Logic | |
| 36 | ||
| 37 | - Enhanced username detection with case-insensitive matching | |
| 38 | - Added direct mention detection for full Matrix IDs (e.g., @username:server.com) | |
| 39 | - Implemented option to respond to all messages (not just mentions) | |
| 40 | - Fixed issues with message response inconsistency | |
| 41 | ||
| 42 | ### 2. Natural Language Understanding | |
| 43 | ||
| 44 | - Integrated factual intent detection from modular agent | |
| 45 | - Added command intent detection for better understanding of user requests | |
| 46 | - Implemented semantic function selection feedback system | |
| 47 | ||
| 48 | ### 3. Search Integration | |
| 49 | ||
| 50 | - Added web search capability using OpenSearchNG | |
| 51 | - Implemented `/search` command for direct search queries | |
| 52 | - Added automatic search for messages with factual intent | |
| 53 | - Integrated search context into message processing | |
| 54 | ||
| 55 | ### 4. Conversation History Tracking | |
| 56 | ||
| 57 | - Implemented conversation history storage and retrieval | |
| 58 | - Added `/history` command to view recent conversations | |
| 59 | - Created conversation summarization functionality | |
| 60 | - Implemented persistent storage of conversation history | |
| 61 | ||
| 62 | ### 5. Topic Analysis | |
| 63 | ||
| 64 | - Added conversation topic analysis based on keywords | |
| 65 | - Implemented `/topics` command to view conversation topics | |
| 66 | - Created topic frequency tracking for insights | |
| 67 | ||
| 68 | ### 6. Performance Metrics | |
| 69 | ||
| 70 | - Added response time tracking for performance monitoring | |
| 71 | - Implemented token usage estimation | |
| 72 | - Added `/metrics` command to view performance metrics | |
| 73 | - Created tracking for slowest and fastest responses | |
| 74 | ||
| 75 | ### 7. Enhanced Configuration | |
| 76 | ||
| 77 | - Created `create_enhanced_matrix_config` function for default configuration | |
| 78 | - Added enhanced configuration options in YAML format | |
| 79 | - Implemented configuration for all new features | |
| 80 | - Updated example configuration file | |
| 81 | ||
| 82 | ### 8. Documentation and Examples | |
| 83 | ||
| 84 | - Updated `docs/matrix_integration.md` with comprehensive documentation | |
| 85 | - Enhanced `examples/enhanced_matrix_client_example.py` to use new features | |
| 86 | - Updated `examples/config/enhanced_matrix_config.yaml` with new options | |
| 87 | - Added entries to `CHANGELOG.md` for the new features | |
| 88 | ||
| 89 | ## Implementation Details | |
| 90 | ||
| 91 | ### EnhancedMatrixClient Class | |
| 92 | ||
| 93 | The core of the implementation is the `EnhancedMatrixClient` class in `ollamatools/tools/enhanced_matrix_tools.py`. This class extends the base `MatrixClient` class with the following enhancements: | |
| 94 | ||
| 95 | 1. **Improved Message Callback**: | |
| 96 | - Enhanced message detection logic | |
| 97 | - Integration with search functionality | |
| 98 | - Performance tracking | |
| 99 | - Conversation history recording | |
| 100 | ||
| 101 | 2. **Enhanced Command Handling**: | |
| 102 | - Added new commands: `/search`, `/history`, `/topics`, `/metrics` | |
| 103 | - Improved help system with conditional command listing | |
| 104 | - Better command parsing with regex patterns | |
| 105 | ||
| 106 | 3. **Search Integration**: | |
| 107 | - Implemented `_execute_search` method for web search | |
| 108 | - Added search context augmentation for messages | |
| 109 | - Created search result formatting | |
| 110 | ||
| 111 | 4. **Conversation Tracking**: | |
| 112 | - Implemented `_record_conversation` method for history tracking | |
| 113 | - Added conversation loading and saving to persistent storage | |
| 114 | - Created conversation analysis methods | |
| 115 | ||
| 116 | ### Module API | |
| 117 | ||
| 118 | The module now provides a comprehensive API with the following functions: | |
| 119 | ||
| 120 | 1. **Connection Functions**: | |
| 121 | - `connect_enhanced_matrix`: Connect to a Matrix homeserver with an enhanced client | |
| 122 | - `disconnect_enhanced_matrix`: Properly disconnect and clean up resources | |
| 123 | ||
| 124 | 2. **Room Interaction Functions**: | |
| 125 | - `join_enhanced_matrix_room`: Join a Matrix room with an enhanced client | |
| 126 | - `send_enhanced_matrix_message`: Send a message to a Matrix room | |
| 127 | - `listen_for_enhanced_matrix_messages`: Start listening for messages in joined rooms | |
| 128 | ||
| 129 | 3. **Message Processing Functions**: | |
| 130 | - `process_with_enhanced_ollama`: Process a message with Ollama using an enhanced client | |
| 131 | ||
| 132 | 4. **Configuration Functions**: | |
| 133 | - `create_enhanced_matrix_config`: Create a default configuration for the enhanced client | |
| 134 | ||
| 135 | 5. **Status Flags**: | |
| 136 | - `MODULAR_AGENT_AVAILABLE`: Flag indicating if modular agent components are available | |
| 137 | - `OPENSEARCH_AVAILABLE`: Flag indicating if OpenSearchNG is available | |
| 138 | ||
| 139 | Each module also provides specialized functions: | |
| 140 | ||
| 141 | - **enhanced_matrix_search.py**: | |
| 142 | - `execute_search_query`: Execute a search query using OpenSearchNG | |
| 143 | - `execute_search_for_message`: Execute a search for a message and return the search context | |
| 144 | - `handle_search_command`: Handle the /search command | |
| 145 | ||
| 146 | - **enhanced_matrix_history.py**: | |
| 147 | - `load_conversation_history`: Load conversation history from file | |
| 148 | - `save_conversation_history`: Save conversation history to file | |
| 149 | - `record_conversation`: Record a conversation in the history | |
| 150 | - `analyze_conversation_topics`: Analyze common topics in conversation history | |
| 151 | - `get_performance_metrics`: Get performance metrics from conversation history | |
| 152 | ||
| 153 | ## Future Work | |
| 154 | ||
| 155 | While the current implementation provides a solid foundation for the enhanced Matrix client, there are several areas for future improvement: | |
| 156 | ||
| 157 | 1. **Streaming Responses**: | |
| 158 | - Implement streaming for longer responses from Ollama | |
| 159 | - Add progress indicators during model processing | |
| 160 | ||
| 161 | 2. **Enhanced Admin Controls**: | |
| 162 | - Add admin-only commands for system management | |
| 163 | - Implement room-specific model restrictions | |
| 164 | - Add user management commands | |
| 165 | ||
| 166 | 3. **Performance Optimizations**: | |
| 167 | - Optimize context storage for large deployments | |
| 168 | - Implement caching for frequently used models | |
| 169 | - Add batch processing for high-traffic rooms | |
| 170 | ||
| 171 | 4. **Security Enhancements**: | |
| 172 | - Add rate limiting to prevent abuse | |
| 173 | - Implement more granular permission controls | |
| 174 | - Add encryption for stored contexts | |
| 175 | ||
| 176 | 5. **Integration with Other Agents**: | |
| 177 | - Add integration with other agent types | |
| 178 | - Implement agent selection based on message content | |
| 179 | - Create hybrid agent capabilities | |
| 180 | ||
| 181 | ## Conclusion | |
| 182 | ||
| 183 | The Enhanced Matrix Client with Modular Agent functionality significantly improves the user experience by addressing issues with message response logic and adding powerful new capabilities from the modular and extended agents. The implementation follows best practices for code organization, error handling, and documentation, providing a solid foundation for future improvements. | |
| 184 | ||
| 185 | The recent refactoring into a modular structure has further enhanced the codebase by: | |
| 186 | 1. Improving maintainability through clear separation of concerns | |
| 187 | 2. Enhancing extensibility by making it easier to modify specific components | |
| 188 | 3. Solving file size and context limitations by splitting functionality into multiple files | |
| 189 | 4. Providing better error handling and resource cleanup | |
| 190 | 5. Offering a more comprehensive API with additional synchronous wrapper functions | |
| 191 | 6. Handling optional dependencies more gracefully with clear fallback mechanisms | |
| 192 | ||
| 193 | The enhanced client is now ready for use in production environments and provides a more robust and feature-rich experience for Matrix users. The modular structure ensures that the codebase can continue to evolve and grow without running into the limitations that were previously encountered. |
| … | … | @@ -0,0 +1,133 @@ |
| 1 | # Matrix Integration Enhancement Completion Report | |
| 2 | ||
| 3 | ## Overview | |
| 4 | ||
| 5 | This report documents the enhancements made to the Matrix integration in OllamaTools. The enhancements focus on adding model selection capabilities, user context tracking, and admin user management to the Matrix client. | |
| 6 | ||
| 7 | ## Completed Enhancements | |
| 8 | ||
| 9 | ### 1. Model Selection Support | |
| 10 | ||
| 11 | - Added command-based model switching with `/model <model_name>` command | |
| 12 | - Implemented model listing with `/models` command | |
| 13 | - Added help system with `/help` command | |
| 14 | - Created model validation to ensure requested models are available | |
| 15 | - Added automatic model discovery from Ollama server | |
| 16 | - Implemented configuration-based model specification | |
| 17 | ||
| 18 | ### 2. User Context Tracking | |
| 19 | ||
| 20 | - Added conversation history tracking for each user in each room | |
| 21 | - Implemented context management with configurable context length | |
| 22 | - Created efficient context storage using user and room identifiers | |
| 23 | - Added automatic context pruning to maintain performance | |
| 24 | - Ensured context is properly maintained across model switches | |
| 25 | ||
| 26 | ### 3. Admin User Management | |
| 27 | ||
| 28 | - Added admin user designation in configuration | |
| 29 | - Implemented admin user management methods | |
| 30 | - Created foundation for future admin-only commands | |
| 31 | ||
| 32 | ### 4. Random Greeting Feature | |
| 33 | ||
| 34 | - Added random greeting functionality when joining rooms | |
| 35 | - Implemented 10 different greeting messages | |
| 36 | - Ensured greetings are sent only on successful room joins | |
| 37 | ||
| 38 | ### 5. Documentation and Examples | |
| 39 | ||
| 40 | - Updated `docs/matrix_integration.md` with comprehensive documentation of new features | |
| 41 | - Created enhanced example in `examples/enhanced_matrix_client_example.py` | |
| 42 | - Added sample configuration in `examples/config/enhanced_matrix_config.yaml` | |
| 43 | - Updated CHANGELOG.md to document the enhancements | |
| 44 | ||
| 45 | ## Implementation Details | |
| 46 | ||
| 47 | ### Client Example Enhancements | |
| 48 | ||
| 49 | The Matrix client examples were enhanced with: | |
| 50 | ||
| 51 | 1. **Random Greeting System**: | |
| 52 | - Added `get_random_greeting()` function to generate varied greetings | |
| 53 | - Modified room joining process to send greetings after successful joins | |
| 54 | - Implemented logging of sent greetings for debugging | |
| 55 | ||
| 56 | ### MatrixClient Class Enhancements | |
| 57 | ||
| 58 | The `MatrixClient` class in `ollamatools/tools/matrix_tools.py` was enhanced with: | |
| 59 | ||
| 60 | 1. **Command System**: | |
| 61 | - Added regex patterns for command matching | |
| 62 | - Implemented command handling in `_handle_command` method | |
| 63 | - Created specific handlers for each command type | |
| 64 | ||
| 65 | 2. **Model Management**: | |
| 66 | - Added `available_models` parameter to store available models | |
| 67 | - Implemented `get_available_models` method to discover models from Ollama | |
| 68 | - Created `update_available_models` method to refresh model list | |
| 69 | - Added `_change_model` method to handle model switching | |
| 70 | ||
| 71 | 3. **Context Tracking**: | |
| 72 | - Added `user_contexts` dictionary to store conversation history | |
| 73 | - Enhanced `process_with_ollama` to maintain context | |
| 74 | - Implemented context pruning to limit memory usage | |
| 75 | - Added context clearing when switching models | |
| 76 | ||
| 77 | 4. **Admin Management**: | |
| 78 | - Added `admin_users` parameter to designate admin users | |
| 79 | - Implemented `add_admin_user` and `remove_admin_user` methods | |
| 80 | - Created foundation for admin-only commands | |
| 81 | ||
| 82 | ### Configuration Enhancements | |
| 83 | ||
| 84 | The configuration system was enhanced to support: | |
| 85 | ||
| 86 | 1. **Model Configuration**: | |
| 87 | - Added `AVAILABLE_MODELS` list in the Ollama configuration section | |
| 88 | - Updated default configuration to include sample models | |
| 89 | ||
| 90 | 2. **Admin Configuration**: | |
| 91 | - Added `ADMIN_USERS` list in the Matrix configuration section | |
| 92 | - Updated default configuration to include sample admin users | |
| 93 | ||
| 94 | ### API Enhancements | |
| 95 | ||
| 96 | The module API was enhanced to support: | |
| 97 | ||
| 98 | 1. **New Parameters**: | |
| 99 | - Added `available_models` parameter to `connect_to_matrix` function | |
| 100 | - Added `admin_users` parameter to `connect_to_matrix` function | |
| 101 | ||
| 102 | 2. **Enhanced Process Method**: | |
| 103 | - Updated `process_with_ollama` to accept sender and room_id parameters | |
| 104 | - Modified message handling to use enhanced processing | |
| 105 | ||
| 106 | ## Future Work | |
| 107 | ||
| 108 | While the current enhancements provide a solid foundation for model selection and user context tracking, there are several areas for future improvement: | |
| 109 | ||
| 110 | 1. **Streaming Responses**: | |
| 111 | - Implement streaming for longer responses from Ollama | |
| 112 | - Add progress indicators during model processing | |
| 113 | ||
| 114 | 2. **Enhanced Admin Controls**: | |
| 115 | - Add admin-only commands for system management | |
| 116 | - Implement room-specific model restrictions | |
| 117 | - Add user management commands | |
| 118 | ||
| 119 | 3. **Performance Optimizations**: | |
| 120 | - Optimize context storage for large deployments | |
| 121 | - Implement caching for frequently used models | |
| 122 | - Add batch processing for high-traffic rooms | |
| 123 | ||
| 124 | 4. **Security Enhancements**: | |
| 125 | - Add rate limiting to prevent abuse | |
| 126 | - Implement more granular permission controls | |
| 127 | - Add encryption for stored contexts | |
| 128 | ||
| 129 | ## Conclusion | |
| 130 | ||
| 131 | The enhancements to the Matrix integration significantly improve the user experience by allowing model selection and maintaining conversation context. These features make the Matrix client more versatile and useful for real-world applications. | |
| 132 | ||
| 133 | The implementation follows best practices for code organization, error handling, and documentation. The enhanced Matrix client is now ready for use in production environments and provides a solid foundation for future improvements. |
| … | … | @@ -0,0 +1,250 @@ |
| 1 | # OllamaTools Implementation Issues and Solutions | |
| 2 | ||
| 3 | After a thorough code review of the OllamaTools project, focusing on the integration between matrix, modular, and extended agent libraries, I've identified several specific implementation issues that need to be addressed to achieve the unified plugin architecture proposed in the documentation. | |
| 4 | ||
| 5 | ## 1. Improper Module Organization for ExtendedModularAgent | |
| 6 | ||
| 7 | **Issue:** The `ExtendedModularAgent` is defined in an example file (`examples/extended_modular_agent.py`) rather than as a proper module in the package structure, violating package organization principles. | |
| 8 | ||
| 9 | **Prompt Plan:** | |
| 10 | ``` | |
| 11 | Create a proper module structure for the ExtendedModularAgent by: | |
| 12 | ||
| 13 | 1. Create a new file at ollamatools/agent/extended/agent.py | |
| 14 | 2. Move the ExtendedModularAgent class from examples/extended_modular_agent.py to this new file | |
| 15 | 3. Add proper imports and exports in ollamatools/agent/extended/__init__.py to expose the ExtendedModularAgent | |
| 16 | 4. Update the examples/extended_modular_agent.py file to import from the new module location | |
| 17 | 5. Ensure all functionality is preserved with the same API | |
| 18 | 6. Add proper docstrings and type hints to the new module | |
| 19 | ``` | |
| 20 | ||
| 21 | ## 2. Duplicated Conversation History Functionality | |
| 22 | ||
| 23 | **Issue:** There is significant duplication between `enhanced_matrix_history.py` and the `ExtendedModularAgent` class, particularly in conversation history tracking, topic analysis, and performance metrics. | |
| 24 | ||
| 25 | **Prompt Plan:** | |
| 26 | ``` | |
| 27 | Create a shared ConversationHistoryPlugin that can be used by both the Matrix client and agent components: | |
| 28 | ||
| 29 | 1. Create a new file at ollamatools/plugins/conversation_history.py | |
| 30 | 2. Implement a ConversationHistoryPlugin class that follows the SharedPlugin interface from the unified architecture | |
| 31 | 3. Extract the common functionality from enhanced_matrix_history.py and ExtendedModularAgent: | |
| 32 | - Conversation loading/saving | |
| 33 | - Recording conversations | |
| 34 | - Topic analysis | |
| 35 | - Performance metrics | |
| 36 | 4. Add proper configuration options for file paths, history limits, etc. | |
| 37 | 5. Implement the initialize(), shutdown(), and get_capabilities() methods | |
| 38 | 6. Add unit tests for the new plugin | |
| 39 | 7. Update the ExtendedModularAgent to use this plugin instead of its own implementation | |
| 40 | 8. Update the EnhancedMatrixClient to use this plugin instead of the functions in enhanced_matrix_history.py | |
| 41 | ``` | |
| 42 | ||
| 43 | ## 3. Inconsistent Plugin Architecture in Matrix Integration | |
| 44 | ||
| 45 | **Issue:** The modular agent has a well-defined plugin architecture, but this isn't consistently applied across the matrix integration, leading to ad-hoc integration points and conditional imports. | |
| 46 | ||
| 47 | **Prompt Plan:** | |
| 48 | ``` | |
| 49 | Refactor the Matrix integration to use the plugin architecture: | |
| 50 | ||
| 51 | 1. Create a new file at ollamatools/plugins/matrix_core.py | |
| 52 | 2. Implement a MatrixCorePlugin class that follows the MatrixPlugin interface | |
| 53 | 3. Extract the core Matrix functionality from enhanced_matrix_core.py into this plugin | |
| 54 | 4. Create a PluginEnabledMatrixClient class that uses the plugin registry | |
| 55 | 5. Update the Matrix client initialization to register and initialize plugins | |
| 56 | 6. Modify the message handling to use plugins for processing messages | |
| 57 | 7. Add proper error handling and logging | |
| 58 | 8. Update the Matrix command handling to use plugins for command registration and execution | |
| 59 | 9. Add unit tests for the new plugin-enabled client | |
| 60 | 10. Update examples to use the new plugin-enabled client | |
| 61 | ``` | |
| 62 | ||
| 63 | ## 4. Weak Integration Between Matrix Client and Agent | |
| 64 | ||
| 65 | **Issue:** The matrix integration and agent architecture are separate components that borrow from each other rather than being integrated in a clean way. | |
| 66 | ||
| 67 | **Prompt Plan:** | |
| 68 | ``` | |
| 69 | Create a MatrixAgentAdapterPlugin to properly connect Matrix clients with agents: | |
| 70 | ||
| 71 | 1. Create a new file at ollamatools/plugins/matrix_agent_adapter.py | |
| 72 | 2. Implement a MatrixAgentAdapterPlugin class that follows the MatrixPlugin interface | |
| 73 | 3. The plugin should: | |
| 74 | - Accept an agent class and configuration in its constructor | |
| 75 | - Initialize the agent in its initialize() method | |
| 76 | - Handle Matrix messages by passing them to the agent in on_message() | |
| 77 | - Forward agent commands to Matrix in get_commands() | |
| 78 | - Clean up the agent in shutdown() | |
| 79 | 4. Add proper error handling and logging | |
| 80 | 5. Add unit tests for the adapter plugin | |
| 81 | 6. Create an example that demonstrates using the adapter to connect a Matrix client with different agent types | |
| 82 | 7. Update the documentation to explain how to use the adapter | |
| 83 | ``` | |
| 84 | ||
| 85 | ## 5. Lack of Unified Configuration | |
| 86 | ||
| 87 | **Issue:** Each component has its own configuration approach, leading to inconsistent configuration handling. | |
| 88 | ||
| 89 | **Prompt Plan:** | |
| 90 | ``` | |
| 91 | Create a unified configuration system for the OllamaTools project: | |
| 92 | ||
| 93 | 1. Create a new file at ollamatools/config/plugin_config.py | |
| 94 | 2. Implement a PluginConfig class that can: | |
| 95 | - Load configuration from YAML, JSON, or Python dictionaries | |
| 96 | - Validate configuration against schemas | |
| 97 | - Provide default values for missing configuration | |
| 98 | - Merge configurations from different sources | |
| 99 | 3. Update the plugin registry to use this configuration system | |
| 100 | 4. Add configuration schemas for each plugin type | |
| 101 | 5. Update the plugin initialization to use the new configuration system | |
| 102 | 6. Add utility functions for creating default configurations | |
| 103 | 7. Add unit tests for the configuration system | |
| 104 | 8. Update examples to use the new configuration system | |
| 105 | 9. Add documentation for the configuration system | |
| 106 | ``` | |
| 107 | ||
| 108 | ## 6. Inconsistent Error Handling | |
| 109 | ||
| 110 | **Issue:** Error handling varies across the codebase, with different approaches to exception handling. | |
| 111 | ||
| 112 | **Prompt Plan:** | |
| 113 | ``` | |
| 114 | Create a unified error handling system for plugins: | |
| 115 | ||
| 116 | 1. Create a new file at ollamatools/plugins/errors.py | |
| 117 | 2. Define a hierarchy of exception classes for plugin-related errors: | |
| 118 | - PluginError as the base class | |
| 119 | - PluginInitializationError for initialization failures | |
| 120 | - PluginDependencyError for dependency issues | |
| 121 | - PluginExecutionError for runtime errors | |
| 122 | 3. Implement utility functions for handling plugin errors: | |
| 123 | - format_plugin_error() to create user-friendly error messages | |
| 124 | - log_plugin_error() to log errors with appropriate context | |
| 125 | - handle_plugin_exception() to catch and process exceptions | |
| 126 | 4. Update the plugin registry to use the new error handling system | |
| 127 | 5. Update plugins to use the new exception classes | |
| 128 | 6. Add unit tests for the error handling system | |
| 129 | 7. Add documentation for the error handling system | |
| 130 | ``` | |
| 131 | ||
| 132 | ## 7. Limited Extensibility for Matrix Commands | |
| 133 | ||
| 134 | **Issue:** The matrix client's command handling is hardcoded, making it difficult to extend with new commands. | |
| 135 | ||
| 136 | **Prompt Plan:** | |
| 137 | ``` | |
| 138 | Create a command registry system for Matrix plugins: | |
| 139 | ||
| 140 | 1. Create a new file at ollamatools/plugins/matrix_commands.py | |
| 141 | 2. Implement a MatrixCommandRegistry class that can: | |
| 142 | - Register command handlers from plugins | |
| 143 | - Parse and route commands to the appropriate handler | |
| 144 | - Provide help text for available commands | |
| 145 | - Handle command errors | |
| 146 | 3. Update the MatrixPlugin interface to include a get_commands() method | |
| 147 | 4. Modify the PluginEnabledMatrixClient to use the command registry | |
| 148 | 5. Create example command plugins: | |
| 149 | - HelpCommandPlugin for providing help | |
| 150 | - SearchCommandPlugin for search commands | |
| 151 | - HistoryCommandPlugin for history commands | |
| 152 | 6. Add unit tests for the command registry | |
| 153 | 7. Update examples to demonstrate adding custom commands | |
| 154 | 8. Add documentation for the command system | |
| 155 | ``` | |
| 156 | ||
| 157 | ## 8. No Standardized Plugin Discovery | |
| 158 | ||
| 159 | **Issue:** There's no standardized way to discover and load plugins across the project. | |
| 160 | ||
| 161 | **Prompt Plan:** | |
| 162 | ``` | |
| 163 | Implement a plugin discovery system using entry points: | |
| 164 | ||
| 165 | 1. Update the PluginRegistry class to discover plugins using entry points | |
| 166 | 2. Add a setup.py entry_points configuration for built-in plugins | |
| 167 | 3. Create a plugin discovery function that can: | |
| 168 | - Find plugins in specified directories | |
| 169 | - Load plugins from entry points | |
| 170 | - Load plugins from configuration | |
| 171 | 4. Add a plugin validation function to ensure plugins implement the required interfaces | |
| 172 | 5. Update the plugin initialization to use the discovery system | |
| 173 | 6. Add unit tests for the discovery system | |
| 174 | 7. Create an example third-party plugin package | |
| 175 | 8. Add documentation for creating and distributing plugins | |
| 176 | ``` | |
| 177 | ||
| 178 | ## 9. Inefficient Conversation Context Management | |
| 179 | ||
| 180 | **Issue:** The current context management in the matrix client is simplistic and could lead to memory issues. | |
| 181 | ||
| 182 | **Prompt Plan:** | |
| 183 | ``` | |
| 184 | Create a ConversationContextPlugin for efficient context management: | |
| 185 | ||
| 186 | 1. Create a new file at ollamatools/plugins/conversation_context.py | |
| 187 | 2. Implement a ConversationContextPlugin class that: | |
| 188 | - Maintains a sliding window of conversation history | |
| 189 | - Implements intelligent context pruning based on relevance | |
| 190 | - Supports different context strategies (time-based, token-based, etc.) | |
| 191 | - Provides methods for adding to and retrieving context | |
| 192 | 3. Add configuration options for context size, pruning strategy, etc. | |
| 193 | 4. Update the Matrix client and agent to use this plugin for context management | |
| 194 | 5. Add unit tests for the context management plugin | |
| 195 | 6. Add documentation for the context management system | |
| 196 | ``` | |
| 197 | ||
| 198 | ## 10. Limited Integration with Extended Agent Features | |
| 199 | ||
| 200 | **Issue:** The matrix client doesn't leverage the more sophisticated history features available in the ExtendedModularAgent. | |
| 201 | ||
| 202 | **Prompt Plan:** | |
| 203 | ``` | |
| 204 | Create adapter plugins to expose extended agent features to Matrix: | |
| 205 | ||
| 206 | 1. Create a new file at ollamatools/plugins/extended_agent_features.py | |
| 207 | 2. Implement adapter plugins for each extended feature: | |
| 208 | - TopicAnalysisPlugin for topic analysis | |
| 209 | - PerformanceMetricsPlugin for performance tracking | |
| 210 | - ConversationSummaryPlugin for conversation summarization | |
| 211 | 3. Each plugin should: | |
| 212 | - Follow the MatrixPlugin interface | |
| 213 | - Add Matrix commands for accessing the feature | |
| 214 | - Connect to the appropriate agent functionality | |
| 215 | 4. Update the MatrixAgentAdapterPlugin to register these plugins when using an extended agent | |
| 216 | 5. Add unit tests for the adapter plugins | |
| 217 | 6. Create examples demonstrating the extended features in Matrix | |
| 218 | 7. Add documentation for the extended features | |
| 219 | ``` | |
| 220 | ||
| 221 | ## Implementation Strategy | |
| 222 | ||
| 223 | To implement these changes effectively, I recommend a phased approach: | |
| 224 | ||
| 225 | 1. **Phase 1: Core Infrastructure** | |
| 226 | - Implement the base plugin interfaces and registry | |
| 227 | - Create the unified configuration system | |
| 228 | - Implement the error handling system | |
| 229 | ||
| 230 | 2. **Phase 2: Shared Plugins** | |
| 231 | - Implement the ConversationHistoryPlugin | |
| 232 | - Implement the ConversationContextPlugin | |
| 233 | - Create tests for these plugins | |
| 234 | ||
| 235 | 3. **Phase 3: Matrix Integration** | |
| 236 | - Refactor the Matrix client to use plugins | |
| 237 | - Implement the command registry | |
| 238 | - Create the MatrixCorePlugin | |
| 239 | ||
| 240 | 4. **Phase 4: Agent Integration** | |
| 241 | - Move the ExtendedModularAgent to a proper module | |
| 242 | - Update agents to use shared plugins | |
| 243 | - Create the MatrixAgentAdapterPlugin | |
| 244 | ||
| 245 | 5. **Phase 5: Extended Features** | |
| 246 | - Implement the extended feature adapter plugins | |
| 247 | - Create the plugin discovery system | |
| 248 | - Update examples and documentation | |
| 249 | ||
| 250 | This phased approach ensures that each component builds on the previous ones, creating a solid foundation for the unified plugin architecture. |
| … | … | @@ -1,120 +1,399 @@ |
| 1 | # Issues for the Transparent Ollama Agent | |
| 2 | ||
| 3 | ## Original Issues Status | |
| 4 | ||
| 5 | 1. **Natural Language Understanding for Search** | |
| 6 | - **Problem**: The agent requires explicit search commands rather than understanding natural language requests. | |
| 7 | - **Description**: Currently, users must type "search [query]" instead of casual questions like "can you give me a recipe for chilli sauce?" | |
| 8 | - **Status**: RESOLVED ✅ | |
| 9 | - **Resolution**: | |
| 10 | - Implemented search intent detection and automatic search trigger in the code | |
| 11 | - Added the necessary logic to enhance natural language understanding | |
| 12 | - Fixed the implementation of the `chat` method to properly detect function calls in the LLM response | |
| 13 | - Added JSON parsing and explicit function execution when a search function call is detected | |
| 14 | - **Testing**: Verified with test scripts that the agent properly handles natural language search requests and executes function calls correctly. | |
| 15 | ||
| 16 | 2. **Session Permission Not Working as Expected** | |
| 17 | - **Problem**: The "Allow for Session" option doesn't prevent further permission prompts. | |
| 18 | - **Description**: When typing 'S' for session-level permissions, the user still gets prompted for similar operations. | |
| 19 | - **Status**: RESOLVED ✅ | |
| 20 | - **Resolution**: Enhanced the permission system in `ollamatools/security.py` to use more flexible permission keys that recognize similar operations. Implemented command-based and directory-based permission patterns for better session handling. | |
| 21 | - **Testing Needed**: Verify the improvements by testing with various similar commands and file operations | |
| 22 | ||
| 23 | 3. **Command List Management** | |
| 24 | - **Problem**: No easy way to view, add or remove allowed commands. | |
| 25 | - **Description**: Users need the ability to manage the list of allowed commands through the interface. | |
| 26 | - **Status**: RESOLVED ✅ | |
| 27 | - **Resolution**: Added command management features to `examples/transparent_ollama_agent.py` including new commands: `commands` (to list all allowed commands), `add_command` (to add a command to the allowlist), and `remove_command` (to remove a command from the allowlist). | |
| 28 | ||
| 29 | 4. **Lack of Structured Issue Tracking** | |
| 30 | - **Problem**: No built-in way to log and manage issues discovered during operation. | |
| 31 | - **Description**: Currently, there's no mechanism to save and track issues discovered during operation. | |
| 32 | - **Status**: RESOLVED ✅ | |
| 33 | - **Resolution**: Implemented a full issue tracking system in `examples/transparent_ollama_agent.py` with persistence to disk. Added commands for managing issues: `issues` (to list all issues), `issue <id>` (to show issue details), `add_issue` (to add a new issue), and `resolve_issue` (to mark an issue as resolved). | |
| 34 | ||
| 35 | ## New Issues Identified | |
| 36 | ||
| 37 | 5. **Code Organization and Modularity** | |
| 38 | - **Problem**: The transparent agent code is in a single large file with multiple responsibilities. | |
| 39 | - **Description**: The `transparent_ollama_agent.py` has grown to include many features (search, commands, issues, etc.) making it difficult to maintain and extend. | |
| 40 | - **Status**: IDENTIFIED 🆕 | |
| 41 | - **Proposed Solution**: Refactor the code into a modular structure: | |
| 42 | ``` | |
| 43 | ollamatools/ | |
| 44 | ├── agent/ | |
| 45 | │ └── transparent/ | |
| 46 | │ ├── __init__.py # Package initialization | |
| 47 | │ ├── agent.py # TransparentAgent class | |
| 48 | │ ├── search.py # Search functionality | |
| 49 | │ ├── commands.py # Command management | |
| 50 | │ ├── issues.py # Issue tracking | |
| 51 | │ └── utils.py # Utility functions | |
| 52 | └── examples/ | |
| 53 | └── transparent_agent.py # Main CLI entry point | |
| 54 | ``` | |
| 55 | - This structure aligns the file organization with the namespace, placing all transparent agent code in a dedicated directory under `ollamatools/agent/transparent/`. | |
| 56 | ||
| 57 | 6. **OpenSearchNG Integration Error Handling** | |
| 58 | - **Problem**: The agent fails with a type error when OpenSearchNG is not available. | |
| 59 | - **Description**: When OpenSearchNG is not running, the error "sequence item 0: expected str instance, dict found" occurs, indicating improper error handling. | |
| 60 | - **Status**: RESOLVED ✅ | |
| 61 | - **Resolution**: | |
| 62 | - Enhanced error handling in the OpenSearchNG integration code to properly handle dictionary values in search results | |
| 63 | - Added type checking and proper conversion for answers, suggestions, and infoboxes | |
| 64 | - Improved fallback mechanism when OpenSearchNG is not available | |
| 65 | - Added better error reporting and logging | |
| 66 | - **Testing**: Verified with test scripts that the error is properly handled and the agent gracefully falls back to alternative search methods when OpenSearchNG is not available. | |
| 67 | ||
| 68 | 7. **Inconsistent Function Execution** | |
| 69 | - **Problem**: The agent sometimes returns function calls as raw JSON instead of executing them. | |
| 70 | - **Description**: When the LLM returns a function call in JSON format, the agent is not properly parsing and executing it. | |
| 71 | - **Status**: RESOLVED ✅ | |
| 72 | - **Resolution**: | |
| 73 | - Added code to parse JSON function calls and execute them appropriately | |
| 74 | - Implemented proper error handling for malformed JSON | |
| 75 | - Enhanced the function execution logic to handle different JSON formats | |
| 76 | - **Testing**: Verified with test scripts that the agent correctly detects, parses, and executes function calls, and handles malformed JSON gracefully. | |
| 77 | ||
| 78 | ## Implementation Notes | |
| 79 | ||
| 80 | ### Natural Language Understanding for Search | |
| 81 | - Fixed the `chat` method in `TransparentAgent` to handle function calls properly: | |
| 82 | - Added JSON parsing for responses that appear to be function calls | |
| 83 | - Implemented direct execution of search functions when detected | |
| 84 | - Enhanced result formatting to create user-friendly responses | |
| 85 | ||
| 86 | ### Code Organization Plan | |
| 87 | - Break down the large `transparent_ollama_agent.py` file into several specialized modules: | |
| 88 | - `agent.py`: Core agent class with initialization and chat handling | |
| 89 | - `search.py`: Search intent detection and execution | |
| 90 | - `commands.py`: Command management functionality | |
| 91 | - `issues.py`: Issue tracking system | |
| 92 | - `utils.py`: Display helpers and common utilities | |
| 93 | - Create a dedicated namespace under `ollamatools.agent.transparent` | |
| 94 | - Update imports and maintain backward compatibility | |
| 95 | - Create proper interfaces between components | |
| 96 | ||
| 97 | ### OpenSearchNG Error Handling | |
| 98 | - Add better exception handling in the OpenSearchNG integration code | |
| 99 | - Implement a fallback mechanism for when OpenSearchNG is not available | |
| 100 | - Add clear error messages and suggestions for fixing connection issues | |
| 101 | ||
| 102 | ## Future Considerations | |
| 103 | ||
| 104 | 1. **Advanced Search Intent Detection**: The current implementation uses simple heuristics for intent detection. Future versions could use more sophisticated language understanding techniques. | |
| 105 | ||
| 106 | 2. **Fine-grained Permission System**: Enhance the permission system to support more granular permissions and remember user preferences across sessions. | |
| 107 | ||
| 108 | 3. **Command Group Permissions**: Allow users to grant permissions to groups of related commands rather than individual commands. | |
| 109 | ||
| 110 | 4. **Issue Categories and Labels**: Add support for categorizing issues and applying labels for better organization. | |
| 111 | ||
| 112 | 5. **Export/Import Issues**: Add functionality to export issues to standard formats like CSV or JSON for integration with external issue tracking systems. | |
| 113 | ||
| 114 | 6. **Advanced Code Analysis**: Implement code analysis features to help identify potential issues in code being developed with the agent. | |
| 115 | ||
| 116 | 7. **Plugin System**: Create a plugin architecture to allow for easy extension of agent capabilities. | |
| 117 | ||
| 118 | 8. **Configuration Management**: Add proper configuration file support for customizing agent behavior. | |
| 119 | ||
| 120 | 9. **Testing Framework**: Develop a comprehensive testing framework for validating agent functionality. | |
| 1 | # OllamaTools Project Issues | |
| 2 | ||
| 3 | This document outlines key issues identified in the OllamaTools project, focusing on integration between matrix, modular, and extended agent libraries. | |
| 4 | ||
| 5 | ## 1. Inconsistent Plugin Architecture Across Components | |
| 6 | ||
| 7 | **Description:** | |
| 8 | The modular agent has a well-defined plugin architecture, but this architecture isn't consistently applied across the matrix integration. This leads to ad-hoc integration points and conditional imports rather than a clean, extensible design. | |
| 9 | ||
| 10 | **Impact:** | |
| 11 | - Difficult to extend the matrix integration with new capabilities | |
| 12 | - Fragile dependencies between components | |
| 13 | - Inconsistent code organization | |
| 14 | ||
| 15 | **Suggested Solution:** | |
| 16 | Implement a consistent plugin architecture across all components: | |
| 17 | - Define a clear plugin interface for matrix integrations | |
| 18 | - Allow matrix clients to load plugins from the modular and extended agent libraries | |
| 19 | - Create a registry of available plugins that can be dynamically loaded | |
| 20 | ||
| 21 | **Example Implementation:** | |
| 22 | ```python | |
| 23 | # Define a plugin interface | |
| 24 | class MatrixPlugin: | |
| 25 | def initialize(self, client): | |
| 26 | """Initialize the plugin with the client instance.""" | |
| 27 | pass | |
| 28 | ||
| 29 | def on_message(self, room, event, client): | |
| 30 | """Handle incoming messages.""" | |
| 31 | return False # Return True if message was handled | |
| 32 | ||
| 33 | def get_commands(self): | |
| 34 | """Return commands provided by this plugin.""" | |
| 35 | return {} | |
| 36 | ||
| 37 | # Example search plugin | |
| 38 | class SearchPlugin(MatrixPlugin): | |
| 39 | def initialize(self, client): | |
| 40 | self.opensearch_config = client.opensearch_config | |
| 41 | ||
| 42 | def on_message(self, room, event, client): | |
| 43 | # Handle search-related functionality | |
| 44 | pass | |
| 45 | ||
| 46 | def get_commands(self): | |
| 47 | return { | |
| 48 | "/search": self.handle_search_command | |
| 49 | } | |
| 50 | ``` | |
| 51 | ||
| 52 | ## 2. Duplicated Functionality Between Enhanced Matrix and Extended Agent | |
| 53 | ||
| 54 | **Description:** | |
| 55 | There's significant duplication of functionality between `enhanced_matrix_history.py` and the `ExtendedModularAgent` class, particularly in conversation history tracking, topic analysis, and performance metrics. | |
| 56 | ||
| 57 | **Impact:** | |
| 58 | - Code duplication leads to maintenance challenges | |
| 59 | - Inconsistent implementations of the same features | |
| 60 | - Difficult to ensure feature parity across components | |
| 61 | ||
| 62 | **Suggested Solution:** | |
| 63 | Extract common functionality into shared utility modules that can be used by both the matrix integration and the agent libraries: | |
| 64 | - Create a `conversation_tracking` module with shared functionality | |
| 65 | - Implement a common interface for history storage and retrieval | |
| 66 | - Ensure consistent metrics calculation across all components | |
| 67 | ||
| 68 | **Example Implementation:** | |
| 69 | ```python | |
| 70 | # In ollamatools/utils/conversation_tracking.py | |
| 71 | class ConversationTracker: | |
| 72 | def __init__(self, history_file=None): | |
| 73 | # Initialize conversation tracking | |
| 74 | pass | |
| 75 | ||
| 76 | def record_conversation(self, user_message, assistant_response, metadata=None): | |
| 77 | # Record a conversation | |
| 78 | pass | |
| 79 | ||
| 80 | def analyze_topics(self): | |
| 81 | # Analyze conversation topics | |
| 82 | pass | |
| 83 | ||
| 84 | def get_metrics(self): | |
| 85 | # Get performance metrics | |
| 86 | pass | |
| 87 | ``` | |
| 88 | ||
| 89 | ## 3. Improper Module Organization for Extended Agent | |
| 90 | ||
| 91 | **Description:** | |
| 92 | The `ExtendedModularAgent` is defined in an example file (`examples/extended_modular_agent.py`) rather than as a proper module, and `ollamatools/agent/extended/__init__.py` simply imports from this example. This violates proper package organization principles. | |
| 93 | ||
| 94 | **Impact:** | |
| 95 | - Confusing import paths | |
| 96 | - Difficult to maintain and extend | |
| 97 | - Potential for circular imports | |
| 98 | - Inconsistent with the rest of the project structure | |
| 99 | ||
| 100 | **Suggested Solution:** | |
| 101 | Reorganize the extended agent as a proper module: | |
| 102 | - Move the `ExtendedModularAgent` class to `ollamatools/agent/extended/agent.py` | |
| 103 | - Update `__init__.py` to import from this module | |
| 104 | - Ensure proper separation of example code and library code | |
| 105 | ||
| 106 | **Example Implementation:** | |
| 107 | ```python | |
| 108 | # In ollamatools/agent/extended/agent.py | |
| 109 | from ollamatools.agent import ModularOllamaAgent | |
| 110 | ||
| 111 | class ExtendedModularAgent(ModularOllamaAgent): | |
| 112 | # Implementation here | |
| 113 | pass | |
| 114 | ||
| 115 | # In ollamatools/agent/extended/__init__.py | |
| 116 | from .agent import ExtendedModularAgent | |
| 117 | ||
| 118 | __all__ = ['ExtendedModularAgent'] | |
| 119 | ``` | |
| 120 | ||
| 121 | ## 4. Lack of Unified Configuration System | |
| 122 | ||
| 123 | **Description:** | |
| 124 | Each component (matrix, modular agent, extended agent) has its own configuration approach, leading to inconsistent configuration handling across the project. | |
| 125 | ||
| 126 | **Impact:** | |
| 127 | - Difficult to configure multiple components consistently | |
| 128 | - Redundant configuration code | |
| 129 | - Potential for configuration conflicts | |
| 130 | ||
| 131 | **Suggested Solution:** | |
| 132 | Implement a unified configuration system: | |
| 133 | - Create a central configuration registry | |
| 134 | - Allow components to register their configuration schemas | |
| 135 | - Provide validation and merging of configurations | |
| 136 | - Support hierarchical configuration with inheritance | |
| 137 | ||
| 138 | **Example Implementation:** | |
| 139 | ```python | |
| 140 | # In ollamatools/config.py | |
| 141 | class ConfigRegistry: | |
| 142 | def __init__(self): | |
| 143 | self.schemas = {} | |
| 144 | self.defaults = {} | |
| 145 | ||
| 146 | def register_schema(self, component, schema, defaults=None): | |
| 147 | # Register configuration schema for a component | |
| 148 | pass | |
| 149 | ||
| 150 | def validate_config(self, component, config): | |
| 151 | # Validate configuration against schema | |
| 152 | pass | |
| 153 | ||
| 154 | def get_config(self, component, user_config=None): | |
| 155 | # Get configuration with defaults applied | |
| 156 | pass | |
| 157 | ``` | |
| 158 | ||
| 159 | ## 5. Weak Integration Between Matrix Client and Agent Architecture | |
| 160 | ||
| 161 | **Description:** | |
| 162 | The matrix integration and agent architecture are separate components that borrow from each other rather than being integrated in a clean way. The matrix client uses features from the modular agent conditionally, but doesn't fully leverage the agent's capabilities. | |
| 163 | ||
| 164 | **Impact:** | |
| 165 | - Limited ability to use agent features in matrix clients | |
| 166 | - Inconsistent user experience between direct agent usage and matrix-based interaction | |
| 167 | - Difficult to extend with new agent capabilities | |
| 168 | ||
| 169 | **Suggested Solution:** | |
| 170 | Create a proper integration layer between matrix clients and agents: | |
| 171 | - Implement an adapter pattern to connect matrix clients to agents | |
| 172 | - Allow matrix clients to delegate message processing to agents | |
| 173 | - Ensure consistent behavior between direct agent usage and matrix-based interaction | |
| 174 | ||
| 175 | **Example Implementation:** | |
| 176 | ```python | |
| 177 | # In ollamatools/tools/matrix_agent_adapter.py | |
| 178 | class MatrixAgentAdapter: | |
| 179 | def __init__(self, agent_class, agent_config=None): | |
| 180 | # Initialize with agent class and configuration | |
| 181 | self.agent = agent_class(**(agent_config or {})) | |
| 182 | ||
| 183 | async def process_message(self, message, sender, room_id): | |
| 184 | # Process message using the agent | |
| 185 | return self.agent.chat(message) | |
| 186 | ||
| 187 | def get_agent_commands(self): | |
| 188 | # Get commands supported by the agent | |
| 189 | return self.agent.get_commands() | |
| 190 | ``` | |
| 191 | ||
| 192 | ## 6. Inconsistent Error Handling Across Components | |
| 193 | ||
| 194 | **Description:** | |
| 195 | Error handling is inconsistent across the codebase, with some components using try/except blocks with specific error types, others using generic exception handling, and others relying on the caller to handle errors. | |
| 196 | ||
| 197 | **Impact:** | |
| 198 | - Unpredictable error behavior | |
| 199 | - Difficult to debug issues | |
| 200 | - Potential for unhandled exceptions | |
| 201 | ||
| 202 | **Suggested Solution:** | |
| 203 | Implement a consistent error handling strategy: | |
| 204 | - Define a hierarchy of custom exception types | |
| 205 | - Document which functions can raise which exceptions | |
| 206 | - Ensure consistent error reporting across components | |
| 207 | - Implement proper error recovery mechanisms | |
| 208 | ||
| 209 | **Example Implementation:** | |
| 210 | ```python | |
| 211 | # In ollamatools/errors.py | |
| 212 | class OllamaToolsError(Exception): | |
| 213 | """Base class for all OllamaTools errors.""" | |
| 214 | pass | |
| 215 | ||
| 216 | class MatrixError(OllamaToolsError): | |
| 217 | """Base class for Matrix-related errors.""" | |
| 218 | pass | |
| 219 | ||
| 220 | class AgentError(OllamaToolsError): | |
| 221 | """Base class for Agent-related errors.""" | |
| 222 | pass | |
| 223 | ||
| 224 | class ConfigError(OllamaToolsError): | |
| 225 | """Configuration error.""" | |
| 226 | pass | |
| 227 | ``` | |
| 228 | ||
| 229 | ## 7. Limited Extensibility for Matrix Command Handling | |
| 230 | ||
| 231 | **Description:** | |
| 232 | The matrix client's command handling is hardcoded with specific command patterns and handlers, making it difficult to extend with new commands or modify existing ones. | |
| 233 | ||
| 234 | **Impact:** | |
| 235 | - Difficult to add new commands | |
| 236 | - Cannot customize command behavior | |
| 237 | - Tight coupling between command detection and execution | |
| 238 | ||
| 239 | **Suggested Solution:** | |
| 240 | Implement a flexible command registry: | |
| 241 | - Allow registration of commands with patterns and handlers | |
| 242 | - Support command categories and help text | |
| 243 | - Enable plugins to register their own commands | |
| 244 | - Implement command permissions and validation | |
| 245 | ||
| 246 | **Example Implementation:** | |
| 247 | ```python | |
| 248 | # In ollamatools/tools/matrix_commands.py | |
| 249 | class CommandRegistry: | |
| 250 | def __init__(self): | |
| 251 | self.commands = {} | |
| 252 | ||
| 253 | def register_command(self, pattern, handler, help_text=None, category=None): | |
| 254 | # Register a command | |
| 255 | self.commands[pattern] = { | |
| 256 | 'handler': handler, | |
| 257 | 'help_text': help_text, | |
| 258 | 'category': category | |
| 259 | } | |
| 260 | ||
| 261 | async def handle_command(self, message, room_id, sender, client): | |
| 262 | # Find and execute matching command | |
| 263 | for pattern, command in self.commands.items(): | |
| 264 | match = re.match(pattern, message) | |
| 265 | if match: | |
| 266 | return await command['handler'](match, room_id, sender, client) | |
| 267 | return False | |
| 268 | ``` | |
| 269 | ||
| 270 | ## 8. Lack of Standardized Plugin Discovery Mechanism | |
| 271 | ||
| 272 | **Description:** | |
| 273 | There's no standardized way to discover and load plugins across the project, leading to manual imports and conditional feature flags. | |
| 274 | ||
| 275 | **Impact:** | |
| 276 | - Difficult to extend the system with new plugins | |
| 277 | - Manual management of dependencies | |
| 278 | - Inconsistent plugin loading across components | |
| 279 | ||
| 280 | **Suggested Solution:** | |
| 281 | Implement a plugin discovery mechanism: | |
| 282 | - Use entry points or a similar mechanism for plugin registration | |
| 283 | - Provide a plugin registry that can discover and load plugins | |
| 284 | - Support plugin dependencies and conflicts | |
| 285 | - Enable dynamic loading and unloading of plugins | |
| 286 | ||
| 287 | **Example Implementation:** | |
| 288 | ```python | |
| 289 | # In ollamatools/plugin.py | |
| 290 | class PluginRegistry: | |
| 291 | def __init__(self): | |
| 292 | self.plugins = {} | |
| 293 | ||
| 294 | def discover_plugins(self): | |
| 295 | # Discover plugins using entry points | |
| 296 | import pkg_resources | |
| 297 | for entry_point in pkg_resources.iter_entry_points('ollamatools.plugins'): | |
| 298 | plugin_class = entry_point.load() | |
| 299 | self.register_plugin(entry_point.name, plugin_class) | |
| 300 | ||
| 301 | def register_plugin(self, name, plugin_class): | |
| 302 | # Register a plugin | |
| 303 | self.plugins[name] = plugin_class | |
| 304 | ||
| 305 | def get_plugin(self, name): | |
| 306 | # Get a plugin by name | |
| 307 | return self.plugins.get(name) | |
| 308 | ``` | |
| 309 | ||
| 310 | ## 9. Inefficient Conversation Context Management | |
| 311 | ||
| 312 | **Description:** | |
| 313 | The current conversation context management in the matrix client is simplistic and could lead to memory issues with long-running bots or busy rooms. | |
| 314 | ||
| 315 | **Impact:** | |
| 316 | - Potential memory leaks | |
| 317 | - Degraded performance over time | |
| 318 | - Limited context window management | |
| 319 | ||
| 320 | **Suggested Solution:** | |
| 321 | Implement a more sophisticated context management system: | |
| 322 | - Use a sliding window approach for context | |
| 323 | - Implement context summarization for long conversations | |
| 324 | - Add context pruning based on relevance | |
| 325 | - Support context persistence across restarts | |
| 326 | ||
| 327 | **Example Implementation:** | |
| 328 | ```python | |
| 329 | # In ollamatools/utils/context_manager.py | |
| 330 | class ConversationContext: | |
| 331 | def __init__(self, max_messages=10, max_tokens=1000): | |
| 332 | self.messages = [] | |
| 333 | self.max_messages = max_messages | |
| 334 | self.max_tokens = max_tokens | |
| 335 | ||
| 336 | def add_message(self, role, content): | |
| 337 | # Add a message to the context | |
| 338 | self.messages.append({"role": role, "content": content}) | |
| 339 | self._prune_context() | |
| 340 | ||
| 341 | def _prune_context(self): | |
| 342 | # Prune context if it exceeds limits | |
| 343 | while len(self.messages) > self.max_messages: | |
| 344 | self.messages.pop(0) | |
| 345 | ||
| 346 | # Token-based pruning | |
| 347 | total_tokens = sum(len(m["content"].split()) for m in self.messages) | |
| 348 | while total_tokens > self.max_tokens and len(self.messages) > 1: | |
| 349 | removed = self.messages.pop(0) | |
| 350 | total_tokens -= len(removed["content"].split()) | |
| 351 | ||
| 352 | def get_context(self): | |
| 353 | # Get the current context | |
| 354 | return self.messages | |
| 355 | ``` | |
| 356 | ||
| 357 | ## 10. Lack of Integration with Extended Agent's History Features | |
| 358 | ||
| 359 | **Description:** | |
| 360 | The enhanced matrix client implements its own conversation history tracking, but doesn't leverage the more sophisticated history features available in the ExtendedModularAgent. | |
| 361 | ||
| 362 | **Impact:** | |
| 363 | - Duplicated functionality | |
| 364 | - Inconsistent history tracking between matrix and agent | |
| 365 | - Limited history analysis capabilities in matrix client | |
| 366 | ||
| 367 | **Suggested Solution:** | |
| 368 | Integrate the matrix client with the ExtendedModularAgent's history features: | |
| 369 | - Create a shared history storage mechanism | |
| 370 | - Allow the matrix client to use the agent's history analysis | |
| 371 | - Ensure consistent history tracking across all components | |
| 372 | - Implement a unified API for history access | |
| 373 | ||
| 374 | **Example Implementation:** | |
| 375 | ```python | |
| 376 | # In ollamatools/tools/enhanced_matrix_core.py | |
| 377 | from ollamatools.agent.extended import ExtendedModularAgent | |
| 378 | ||
| 379 | class EnhancedMatrixClient(MatrixClient): | |
| 380 | def __init__(self, *args, **kwargs): | |
| 381 | super().__init__(*args, **kwargs) | |
| 382 | ||
| 383 | # Create an agent instance for history tracking | |
| 384 | self.agent = ExtendedModularAgent( | |
| 385 | model=self.ollama_model, | |
| 386 | # Other configuration | |
| 387 | ) | |
| 388 | ||
| 389 | async def _message_callback(self, room, event): | |
| 390 | # Process message | |
| 391 | # ... | |
| 392 | ||
| 393 | # Use agent for history tracking | |
| 394 | self.agent.record_conversation(message, response) | |
| 395 | ||
| 396 | async def handle_history_command(self, count, room_id): | |
| 397 | # Use agent's history features | |
| 398 | summary = self.agent.summarize_conversations(count) | |
| 399 | await self.send_message(room_id, summary) |
| … | … | @@ -0,0 +1,1105 @@ |
| 1 | # Phase 1: Core Infrastructure Implementation Plan | |
| 2 | ||
| 3 | This document outlines the detailed implementation plan for Phase 1 of the unified plugin architecture for OllamaTools. Phase 1 focuses on creating the core infrastructure needed for the plugin system. | |
| 4 | ||
| 5 | ## 1. Base Plugin Interface and Registry | |
| 6 | ||
| 7 | ### 1.1 Create Plugin Interface Module | |
| 8 | ||
| 9 | **File:** `ollamatools/plugins/__init__.py` | |
| 10 | ||
| 11 | ```python | |
| 12 | """ | |
| 13 | OllamaTools Plugin System. | |
| 14 | ||
| 15 | This module provides the base interfaces and registry for the OllamaTools plugin system. | |
| 16 | """ | |
| 17 | ||
| 18 | import os | |
| 19 | import sys | |
| 20 | import importlib | |
| 21 | import logging | |
| 22 | from typing import Dict, Any, List, Optional, Set, Type, Callable, Union | |
| 23 | ||
| 24 | # Configure logging | |
| 25 | logger = logging.getLogger(__name__) | |
| 26 | ||
| 27 | class OllamaToolsPlugin: | |
| 28 | """Base interface for all OllamaTools plugins.""" | |
| 29 | ||
| 30 | def __init__(self, config=None): | |
| 31 | """ | |
| 32 | Initialize the plugin with optional configuration. | |
| 33 | ||
| 34 | Args: | |
| 35 | config: Optional configuration dictionary for the plugin | |
| 36 | """ | |
| 37 | self.config = config or {} | |
| 38 | ||
| 39 | def initialize(self, context): | |
| 40 | """ | |
| 41 | Initialize the plugin with the runtime context. | |
| 42 | ||
| 43 | Args: | |
| 44 | context: Runtime context dictionary containing references to other components | |
| 45 | """ | |
| 46 | pass | |
| 47 | ||
| 48 | def shutdown(self): | |
| 49 | """Clean up resources when shutting down.""" | |
| 50 | pass | |
| 51 | ||
| 52 | @property | |
| 53 | def name(self): | |
| 54 | """ | |
| 55 | Get the plugin name. | |
| 56 | ||
| 57 | Returns: | |
| 58 | str: The plugin name | |
| 59 | """ | |
| 60 | return self.__class__.__name__ | |
| 61 | ||
| 62 | @property | |
| 63 | def version(self): | |
| 64 | """ | |
| 65 | Get the plugin version. | |
| 66 | ||
| 67 | Returns: | |
| 68 | str: The plugin version | |
| 69 | """ | |
| 70 | return "1.0.0" | |
| 71 | ||
| 72 | @property | |
| 73 | def dependencies(self): | |
| 74 | """ | |
| 75 | Get plugin dependencies. | |
| 76 | ||
| 77 | Returns: | |
| 78 | List[str]: List of plugin names that this plugin depends on | |
| 79 | """ | |
| 80 | return [] | |
| 81 | ||
| 82 | ||
| 83 | class MatrixPlugin(OllamaToolsPlugin): | |
| 84 | """Base class for Matrix plugins.""" | |
| 85 | ||
| 86 | async def on_message(self, room, event, client): | |
| 87 | """ | |
| 88 | Handle an incoming Matrix message. | |
| 89 | ||
| 90 | Args: | |
| 91 | room: The room the message was sent in | |
| 92 | event: The message event | |
| 93 | client: The Matrix client | |
| 94 | ||
| 95 | Returns: | |
| 96 | bool: True if the message was handled, False otherwise | |
| 97 | """ | |
| 98 | return False | |
| 99 | ||
| 100 | def get_commands(self): | |
| 101 | """ | |
| 102 | Get Matrix commands provided by this plugin. | |
| 103 | ||
| 104 | Returns: | |
| 105 | Dict[str, Callable]: Dictionary of command names to handler functions | |
| 106 | """ | |
| 107 | return {} | |
| 108 | ||
| 109 | async def on_room_join(self, room, client): | |
| 110 | """ | |
| 111 | Handle room join event. | |
| 112 | ||
| 113 | Args: | |
| 114 | room: The room that was joined | |
| 115 | client: The Matrix client | |
| 116 | """ | |
| 117 | pass | |
| 118 | ||
| 119 | async def on_client_start(self, client): | |
| 120 | """ | |
| 121 | Handle client start event. | |
| 122 | ||
| 123 | Args: | |
| 124 | client: The Matrix client | |
| 125 | """ | |
| 126 | pass | |
| 127 | ||
| 128 | async def on_client_stop(self, client): | |
| 129 | """ | |
| 130 | Handle client stop event. | |
| 131 | ||
| 132 | Args: | |
| 133 | client: The Matrix client | |
| 134 | """ | |
| 135 | pass | |
| 136 | ||
| 137 | ||
| 138 | class AgentPlugin(OllamaToolsPlugin): | |
| 139 | """Base class for agent plugins.""" | |
| 140 | ||
| 141 | def process_message(self, message, context): | |
| 142 | """ | |
| 143 | Process a message and return a response. | |
| 144 | ||
| 145 | Args: | |
| 146 | message: The user message | |
| 147 | context: Processing context | |
| 148 | ||
| 149 | Returns: | |
| 150 | Optional[str]: Response message if handled, None otherwise | |
| 151 | """ | |
| 152 | return None | |
| 153 | ||
| 154 | def get_commands(self): | |
| 155 | """ | |
| 156 | Get commands provided by this plugin. | |
| 157 | ||
| 158 | Returns: | |
| 159 | Dict[str, Callable]: Dictionary of command names to handler functions | |
| 160 | """ | |
| 161 | return {} | |
| 162 | ||
| 163 | def on_function_call(self, function_name, args, context): | |
| 164 | """ | |
| 165 | Handle a function call. | |
| 166 | ||
| 167 | Args: | |
| 168 | function_name: The name of the function being called | |
| 169 | args: The arguments to the function | |
| 170 | context: Processing context | |
| 171 | ||
| 172 | Returns: | |
| 173 | Any: The result of the function call | |
| 174 | """ | |
| 175 | return None | |
| 176 | ||
| 177 | ||
| 178 | class SharedPlugin(OllamaToolsPlugin): | |
| 179 | """Base class for plugins that can be used by multiple components.""" | |
| 180 | ||
| 181 | def get_capabilities(self): | |
| 182 | """ | |
| 183 | Get capabilities provided by this plugin. | |
| 184 | ||
| 185 | Returns: | |
| 186 | List[str]: List of capability names | |
| 187 | """ | |
| 188 | return [] | |
| 189 | ``` | |
| 190 | ||
| 191 | ### 1.2 Create Plugin Registry | |
| 192 | ||
| 193 | **File:** `ollamatools/plugins/registry.py` | |
| 194 | ||
| 195 | ```python | |
| 196 | """ | |
| 197 | Plugin Registry for OllamaTools. | |
| 198 | ||
| 199 | This module provides the PluginRegistry class for managing OllamaTools plugins. | |
| 200 | """ | |
| 201 | ||
| 202 | import os | |
| 203 | import sys | |
| 204 | import importlib | |
| 205 | import logging | |
| 206 | import pkg_resources | |
| 207 | from typing import Dict, Any, List, Optional, Set, Type, Callable, Union | |
| 208 | ||
| 209 | from ollamatools.plugins import OllamaToolsPlugin | |
| 210 | from ollamatools.plugins.errors import ( | |
| 211 | PluginError, | |
| 212 | PluginInitializationError, | |
| 213 | PluginDependencyError, | |
| 214 | PluginNotFoundError, | |
| 215 | handle_plugin_exception | |
| 216 | ) | |
| 217 | ||
| 218 | # Configure logging | |
| 219 | logger = logging.getLogger(__name__) | |
| 220 | ||
| 221 | class PluginRegistry: | |
| 222 | """Registry for OllamaTools plugins.""" | |
| 223 | ||
| 224 | def __init__(self): | |
| 225 | """Initialize the plugin registry.""" | |
| 226 | self.plugins = {} | |
| 227 | self.initialized_plugins = {} | |
| 228 | ||
| 229 | def register_plugin(self, plugin_class, config=None): | |
| 230 | """ | |
| 231 | Register a plugin class. | |
| 232 | ||
| 233 | Args: | |
| 234 | plugin_class: The plugin class to register | |
| 235 | config: Optional configuration for the plugin | |
| 236 | ||
| 237 | Returns: | |
| 238 | PluginRegistry: Self for method chaining | |
| 239 | """ | |
| 240 | plugin_name = plugin_class.__name__ | |
| 241 | self.plugins[plugin_name] = { | |
| 242 | "class": plugin_class, | |
| 243 | "config": config | |
| 244 | } | |
| 245 | logger.debug(f"Registered plugin: {plugin_name}") | |
| 246 | return self | |
| 247 | ||
| 248 | def discover_plugins(self): | |
| 249 | """ | |
| 250 | Discover plugins using entry points. | |
| 251 | ||
| 252 | Returns: | |
| 253 | PluginRegistry: Self for method chaining | |
| 254 | """ | |
| 255 | try: | |
| 256 | for entry_point in pkg_resources.iter_entry_points('ollamatools.plugins'): | |
| 257 | try: | |
| 258 | plugin_class = entry_point.load() | |
| 259 | self.register_plugin(plugin_class) | |
| 260 | logger.info(f"Discovered plugin: {plugin_class.__name__} from entry point {entry_point.name}") | |
| 261 | except Exception as e: | |
| 262 | logger.error(f"Error loading plugin from entry point {entry_point.name}: {e}") | |
| 263 | except Exception as e: | |
| 264 | logger.error(f"Error discovering plugins: {e}") | |
| 265 | ||
| 266 | return self | |
| 267 | ||
| 268 | def initialize_plugin(self, plugin_name, context): | |
| 269 | """ | |
| 270 | Initialize a plugin and its dependencies. | |
| 271 | ||
| 272 | Args: | |
| 273 | plugin_name: The name of the plugin to initialize | |
| 274 | context: Runtime context for the plugin | |
| 275 | ||
| 276 | Returns: | |
| 277 | OllamaToolsPlugin: The initialized plugin instance | |
| 278 | ||
| 279 | Raises: | |
| 280 | PluginNotFoundError: If the plugin is not found | |
| 281 | PluginInitializationError: If the plugin fails to initialize | |
| 282 | PluginDependencyError: If a dependency cannot be initialized | |
| 283 | """ | |
| 284 | # Return already initialized plugin | |
| 285 | if plugin_name in self.initialized_plugins: | |
| 286 | return self.initialized_plugins[plugin_name] | |
| 287 | ||
| 288 | # Check if plugin exists | |
| 289 | if plugin_name not in self.plugins: | |
| 290 | raise PluginNotFoundError(f"Plugin {plugin_name} not found") | |
| 291 | ||
| 292 | logger.debug(f"Initializing plugin: {plugin_name}") | |
| 293 | ||
| 294 | plugin_info = self.plugins[plugin_name] | |
| 295 | plugin_class = plugin_info["class"] | |
| 296 | plugin_config = plugin_info["config"] | |
| 297 | ||
| 298 | try: | |
| 299 | # Create plugin instance | |
| 300 | plugin_instance = plugin_class(plugin_config) | |
| 301 | ||
| 302 | # Initialize dependencies first | |
| 303 | for dependency in plugin_instance.dependencies: | |
| 304 | try: | |
| 305 | self.initialize_plugin(dependency, context) | |
| 306 | except PluginError as e: | |
| 307 | raise PluginDependencyError( | |
| 308 | f"Failed to initialize dependency {dependency} for plugin {plugin_name}", | |
| 309 | details={"plugin": plugin_name, "dependency": dependency, "original_error": str(e)} | |
| 310 | ) from e | |
| 311 | ||
| 312 | # Initialize the plugin | |
| 313 | plugin_instance.initialize(context) | |
| 314 | self.initialized_plugins[plugin_name] = plugin_instance | |
| 315 | ||
| 316 | logger.info(f"Initialized plugin: {plugin_name}") | |
| 317 | return plugin_instance | |
| 318 | ||
| 319 | except PluginError: | |
| 320 | # Re-raise plugin errors | |
| 321 | raise | |
| 322 | except Exception as e: | |
| 323 | # Wrap other exceptions | |
| 324 | raise PluginInitializationError( | |
| 325 | f"Failed to initialize plugin {plugin_name}: {str(e)}", | |
| 326 | details={"plugin": plugin_name} | |
| 327 | ) from e | |
| 328 | ||
| 329 | def initialize_all(self, context): | |
| 330 | """ | |
| 331 | Initialize all registered plugins. | |
| 332 | ||
| 333 | Args: | |
| 334 | context: Runtime context for the plugins | |
| 335 | ||
| 336 | Returns: | |
| 337 | Dict[str, OllamaToolsPlugin]: Dictionary of initialized plugins | |
| 338 | """ | |
| 339 | for plugin_name in list(self.plugins.keys()): | |
| 340 | try: | |
| 341 | self.initialize_plugin(plugin_name, context) | |
| 342 | except PluginError as e: | |
| 343 | logger.error(f"Error initializing plugin {plugin_name}: {e}") | |
| 344 | handle_plugin_exception(e) | |
| 345 | except Exception as e: | |
| 346 | logger.error(f"Unexpected error initializing plugin {plugin_name}: {e}") | |
| 347 | ||
| 348 | return self.initialized_plugins | |
| 349 | ||
| 350 | def get_plugin(self, plugin_name): | |
| 351 | """ | |
| 352 | Get an initialized plugin. | |
| 353 | ||
| 354 | Args: | |
| 355 | plugin_name: The name of the plugin to get | |
| 356 | ||
| 357 | Returns: | |
| 358 | Optional[OllamaToolsPlugin]: The plugin instance or None if not found | |
| 359 | """ | |
| 360 | return self.initialized_plugins.get(plugin_name) | |
| 361 | ||
| 362 | def get_plugins_by_type(self, plugin_type): | |
| 363 | """ | |
| 364 | Get all initialized plugins of a specific type. | |
| 365 | ||
| 366 | Args: | |
| 367 | plugin_type: The plugin type class | |
| 368 | ||
| 369 | Returns: | |
| 370 | Dict[str, OllamaToolsPlugin]: Dictionary of plugin names to instances | |
| 371 | """ | |
| 372 | return { | |
| 373 | name: plugin for name, plugin in self.initialized_plugins.items() | |
| 374 | if isinstance(plugin, plugin_type) | |
| 375 | } | |
| 376 | ||
| 377 | def shutdown(self): | |
| 378 | """ | |
| 379 | Shutdown all initialized plugins. | |
| 380 | ||
| 381 | Returns: | |
| 382 | PluginRegistry: Self for method chaining | |
| 383 | """ | |
| 384 | for plugin_name, plugin in list(self.initialized_plugins.items()): | |
| 385 | try: | |
| 386 | logger.debug(f"Shutting down plugin: {plugin_name}") | |
| 387 | plugin.shutdown() | |
| 388 | logger.info(f"Shut down plugin: {plugin_name}") | |
| 389 | except Exception as e: | |
| 390 | logger.error(f"Error shutting down plugin {plugin_name}: {e}") | |
| 391 | ||
| 392 | self.initialized_plugins = {} | |
| 393 | return self | |
| 394 | ``` | |
| 395 | ||
| 396 | ## 2. Error Handling System | |
| 397 | ||
| 398 | ### 2.1 Create Plugin Error Module | |
| 399 | ||
| 400 | **File:** `ollamatools/plugins/errors.py` | |
| 401 | ||
| 402 | ```python | |
| 403 | """ | |
| 404 | Error handling for the OllamaTools plugin system. | |
| 405 | ||
| 406 | This module provides exception classes and utility functions for handling | |
| 407 | errors in the OllamaTools plugin system. | |
| 408 | """ | |
| 409 | ||
| 410 | import sys | |
| 411 | import logging | |
| 412 | import traceback | |
| 413 | from typing import Dict, Any, Optional | |
| 414 | ||
| 415 | # Configure logging | |
| 416 | logger = logging.getLogger(__name__) | |
| 417 | ||
| 418 | class PluginError(Exception): | |
| 419 | """Base class for all plugin-related errors.""" | |
| 420 | ||
| 421 | def __init__(self, message, details=None): | |
| 422 | """ | |
| 423 | Initialize the error. | |
| 424 | ||
| 425 | Args: | |
| 426 | message: Error message | |
| 427 | details: Optional dictionary with additional error details | |
| 428 | """ | |
| 429 | super().__init__(message) | |
| 430 | self.details = details or {} | |
| 431 | ||
| 432 | ||
| 433 | class PluginNotFoundError(PluginError): | |
| 434 | """Error raised when a plugin is not found.""" | |
| 435 | pass | |
| 436 | ||
| 437 | ||
| 438 | class PluginInitializationError(PluginError): | |
| 439 | """Error raised when a plugin fails to initialize.""" | |
| 440 | pass | |
| 441 | ||
| 442 | ||
| 443 | class PluginDependencyError(PluginError): | |
| 444 | """Error raised when a plugin dependency cannot be satisfied.""" | |
| 445 | pass | |
| 446 | ||
| 447 | ||
| 448 | class PluginExecutionError(PluginError): | |
| 449 | """Error raised when a plugin operation fails during execution.""" | |
| 450 | pass | |
| 451 | ||
| 452 | ||
| 453 | class PluginConfigurationError(PluginError): | |
| 454 | """Error raised when a plugin has invalid configuration.""" | |
| 455 | pass | |
| 456 | ||
| 457 | ||
| 458 | def format_plugin_error(error: PluginError) -> str: | |
| 459 | """ | |
| 460 | Format a plugin error into a user-friendly message. | |
| 461 | ||
| 462 | Args: | |
| 463 | error: The plugin error | |
| 464 | ||
| 465 | Returns: | |
| 466 | str: Formatted error message | |
| 467 | """ | |
| 468 | message = str(error) | |
| 469 | ||
| 470 | if isinstance(error, PluginNotFoundError): | |
| 471 | return f"Plugin not found: {message}" | |
| 472 | ||
| 473 | if isinstance(error, PluginInitializationError): | |
| 474 | return f"Failed to initialize plugin: {message}" | |
| 475 | ||
| 476 | if isinstance(error, PluginDependencyError): | |
| 477 | dependency = error.details.get("dependency", "unknown") | |
| 478 | return f"Plugin dependency error: {message} (missing: {dependency})" | |
| 479 | ||
| 480 | if isinstance(error, PluginExecutionError): | |
| 481 | plugin = error.details.get("plugin", "unknown") | |
| 482 | operation = error.details.get("operation", "unknown operation") | |
| 483 | return f"Plugin execution error in {plugin}.{operation}: {message}" | |
| 484 | ||
| 485 | if isinstance(error, PluginConfigurationError): | |
| 486 | plugin = error.details.get("plugin", "unknown") | |
| 487 | return f"Invalid configuration for plugin {plugin}: {message}" | |
| 488 | ||
| 489 | # Generic plugin error | |
| 490 | return f"Plugin error: {message}" | |
| 491 | ||
| 492 | ||
| 493 | def log_plugin_error(error: PluginError, level=logging.ERROR) -> None: | |
| 494 | """ | |
| 495 | Log a plugin error with appropriate context. | |
| 496 | ||
| 497 | Args: | |
| 498 | error: The plugin error | |
| 499 | level: Logging level | |
| 500 | """ | |
| 501 | # Format the error message | |
| 502 | message = format_plugin_error(error) | |
| 503 | ||
| 504 | # Add details if available | |
| 505 | if error.details: | |
| 506 | details_str = ", ".join(f"{k}={v}" for k, v in error.details.items()) | |
| 507 | message = f"{message} [Details: {details_str}]" | |
| 508 | ||
| 509 | # Log the error | |
| 510 | logger.log(level, message, exc_info=True) | |
| 511 | ||
| 512 | ||
| 513 | def handle_plugin_exception(error: Exception) -> Dict[str, Any]: | |
| 514 | """ | |
| 515 | Handle a plugin exception and return error details. | |
| 516 | ||
| 517 | Args: | |
| 518 | error: The exception to handle | |
| 519 | ||
| 520 | Returns: | |
| 521 | Dict[str, Any]: Error details | |
| 522 | """ | |
| 523 | error_type = type(error).__name__ | |
| 524 | error_message = str(error) | |
| 525 | error_traceback = traceback.format_exc() | |
| 526 | ||
| 527 | # Log the error | |
| 528 | if isinstance(error, PluginError): | |
| 529 | log_plugin_error(error) | |
| 530 | else: | |
| 531 | logger.error(f"Unexpected error: {error_type}: {error_message}", exc_info=True) | |
| 532 | ||
| 533 | # Create error details | |
| 534 | error_details = { | |
| 535 | "error_type": error_type, | |
| 536 | "error_message": error_message, | |
| 537 | "traceback": error_traceback | |
| 538 | } | |
| 539 | ||
| 540 | # Add plugin-specific details | |
| 541 | if isinstance(error, PluginError) and error.details: | |
| 542 | error_details["details"] = error.details | |
| 543 | ||
| 544 | return error_details | |
| 545 | ``` | |
| 546 | ||
| 547 | ## 3. Unified Configuration System | |
| 548 | ||
| 549 | ### 3.1 Create Plugin Configuration Module | |
| 550 | ||
| 551 | **File:** `ollamatools/config/plugin_config.py` | |
| 552 | ||
| 553 | ```python | |
| 554 | """ | |
| 555 | Configuration system for the OllamaTools plugin architecture. | |
| 556 | ||
| 557 | This module provides classes and functions for managing plugin configuration. | |
| 558 | """ | |
| 559 | ||
| 560 | import os | |
| 561 | import sys | |
| 562 | import yaml | |
| 563 | import json | |
| 564 | import logging | |
| 565 | from typing import Dict, Any, List, Optional, Set, Union, Callable | |
| 566 | ||
| 567 | from ollamatools.plugins.errors import PluginConfigurationError | |
| 568 | ||
| 569 | # Configure logging | |
| 570 | logger = logging.getLogger(__name__) | |
| 571 | ||
| 572 | class PluginConfig: | |
| 573 | """Configuration manager for plugins.""" | |
| 574 | ||
| 575 | def __init__(self, config=None, schema=None): | |
| 576 | """ | |
| 577 | Initialize the configuration manager. | |
| 578 | ||
| 579 | Args: | |
| 580 | config: Initial configuration dictionary or path to config file | |
| 581 | schema: Optional schema for validation | |
| 582 | """ | |
| 583 | self.schema = schema or {} | |
| 584 | self.config = {} | |
| 585 | ||
| 586 | if config: | |
| 587 | self.load_config(config) | |
| 588 | ||
| 589 | def load_config(self, config): | |
| 590 | """ | |
| 591 | Load configuration from a dictionary, JSON, or YAML file. | |
| 592 | ||
| 593 | Args: | |
| 594 | config: Configuration source (dict, path to JSON/YAML file) | |
| 595 | ||
| 596 | Returns: | |
| 597 | PluginConfig: Self for method chaining | |
| 598 | ||
| 599 | Raises: | |
| 600 | PluginConfigurationError: If configuration loading fails | |
| 601 | """ | |
| 602 | try: | |
| 603 | if isinstance(config, dict): | |
| 604 | # Load from dictionary | |
| 605 | self.config = config | |
| 606 | elif isinstance(config, str) and os.path.exists(config): | |
| 607 | # Load from file | |
| 608 | with open(config, 'r') as f: | |
| 609 | if config.endswith('.json'): | |
| 610 | self.config = json.load(f) | |
| 611 | elif config.endswith(('.yaml', '.yml')): | |
| 612 | self.config = yaml.safe_load(f) | |
| 613 | else: | |
| 614 | raise PluginConfigurationError( | |
| 615 | f"Unsupported configuration file format: {config}", | |
| 616 | details={"file": config} | |
| 617 | ) | |
| 618 | else: | |
| 619 | raise PluginConfigurationError( | |
| 620 | f"Invalid configuration source: {config}", | |
| 621 | details={"source": str(config)} | |
| 622 | ) | |
| 623 | ||
| 624 | # Validate the configuration | |
| 625 | if self.schema: | |
| 626 | self.validate() | |
| 627 | ||
| 628 | return self | |
| 629 | ||
| 630 | except PluginConfigurationError: | |
| 631 | # Re-raise plugin configuration errors | |
| 632 | raise | |
| 633 | except Exception as e: | |
| 634 | # Wrap other exceptions | |
| 635 | raise PluginConfigurationError( | |
| 636 | f"Failed to load configuration: {str(e)}", | |
| 637 | details={"source": str(config)} | |
| 638 | ) from e | |
| 639 | ||
| 640 | def validate(self): | |
| 641 | """ | |
| 642 | Validate the configuration against the schema. | |
| 643 | ||
| 644 | Returns: | |
| 645 | bool: True if valid | |
| 646 | ||
| 647 | Raises: | |
| 648 | PluginConfigurationError: If validation fails | |
| 649 | """ | |
| 650 | # Simple schema validation | |
| 651 | for key, schema_item in self.schema.items(): | |
| 652 | # Check required fields | |
| 653 | if schema_item.get('required', False) and key not in self.config: | |
| 654 | raise PluginConfigurationError( | |
| 655 | f"Missing required configuration key: {key}", | |
| 656 | details={"key": key, "schema": schema_item} | |
| 657 | ) | |
| 658 | ||
| 659 | # Check type if the key exists | |
| 660 | if key in self.config: | |
| 661 | value = self.config[key] | |
| 662 | expected_type = schema_item.get('type') | |
| 663 | ||
| 664 | if expected_type and not isinstance(value, expected_type): | |
| 665 | raise PluginConfigurationError( | |
| 666 | f"Invalid type for configuration key {key}. Expected {expected_type.__name__}, got {type(value).__name__}", | |
| 667 | details={"key": key, "expected_type": expected_type.__name__, "actual_type": type(value).__name__} | |
| 668 | ) | |
| 669 | ||
| 670 | # Check enum values | |
| 671 | enum_values = schema_item.get('enum') | |
| 672 | if enum_values and value not in enum_values: | |
| 673 | raise PluginConfigurationError( | |
| 674 | f"Invalid value for configuration key {key}. Expected one of {enum_values}, got {value}", | |
| 675 | details={"key": key, "expected_values": enum_values, "actual_value": value} | |
| 676 | ) | |
| 677 | ||
| 678 | # Check custom validation | |
| 679 | validator = schema_item.get('validator') | |
| 680 | if validator and callable(validator): | |
| 681 | if not validator(value): | |
| 682 | raise PluginConfigurationError( | |
| 683 | f"Validation failed for configuration key {key}", | |
| 684 | details={"key": key, "value": value} | |
| 685 | ) | |
| 686 | ||
| 687 | return True | |
| 688 | ||
| 689 | def get(self, key, default=None): | |
| 690 | """ | |
| 691 | Get a configuration value. | |
| 692 | ||
| 693 | Args: | |
| 694 | key: Configuration key | |
| 695 | default: Default value if key is not found | |
| 696 | ||
| 697 | Returns: | |
| 698 | Any: Configuration value or default | |
| 699 | """ | |
| 700 | return self.config.get(key, default) | |
| 701 | ||
| 702 | def set(self, key, value): | |
| 703 | """ | |
| 704 | Set a configuration value. | |
| 705 | ||
| 706 | Args: | |
| 707 | key: Configuration key | |
| 708 | value: Configuration value | |
| 709 | ||
| 710 | Returns: | |
| 711 | PluginConfig: Self for method chaining | |
| 712 | ||
| 713 | Raises: | |
| 714 | PluginConfigurationError: If validation fails | |
| 715 | """ | |
| 716 | self.config[key] = value | |
| 717 | ||
| 718 | # Validate if schema exists for this key | |
| 719 | if self.schema and key in self.schema: | |
| 720 | schema_item = self.schema[key] | |
| 721 | ||
| 722 | # Check type | |
| 723 | expected_type = schema_item.get('type') | |
| 724 | if expected_type and not isinstance(value, expected_type): | |
| 725 | raise PluginConfigurationError( | |
| 726 | f"Invalid type for configuration key {key}. Expected {expected_type.__name__}, got {type(value).__name__}", | |
| 727 | details={"key": key, "expected_type": expected_type.__name__, "actual_type": type(value).__name__} | |
| 728 | ) | |
| 729 | ||
| 730 | # Check enum values | |
| 731 | enum_values = schema_item.get('enum') | |
| 732 | if enum_values and value not in enum_values: | |
| 733 | raise PluginConfigurationError( | |
| 734 | f"Invalid value for configuration key {key}. Expected one of {enum_values}, got {value}", | |
| 735 | details={"key": key, "expected_values": enum_values, "actual_value": value} | |
| 736 | ) | |
| 737 | ||
| 738 | # Check custom validation | |
| 739 | validator = schema_item.get('validator') | |
| 740 | if validator and callable(validator): | |
| 741 | if not validator(value): | |
| 742 | raise PluginConfigurationError( | |
| 743 | f"Validation failed for configuration key {key}", | |
| 744 | details={"key": key, "value": value} | |
| 745 | ) | |
| 746 | ||
| 747 | return self | |
| 748 | ||
| 749 | def merge(self, other_config): | |
| 750 | """ | |
| 751 | Merge another configuration into this one. | |
| 752 | ||
| 753 | Args: | |
| 754 | other_config: Another PluginConfig or dictionary | |
| 755 | ||
| 756 | Returns: | |
| 757 | PluginConfig: Self for method chaining | |
| 758 | """ | |
| 759 | if isinstance(other_config, PluginConfig): | |
| 760 | other_dict = other_config.to_dict() | |
| 761 | else: | |
| 762 | other_dict = other_config | |
| 763 | ||
| 764 | # Deep merge | |
| 765 | self._deep_merge(self.config, other_dict) | |
| 766 | ||
| 767 | # Validate the merged configuration | |
| 768 | if self.schema: | |
| 769 | self.validate() | |
| 770 | ||
| 771 | return self | |
| 772 | ||
| 773 | def _deep_merge(self, target, source): | |
| 774 | """ | |
| 775 | Deep merge two dictionaries. | |
| 776 | ||
| 777 | Args: | |
| 778 | target: Target dictionary | |
| 779 | source: Source dictionary | |
| 780 | """ | |
| 781 | for key, value in source.items(): | |
| 782 | if key in target and isinstance(target[key], dict) and isinstance(value, dict): | |
| 783 | self._deep_merge(target[key], value) | |
| 784 | else: | |
| 785 | target[key] = value | |
| 786 | ||
| 787 | def to_dict(self): | |
| 788 | """ | |
| 789 | Convert the configuration to a dictionary. | |
| 790 | ||
| 791 | Returns: | |
| 792 | Dict[str, Any]: Configuration dictionary | |
| 793 | """ | |
| 794 | return self.config.copy() | |
| 795 | ||
| 796 | def save(self, file_path, format='yaml'): | |
| 797 | """ | |
| 798 | Save the configuration to a file. | |
| 799 | ||
| 800 | Args: | |
| 801 | file_path: Path to save the configuration to | |
| 802 | format: File format ('json' or 'yaml') | |
| 803 | ||
| 804 | Returns: | |
| 805 | PluginConfig: Self for method chaining | |
| 806 | ||
| 807 | Raises: | |
| 808 | PluginConfigurationError: If saving fails | |
| 809 | """ | |
| 810 | try: | |
| 811 | # Create directory if it doesn't exist | |
| 812 | os.makedirs(os.path.dirname(file_path), exist_ok=True) | |
| 813 | ||
| 814 | with open(file_path, 'w') as f: | |
| 815 | if format.lower() == 'json': | |
| 816 | json.dump(self.config, f, indent=2) | |
| 817 | elif format.lower() in ('yaml', 'yml'): | |
| 818 | yaml.dump(self.config, f, default_flow_style=False) | |
| 819 | else: | |
| 820 | raise PluginConfigurationError( | |
| 821 | f"Unsupported configuration format: {format}", | |
| 822 | details={"format": format} | |
| 823 | ) | |
| 824 | ||
| 825 | return self | |
| 826 | ||
| 827 | except PluginConfigurationError: | |
| 828 | # Re-raise plugin configuration errors | |
| 829 | raise | |
| 830 | except Exception as e: | |
| 831 | # Wrap other exceptions | |
| 832 | raise PluginConfigurationError( | |
| 833 | f"Failed to save configuration: {str(e)}", | |
| 834 | details={"file_path": file_path, "format": format} | |
| 835 | ) from e | |
| 836 | ||
| 837 | ||
| 838 | def create_default_config(plugin_name, config_type='shared'): | |
| 839 | """ | |
| 840 | Create a default configuration for a plugin. | |
| 841 | ||
| 842 | Args: | |
| 843 | plugin_name: Name of the plugin | |
| 844 | config_type: Type of configuration ('shared', 'matrix', 'agent') | |
| 845 | ||
| 846 | Returns: | |
| 847 | Dict[str, Any]: Default configuration | |
| 848 | """ | |
| 849 | # Base configuration | |
| 850 | config = { | |
| 851 | "enabled": True, | |
| 852 | "name": plugin_name | |
| 853 | } | |
| 854 | ||
| 855 | # Add type-specific defaults | |
| 856 | if config_type == 'shared': | |
| 857 | pass # No additional defaults | |
| 858 | elif config_type == 'matrix': | |
| 859 | config.update({ | |
| 860 | "commands_enabled": True, | |
| 861 | "command_prefix": "/" | |
| 862 | }) | |
| 863 | elif config_type == 'agent': | |
| 864 | config.update({ | |
| 865 | "functions_enabled": True | |
| 866 | }) | |
| 867 | ||
| 868 | return config | |
| 869 | ||
| 870 | ||
| 871 | def load_plugin_config(plugin_name, config_dir=None): | |
| 872 | """ | |
| 873 | Load configuration for a plugin from the default location. | |
| 874 | ||
| 875 | Args: | |
| 876 | plugin_name: Name of the plugin | |
| 877 | config_dir: Optional custom configuration directory | |
| 878 | ||
| 879 | Returns: | |
| 880 | PluginConfig: Plugin configuration | |
| 881 | """ | |
| 882 | if not config_dir: | |
| 883 | # Use default location | |
| 884 | config_dir = os.path.join( | |
| 885 | os.path.expanduser('~'), | |
| 886 | '.ollamatools', | |
| 887 | 'plugins' | |
| 888 | ) | |
| 889 | ||
| 890 | # Check for plugin-specific config file | |
| 891 | config_file = os.path.join(config_dir, f"{plugin_name.lower()}.yaml") | |
| 892 | ||
| 893 | if os.path.exists(config_file): | |
| 894 | return PluginConfig(config_file) | |
| 895 | else: | |
| 896 | # Return default configuration | |
| 897 | return PluginConfig(create_default_config(plugin_name)) | |
| 898 | ``` | |
| 899 | ||
| 900 | ### 3.2 Create Configuration Schema Module | |
| 901 | ||
| 902 | **File:** `ollamatools/config/schemas.py` | |
| 903 | ||
| 904 | ```python | |
| 905 | """ | |
| 906 | Configuration schemas for OllamaTools plugins. | |
| 907 | ||
| 908 | This module provides schema definitions for validating plugin configurations. | |
| 909 | """ | |
| 910 | ||
| 911 | from typing import Dict, Any, List, Optional, Set, Union, Callable | |
| 912 | ||
| 913 | # Schema for the ConversationHistoryPlugin | |
| 914 | CONVERSATION_HISTORY_SCHEMA = { | |
| 915 | "history_file": { | |
| 916 | "type": str, | |
| 917 | "required": False, | |
| 918 | "description": "Path to the conversation history file" | |
| 919 | }, | |
| 920 | "max_history": { | |
| 921 | "type": int, | |
| 922 | "required": False, | |
| 923 | "default": 100, | |
| 924 | "description": "Maximum number of conversations to keep in history" | |
| 925 | }, | |
| 926 | "enable_topic_analysis": { | |
| 927 | "type": bool, | |
| 928 | "required": False, | |
| 929 | "default": True, | |
| 930 | "description": "Whether to enable topic analysis" | |
| 931 | }, | |
| 932 | "enable_metrics": { | |
| 933 | "type": bool, | |
| 934 | "required": False, | |
| 935 | "default": True, | |
| 936 | "description": "Whether to enable performance metrics" | |
| 937 | } | |
| 938 | } | |
| 939 | ||
| 940 | # Schema for the SearchPlugin | |
| 941 | SEARCH_PLUGIN_SCHEMA = { | |
| 942 | "base_url": { | |
| 943 | "type": str, | |
| 944 | "required": False, | |
| 945 | "default": "http://localhost:8081", | |
| 946 | "description": "Base URL for the OpenSearchNG service" | |
| 947 | }, | |
| 948 | "engines": { | |
| 949 | "type": list, | |
| 950 | "required": False, | |
| 951 | "default": ["google", "bing", "duckduckgo", "brave"], | |
| 952 | "description": "List of search engines to use" | |
| 953 | }, | |
| 954 | "num_results": { | |
| 955 | "type": int, | |
| 956 | "required": False, | |
| 957 | "default": 5, | |
| 958 | "description": "Number of search results to return" | |
| 959 | }, | |
| 960 | "enable_auto_search": { | |
| 961 | "type": bool, | |
| 962 | "required": False, | |
| 963 | "default": True, | |
| 964 | "description": "Whether to enable automatic search for factual queries" | |
| 965 | } | |
| 966 | } | |
| 967 | ||
| 968 | # Schema for the MatrixAgentAdapterPlugin | |
| 969 | MATRIX_AGENT_ADAPTER_SCHEMA = { | |
| 970 | "agent_class": { | |
| 971 | "type": (str, object), # Can be a string path or a class object | |
| 972 | "required": True, | |
| 973 | "description": "Agent class to use (string path or class object)" | |
| 974 | }, | |
| 975 | "agent_config": { | |
| 976 | "type": dict, | |
| 977 | "required": False, | |
| 978 | "default": {}, | |
| 979 | "description": "Configuration for the agent" | |
| 980 | } | |
| 981 | } | |
| 982 | ||
| 983 | # Schema for the MatrixCorePlugin | |
| 984 | MATRIX_CORE_PLUGIN_SCHEMA = { | |
| 985 | "homeserver": { | |
| 986 | "type": str, | |
| 987 | "required": True, | |
| 988 | "description": "URL of the Matrix homeserver" | |
| 989 | }, | |
| 990 | "user_id": { | |
| 991 | "type": str, | |
| 992 | "required": True, | |
| 993 | "description": "Matrix user ID" | |
| 994 | }, | |
| 995 | "password": { | |
| 996 | "type": str, | |
| 997 | "required": True, | |
| 998 | "description": "Matrix password" | |
| 999 | }, | |
| 1000 | "device_id": { | |
| 1001 | "type": str, | |
| 1002 | "required": False, | |
| 1003 | "description": "Device ID for E2E encryption" | |
| 1004 | }, | |
| 1005 | "ssl": { | |
| 1006 | "type": bool, | |
| 1007 | "required": False, | |
| 1008 | "default": True, | |
| 1009 | "description": "Whether to use SSL for the Matrix connection" | |
| 1010 | }, | |
| 1011 | "respond_to_all_messages": { | |
| 1012 | "type": bool, | |
| 1013 | "required": False, | |
| 1014 | "default": False, | |
| 1015 | "description": "Whether to respond to all messages (not just mentions)" | |
| 1016 | } | |
| 1017 | } | |
| 1018 | ||
| 1019 | # Schema for the ConversationContextPlugin | |
| 1020 | CONVERSATION_CONTEXT_SCHEMA = { | |
| 1021 | "max_context_length": { | |
| 1022 | "type": int, | |
| 1023 | "required": False, | |
| 1024 | "default": 10, | |
| 1025 | "description": "Maximum number of messages to keep in context" | |
| 1026 | }, | |
| 1027 | "context_strategy": { | |
| 1028 | "type": str, | |
| 1029 | "required": False, | |
| 1030 | "default": "sliding_window", | |
| 1031 | "enum": ["sliding_window", "token_based", "time_based", "relevance_based"], | |
| 1032 | "description": "Strategy for managing conversation context" | |
| 1033 | }, | |
| 1034 | "token_limit": { | |
| 1035 | "type": int, | |
| 1036 | "required": False, | |
| 1037 | "default": 4096, | |
| 1038 | "description": "Maximum number of tokens to keep in context (for token_based strategy)" | |
| 1039 | }, | |
| 1040 | "time_window": { | |
| 1041 | "type": int, | |
| 1042 | "required": False, | |
| 1043 | "default": 3600, | |
| 1044 | "description": "Time window in seconds for context (for time_based strategy)" | |
| 1045 | } | |
| 1046 | } | |
| 1047 | ||
| 1048 | # Schema registry | |
| 1049 | SCHEMA_REGISTRY = { | |
| 1050 | "ConversationHistoryPlugin": CONVERSATION_HISTORY_SCHEMA, | |
| 1051 | "SearchPlugin": SEARCH_PLUGIN_SCHEMA, | |
| 1052 | "MatrixAgentAdapterPlugin": MATRIX_AGENT_ADAPTER_SCHEMA, | |
| 1053 | "MatrixCorePlugin": MATRIX_CORE_PLUGIN_SCHEMA, | |
| 1054 | "ConversationContextPlugin": CONVERSATION_CONTEXT_SCHEMA | |
| 1055 | } | |
| 1056 | ||
| 1057 | def get_schema_for_plugin(plugin_name): | |
| 1058 | """ | |
| 1059 | Get the configuration schema for a plugin. | |
| 1060 | ||
| 1061 | Args: | |
| 1062 | plugin_name: Name of the plugin | |
| 1063 | ||
| 1064 | Returns: | |
| 1065 | Dict[str, Any]: Schema dictionary or empty dict if not found | |
| 1066 | """ | |
| 1067 | return SCHEMA_REGISTRY.get(plugin_name, {}) | |
| 1068 | ``` | |
| 1069 | ||
| 1070 | ## 4. Directory Structure Setup | |
| 1071 | ||
| 1072 | ### 4.1 Create Plugin Package Structure | |
| 1073 | ||
| 1074 | ``` | |
| 1075 | ollamatools/ | |
| 1076 | ├── plugins/ | |
| 1077 | │ ├── __init__.py # Base plugin interfaces | |
| 1078 | │ ├── registry.py # Plugin registry | |
| 1079 | │ ├── errors.py # Error handling | |
| 1080 | │ └── README.md # Documentation | |
| 1081 | ├── config/ | |
| 1082 | │ ├── __init__.py # Package initialization | |
| 1083 | │ ├── plugin_config.py # Configuration system | |
| 1084 | │ └── schemas.py # Configuration schemas | |
| 1085 | ``` | |
| 1086 | ||
| 1087 | ### 4.2 Create Plugin README | |
| 1088 | ||
| 1089 | **File:** `ollamatools/plugins/README.md` | |
| 1090 | ||
| 1091 | ```markdown | |
| 1092 | # OllamaTools Plugin System | |
| 1093 | ||
| 1094 | This directory contains the core components of the OllamaTools plugin system. | |
| 1095 | ||
| 1096 | ## Overview | |
| 1097 | ||
| 1098 | The plugin system provides a flexible way to extend OllamaTools functionality through plugins. Plugins can be used to add new features, integrate with external services, or customize behavior. | |
| 1099 | ||
| 1100 | ## Plugin Types | |
| 1101 | ||
| 1102 | The plugin system supports several types of plugins: | |
| 1103 | ||
| 1104 | - **SharedPlugin**: Plugins that can be used by multiple components | |
| 1105 | - **MatrixPlugin |
| … | … | @@ -0,0 +1,553 @@ |
| 1 | # Unified Plugin Architecture for OllamaTools | |
| 2 | ||
| 3 | This document proposes a unified plugin architecture for the OllamaTools project, focusing on better integration between matrix, modular, and extended agent libraries. | |
| 4 | ||
| 5 | ## Overview | |
| 6 | ||
| 7 | The current OllamaTools project has several components that operate somewhat independently: | |
| 8 | ||
| 9 | 1. **Matrix Integration**: Provides Matrix chat client functionality | |
| 10 | 2. **Modular Agent**: Provides a modular agent architecture with plugins | |
| 11 | 3. **Extended Agent**: Extends the modular agent with additional features | |
| 12 | ||
| 13 | These components share some functionality but are not well integrated. This proposal outlines a unified plugin architecture that would allow these components to work together more seamlessly. | |
| 14 | ||
| 15 | ## Core Design Principles | |
| 16 | ||
| 17 | 1. **Separation of Concerns**: Each plugin should have a clear, focused responsibility | |
| 18 | 2. **Extensibility**: The system should be easy to extend with new plugins | |
| 19 | 3. **Consistency**: Similar functionality should be implemented in similar ways across components | |
| 20 | 4. **Reusability**: Common functionality should be extracted into shared modules | |
| 21 | 5. **Discoverability**: Plugins should be easy to discover and use | |
| 22 | ||
| 23 | ## Proposed Architecture | |
| 24 | ||
| 25 | ### 1. Plugin Interface | |
| 26 | ||
| 27 | At the core of the architecture is a common plugin interface: | |
| 28 | ||
| 29 | ```python | |
| 30 | class OllamaToolsPlugin: | |
| 31 | """Base interface for all OllamaTools plugins.""" | |
| 32 | ||
| 33 | def __init__(self, config=None): | |
| 34 | """Initialize the plugin with optional configuration.""" | |
| 35 | self.config = config or {} | |
| 36 | ||
| 37 | def initialize(self, context): | |
| 38 | """Initialize the plugin with the runtime context.""" | |
| 39 | pass | |
| 40 | ||
| 41 | def shutdown(self): | |
| 42 | """Clean up resources when shutting down.""" | |
| 43 | pass | |
| 44 | ||
| 45 | @property | |
| 46 | def name(self): | |
| 47 | """Get the plugin name.""" | |
| 48 | return self.__class__.__name__ | |
| 49 | ||
| 50 | @property | |
| 51 | def version(self): | |
| 52 | """Get the plugin version.""" | |
| 53 | return "1.0.0" | |
| 54 | ||
| 55 | @property | |
| 56 | def dependencies(self): | |
| 57 | """Get plugin dependencies.""" | |
| 58 | return [] | |
| 59 | ``` | |
| 60 | ||
| 61 | ### 2. Plugin Registry | |
| 62 | ||
| 63 | A central registry manages plugin discovery, loading, and dependencies: | |
| 64 | ||
| 65 | ```python | |
| 66 | class PluginRegistry: | |
| 67 | """Registry for OllamaTools plugins.""" | |
| 68 | ||
| 69 | def __init__(self): | |
| 70 | """Initialize the plugin registry.""" | |
| 71 | self.plugins = {} | |
| 72 | self.initialized_plugins = {} | |
| 73 | ||
| 74 | def register_plugin(self, plugin_class, config=None): | |
| 75 | """Register a plugin class.""" | |
| 76 | plugin_name = plugin_class.__name__ | |
| 77 | self.plugins[plugin_name] = { | |
| 78 | "class": plugin_class, | |
| 79 | "config": config | |
| 80 | } | |
| 81 | ||
| 82 | def discover_plugins(self): | |
| 83 | """Discover plugins using entry points.""" | |
| 84 | import pkg_resources | |
| 85 | for entry_point in pkg_resources.iter_entry_points('ollamatools.plugins'): | |
| 86 | plugin_class = entry_point.load() | |
| 87 | self.register_plugin(plugin_class) | |
| 88 | ||
| 89 | def initialize_plugin(self, plugin_name, context): | |
| 90 | """Initialize a plugin and its dependencies.""" | |
| 91 | if plugin_name in self.initialized_plugins: | |
| 92 | return self.initialized_plugins[plugin_name] | |
| 93 | ||
| 94 | if plugin_name not in self.plugins: | |
| 95 | raise ValueError(f"Plugin {plugin_name} not found") | |
| 96 | ||
| 97 | plugin_info = self.plugins[plugin_name] | |
| 98 | plugin_class = plugin_info["class"] | |
| 99 | plugin_config = plugin_info["config"] | |
| 100 | ||
| 101 | # Initialize dependencies first | |
| 102 | plugin_instance = plugin_class(plugin_config) | |
| 103 | for dependency in plugin_instance.dependencies: | |
| 104 | self.initialize_plugin(dependency, context) | |
| 105 | ||
| 106 | # Initialize the plugin | |
| 107 | plugin_instance.initialize(context) | |
| 108 | self.initialized_plugins[plugin_name] = plugin_instance | |
| 109 | ||
| 110 | return plugin_instance | |
| 111 | ||
| 112 | def get_plugin(self, plugin_name): | |
| 113 | """Get an initialized plugin.""" | |
| 114 | return self.initialized_plugins.get(plugin_name) | |
| 115 | ||
| 116 | def shutdown(self): | |
| 117 | """Shutdown all initialized plugins.""" | |
| 118 | for plugin_name, plugin in self.initialized_plugins.items(): | |
| 119 | try: | |
| 120 | plugin.shutdown() | |
| 121 | except Exception as e: | |
| 122 | print(f"Error shutting down plugin {plugin_name}: {e}") | |
| 123 | ||
| 124 | self.initialized_plugins = {} | |
| 125 | ``` | |
| 126 | ||
| 127 | ### 3. Specialized Plugin Types | |
| 128 | ||
| 129 | From the base plugin interface, we can derive specialized plugin types for different components: | |
| 130 | ||
| 131 | #### 3.1 Agent Plugins | |
| 132 | ||
| 133 | ```python | |
| 134 | class AgentPlugin(OllamaToolsPlugin): | |
| 135 | """Base class for agent plugins.""" | |
| 136 | ||
| 137 | def process_message(self, message, context): | |
| 138 | """Process a message and return a response.""" | |
| 139 | return None | |
| 140 | ||
| 141 | def get_commands(self): | |
| 142 | """Get commands provided by this plugin.""" | |
| 143 | return {} | |
| 144 | ||
| 145 | def on_function_call(self, function_name, args, context): | |
| 146 | """Handle a function call.""" | |
| 147 | return None | |
| 148 | ``` | |
| 149 | ||
| 150 | #### 3.2 Matrix Plugins | |
| 151 | ||
| 152 | ```python | |
| 153 | class MatrixPlugin(OllamaToolsPlugin): | |
| 154 | """Base class for Matrix plugins.""" | |
| 155 | ||
| 156 | def on_message(self, room, event, client): | |
| 157 | """Handle an incoming Matrix message.""" | |
| 158 | return False | |
| 159 | ||
| 160 | def get_commands(self): | |
| 161 | """Get Matrix commands provided by this plugin.""" | |
| 162 | return {} | |
| 163 | ||
| 164 | def on_room_join(self, room, client): | |
| 165 | """Handle room join event.""" | |
| 166 | pass | |
| 167 | ||
| 168 | def on_client_start(self, client): | |
| 169 | """Handle client start event.""" | |
| 170 | pass | |
| 171 | ||
| 172 | def on_client_stop(self, client): | |
| 173 | """Handle client stop event.""" | |
| 174 | pass | |
| 175 | ``` | |
| 176 | ||
| 177 | #### 3.3 Shared Plugins | |
| 178 | ||
| 179 | ```python | |
| 180 | class SharedPlugin(OllamaToolsPlugin): | |
| 181 | """Base class for plugins that can be used by multiple components.""" | |
| 182 | ||
| 183 | def get_capabilities(self): | |
| 184 | """Get capabilities provided by this plugin.""" | |
| 185 | return [] | |
| 186 | ``` | |
| 187 | ||
| 188 | ### 4. Plugin Implementation Examples | |
| 189 | ||
| 190 | #### 4.1 Search Plugin | |
| 191 | ||
| 192 | ```python | |
| 193 | class SearchPlugin(SharedPlugin): | |
| 194 | """Plugin for search functionality.""" | |
| 195 | ||
| 196 | def __init__(self, config=None): | |
| 197 | super().__init__(config) | |
| 198 | self.opensearch_config = config.get("opensearch", {}) | |
| 199 | ||
| 200 | def initialize(self, context): | |
| 201 | """Initialize the search plugin.""" | |
| 202 | try: | |
| 203 | from ollamatools.opensearch_web_tools import get_opensearch_info | |
| 204 | self.opensearch_info = get_opensearch_info(self.opensearch_config) | |
| 205 | self.available = True | |
| 206 | except ImportError: | |
| 207 | self.available = False | |
| 208 | ||
| 209 | def detect_factual_intent(self, message): | |
| 210 | """Detect if a message has factual intent that would benefit from search.""" | |
| 211 | if not self.available: | |
| 212 | return False | |
| 213 | ||
| 214 | from ollamatools.agent.modular.search import detect_factual_intent | |
| 215 | return detect_factual_intent(message) | |
| 216 | ||
| 217 | def execute_search(self, query): | |
| 218 | """Execute a search query.""" | |
| 219 | if not self.available: | |
| 220 | return {"error": "Search functionality not available"} | |
| 221 | ||
| 222 | from ollamatools.opensearch_web_tools import opensearch_query | |
| 223 | return opensearch_query(query, self.opensearch_config) | |
| 224 | ||
| 225 | def get_capabilities(self): | |
| 226 | """Get capabilities provided by this plugin.""" | |
| 227 | return ["search"] if self.available else [] | |
| 228 | ``` | |
| 229 | ||
| 230 | #### 4.2 Conversation History Plugin | |
| 231 | ||
| 232 | ```python | |
| 233 | class ConversationHistoryPlugin(SharedPlugin): | |
| 234 | """Plugin for conversation history tracking.""" | |
| 235 | ||
| 236 | def __init__(self, config=None): | |
| 237 | super().__init__(config) | |
| 238 | self.history_file = config.get("history_file") | |
| 239 | self.max_history = config.get("max_history", 100) | |
| 240 | self.conversation_history = [] | |
| 241 | ||
| 242 | def initialize(self, context): | |
| 243 | """Initialize the conversation history plugin.""" | |
| 244 | self._load_conversation_history() | |
| 245 | ||
| 246 | def _load_conversation_history(self): | |
| 247 | """Load conversation history from file.""" | |
| 248 | import os | |
| 249 | import json | |
| 250 | ||
| 251 | if not self.history_file or not os.path.exists(self.history_file): | |
| 252 | return | |
| 253 | ||
| 254 | try: | |
| 255 | with open(self.history_file, 'r') as f: | |
| 256 | self.conversation_history = json.load(f) | |
| 257 | except Exception as e: | |
| 258 | print(f"Error loading conversation history: {e}") | |
| 259 | ||
| 260 | def _save_conversation_history(self): | |
| 261 | """Save conversation history to file.""" | |
| 262 | import os | |
| 263 | import json | |
| 264 | ||
| 265 | if not self.history_file: | |
| 266 | return | |
| 267 | ||
| 268 | try: | |
| 269 | os.makedirs(os.path.dirname(self.history_file), exist_ok=True) | |
| 270 | with open(self.history_file, 'w') as f: | |
| 271 | json.dump(self.conversation_history, f, indent=2) | |
| 272 | except Exception as e: | |
| 273 | print(f"Error saving conversation history: {e}") | |
| 274 | ||
| 275 | def record_conversation(self, user_message, assistant_response, metadata=None): | |
| 276 | """Record a conversation.""" | |
| 277 | import time | |
| 278 | ||
| 279 | conversation = { | |
| 280 | "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), | |
| 281 | "user_message": user_message, | |
| 282 | "assistant_response": assistant_response, | |
| 283 | "metadata": metadata or {} | |
| 284 | } | |
| 285 | ||
| 286 | self.conversation_history.append(conversation) | |
| 287 | ||
| 288 | # Trim conversation history if it gets too long | |
| 289 | if len(self.conversation_history) > self.max_history: | |
| 290 | self.conversation_history = self.conversation_history[-self.max_history:] | |
| 291 | ||
| 292 | self._save_conversation_history() | |
| 293 | ||
| 294 | return conversation | |
| 295 | ||
| 296 | def get_recent_conversations(self, n=5): | |
| 297 | """Get recent conversations.""" | |
| 298 | return self.conversation_history[-n:] if len(self.conversation_history) >= n else self.conversation_history | |
| 299 | ||
| 300 | def analyze_topics(self): | |
| 301 | """Analyze conversation topics.""" | |
| 302 | # Implementation similar to existing code | |
| 303 | pass | |
| 304 | ||
| 305 | def get_metrics(self): | |
| 306 | """Get performance metrics.""" | |
| 307 | # Implementation similar to existing code | |
| 308 | pass | |
| 309 | ||
| 310 | def get_capabilities(self): | |
| 311 | """Get capabilities provided by this plugin.""" | |
| 312 | return ["conversation_history", "topic_analysis", "performance_metrics"] | |
| 313 | ||
| 314 | def shutdown(self): | |
| 315 | """Save conversation history when shutting down.""" | |
| 316 | self._save_conversation_history() | |
| 317 | ``` | |
| 318 | ||
| 319 | #### 4.3 Matrix Agent Adapter Plugin | |
| 320 | ||
| 321 | ```python | |
| 322 | class MatrixAgentAdapterPlugin(MatrixPlugin): | |
| 323 | """Plugin that adapts an agent for use with Matrix.""" | |
| 324 | ||
| 325 | def __init__(self, config=None): | |
| 326 | super().__init__(config) | |
| 327 | self.agent_class = config.get("agent_class") | |
| 328 | self.agent_config = config.get("agent_config", {}) | |
| 329 | self.agent = None | |
| 330 | ||
| 331 | def initialize(self, context): | |
| 332 | """Initialize the plugin with an agent instance.""" | |
| 333 | if not self.agent_class: | |
| 334 | raise ValueError("Agent class not specified") | |
| 335 | ||
| 336 | self.agent = self.agent_class(**self.agent_config) | |
| 337 | self.registry = context.get("plugin_registry") | |
| 338 | ||
| 339 | def on_message(self, room, event, client): | |
| 340 | """Handle an incoming Matrix message using the agent.""" | |
| 341 | message = event.body | |
| 342 | sender = event.sender | |
| 343 | room_id = room.room_id | |
| 344 | ||
| 345 | # Process the message with the agent | |
| 346 | response = self.agent.chat(message) | |
| 347 | ||
| 348 | # Record the conversation if history plugin is available | |
| 349 | history_plugin = self.registry.get_plugin("ConversationHistoryPlugin") | |
| 350 | if history_plugin: | |
| 351 | metadata = { | |
| 352 | "sender": sender, | |
| 353 | "room_id": room_id, | |
| 354 | "timestamp": event.server_timestamp | |
| 355 | } | |
| 356 | history_plugin.record_conversation(message, response, metadata) | |
| 357 | ||
| 358 | # Send the response | |
| 359 | client.send_message(room_id, response) | |
| 360 | ||
| 361 | return True | |
| 362 | ||
| 363 | def get_commands(self): | |
| 364 | """Get commands from the agent.""" | |
| 365 | if hasattr(self.agent, "get_commands"): | |
| 366 | return self.agent.get_commands() | |
| 367 | return {} | |
| 368 | ||
| 369 | def shutdown(self): | |
| 370 | """Clean up the agent.""" | |
| 371 | if hasattr(self.agent, "shutdown"): | |
| 372 | self.agent.shutdown() | |
| 373 | ``` | |
| 374 | ||
| 375 | ### 5. Integration with Existing Components | |
| 376 | ||
| 377 | #### 5.1 Matrix Client Integration | |
| 378 | ||
| 379 | ```python | |
| 380 | class EnhancedMatrixClient: | |
| 381 | """Enhanced Matrix client with plugin support.""" | |
| 382 | ||
| 383 | def __init__(self, homeserver, user_id, password, **kwargs): | |
| 384 | """Initialize the Matrix client with plugins.""" | |
| 385 | self.homeserver = homeserver | |
| 386 | self.user_id = user_id | |
| 387 | self.password = password | |
| 388 | self.kwargs = kwargs | |
| 389 | ||
| 390 | # Initialize plugin registry | |
| 391 | self.plugin_registry = PluginRegistry() | |
| 392 | ||
| 393 | # Register built-in plugins | |
| 394 | self.register_builtin_plugins() | |
| 395 | ||
| 396 | # Initialize client | |
| 397 | self.client = None | |
| 398 | ||
| 399 | def register_builtin_plugins(self): | |
| 400 | """Register built-in plugins.""" | |
| 401 | from ollamatools.plugins.search import SearchPlugin | |
| 402 | from ollamatools.plugins.conversation_history import ConversationHistoryPlugin | |
| 403 | ||
| 404 | self.plugin_registry.register_plugin(SearchPlugin, self.kwargs.get("search_config")) | |
| 405 | self.plugin_registry.register_plugin(ConversationHistoryPlugin, self.kwargs.get("history_config")) | |
| 406 | ||
| 407 | def register_plugin(self, plugin_class, config=None): | |
| 408 | """Register a custom plugin.""" | |
| 409 | self.plugin_registry.register_plugin(plugin_class, config) | |
| 410 | ||
| 411 | async def connect(self): | |
| 412 | """Connect to the Matrix homeserver and initialize plugins.""" | |
| 413 | # Connect to Matrix | |
| 414 | # ... | |
| 415 | ||
| 416 | # Initialize plugins | |
| 417 | context = { | |
| 418 | "client": self, | |
| 419 | "plugin_registry": self.plugin_registry | |
| 420 | } | |
| 421 | ||
| 422 | for plugin_name in self.plugin_registry.plugins: | |
| 423 | self.plugin_registry.initialize_plugin(plugin_name, context) | |
| 424 | ||
| 425 | return True | |
| 426 | ||
| 427 | async def _message_callback(self, room, event): | |
| 428 | """Handle incoming messages using plugins.""" | |
| 429 | # Skip messages from self, etc. | |
| 430 | # ... | |
| 431 | ||
| 432 | # Try each plugin until one handles the message | |
| 433 | for plugin_name, plugin in self.plugin_registry.initialized_plugins.items(): | |
| 434 | if isinstance(plugin, MatrixPlugin): | |
| 435 | if await plugin.on_message(room, event, self): | |
| 436 | return | |
| 437 | ||
| 438 | # Default message handling if no plugin handled it | |
| 439 | # ... | |
| 440 | ||
| 441 | async def disconnect(self): | |
| 442 | """Disconnect from Matrix and shutdown plugins.""" | |
| 443 | # Disconnect from Matrix | |
| 444 | # ... | |
| 445 | ||
| 446 | # Shutdown plugins | |
| 447 | self.plugin_registry.shutdown() | |
| 448 | ``` | |
| 449 | ||
| 450 | #### 5.2 Modular Agent Integration | |
| 451 | ||
| 452 | ```python | |
| 453 | class PluginEnabledModularAgent(ModularOllamaAgent): | |
| 454 | """ModularOllamaAgent with plugin support.""" | |
| 455 | ||
| 456 | def __init__(self, *args, **kwargs): | |
| 457 | """Initialize the agent with plugins.""" | |
| 458 | super().__init__(*args, **kwargs) | |
| 459 | ||
| 460 | # Initialize plugin registry | |
| 461 | self.plugin_registry = PluginRegistry() | |
| 462 | ||
| 463 | # Register built-in plugins | |
| 464 | self.register_builtin_plugins() | |
| 465 | ||
| 466 | # Initialize plugins | |
| 467 | self.initialize_plugins() | |
| 468 | ||
| 469 | def register_builtin_plugins(self): | |
| 470 | """Register built-in plugins.""" | |
| 471 | from ollamatools.plugins.search import SearchPlugin | |
| 472 | from ollamatools.plugins.conversation_history import ConversationHistoryPlugin | |
| 473 | ||
| 474 | self.plugin_registry.register_plugin(SearchPlugin, self.config.get("search_config")) | |
| 475 | self.plugin_registry.register_plugin(ConversationHistoryPlugin, self.config.get("history_config")) | |
| 476 | ||
| 477 | def register_plugin(self, plugin_class, config=None): | |
| 478 | """Register a custom plugin.""" | |
| 479 | self.plugin_registry.register_plugin(plugin_class, config) | |
| 480 | ||
| 481 | def initialize_plugins(self): | |
| 482 | """Initialize all registered plugins.""" | |
| 483 | context = { | |
| 484 | "agent": self, | |
| 485 | "plugin_registry": self.plugin_registry | |
| 486 | } | |
| 487 | ||
| 488 | for plugin_name in self.plugin_registry.plugins: | |
| 489 | self.plugin_registry.initialize_plugin(plugin_name, context) | |
| 490 | ||
| 491 | def chat(self, message): | |
| 492 | """Process a chat message using plugins.""" | |
| 493 | # Try each plugin for message processing | |
| 494 | for plugin_name, plugin in self.plugin_registry.initialized_plugins.items(): | |
| 495 | if isinstance(plugin, AgentPlugin): | |
| 496 | response = plugin.process_message(message, {"agent": self}) | |
| 497 | if response: | |
| 498 | return response | |
| 499 | ||
| 500 | # Fall back to default processing | |
| 501 | return super().chat(message) | |
| 502 | ||
| 503 | def shutdown(self): | |
| 504 | """Shutdown the agent and all plugins.""" | |
| 505 | self.plugin_registry.shutdown() | |
| 506 | super().shutdown() | |
| 507 | ``` | |
| 508 | ||
| 509 | ## Implementation Plan | |
| 510 | ||
| 511 | ### Phase 1: Core Infrastructure | |
| 512 | ||
| 513 | 1. Create the base plugin interface and registry | |
| 514 | 2. Implement specialized plugin types | |
| 515 | 3. Create utility functions for plugin discovery and loading | |
| 516 | ||
| 517 | ### Phase 2: Shared Plugins | |
| 518 | ||
| 519 | 1. Implement the SearchPlugin | |
| 520 | 2. Implement the ConversationHistoryPlugin | |
| 521 | 3. Create tests for these plugins | |
| 522 | ||
| 523 | ### Phase 3: Matrix Integration | |
| 524 | ||
| 525 | 1. Refactor the EnhancedMatrixClient to use plugins | |
| 526 | 2. Implement the MatrixAgentAdapterPlugin | |
| 527 | 3. Update the matrix command handling to use the plugin registry | |
| 528 | ||
| 529 | ### Phase 4: Agent Integration | |
| 530 | ||
| 531 | 1. Refactor the ModularOllamaAgent to use plugins | |
| 532 | 2. Update the ExtendedModularAgent to leverage shared plugins | |
| 533 | 3. Create adapter plugins for different agent types | |
| 534 | ||
| 535 | ### Phase 5: Documentation and Examples | |
| 536 | ||
| 537 | 1. Create comprehensive documentation for the plugin system | |
| 538 | 2. Provide examples of creating custom plugins | |
| 539 | 3. Update existing examples to use the new plugin architecture | |
| 540 | ||
| 541 | ## Benefits | |
| 542 | ||
| 543 | This unified plugin architecture provides several benefits: | |
| 544 | ||
| 545 | 1. **Reduced Code Duplication**: Common functionality is extracted into shared plugins | |
| 546 | 2. **Improved Extensibility**: New features can be added as plugins without modifying core code | |
| 547 | 3. **Better Integration**: Matrix clients and agents can share functionality through plugins | |
| 548 | 4. **Consistent Interfaces**: All plugins follow the same patterns and conventions | |
| 549 | 5. **Simplified Maintenance**: Each plugin has a clear, focused responsibility | |
| 550 | ||
| 551 | ## Conclusion | |
| 552 | ||
| 553 | The proposed unified plugin architecture provides a solid foundation for better integration between the matrix, modular, and extended agent libraries in the OllamaTools project. By implementing this architecture, we can reduce code duplication, improve extensibility, and create a more cohesive user experience across all components. |
| … | … | @@ -0,0 +1,546 @@ |
| 1 | # Matrix Integration for OllamaTools | |
| 2 | ||
| 3 | This document provides information on how to use the Matrix integration in OllamaTools to connect your Ollama-powered applications to Matrix chat rooms. | |
| 4 | ||
| 5 | There are two Matrix client implementations available: | |
| 6 | 1. **Basic Matrix Client**: The standard Matrix client with basic functionality | |
| 7 | 2. **Enhanced Matrix Client**: An extended client with advanced features from modular and extended agents | |
| 8 | ||
| 9 | ## Overview | |
| 10 | ||
| 11 | The Matrix integration allows you to: | |
| 12 | ||
| 13 | - Connect to a Matrix homeserver | |
| 14 | - Join Matrix rooms | |
| 15 | - Send messages to rooms | |
| 16 | - Listen for incoming messages | |
| 17 | - Process messages with Ollama and send responses | |
| 18 | - Switch between different Ollama models | |
| 19 | - Maintain conversation context for each user | |
| 20 | ||
| 21 | ## Requirements | |
| 22 | ||
| 23 | To use the Matrix integration, you need to install the following dependencies: | |
| 24 | ||
| 25 | ```bash | |
| 26 | pip install matrix-nio[e2e] # For Matrix protocol support with end-to-end encryption | |
| 27 | pip install ollama # For Ollama API access | |
| 28 | pip install pyyaml # For configuration handling | |
| 29 | ``` | |
| 30 | ||
| 31 | ## Getting Started | |
| 32 | ||
| 33 | ### 1. Import the Matrix Tools | |
| 34 | ||
| 35 | For the basic Matrix client: | |
| 36 | ||
| 37 | ```python | |
| 38 | from ollamatools.tools.matrix_tools import ( | |
| 39 | MatrixClient, | |
| 40 | connect_to_matrix, | |
| 41 | join_matrix_room, | |
| 42 | send_matrix_message, | |
| 43 | listen_for_matrix_messages | |
| 44 | ) | |
| 45 | ``` | |
| 46 | ||
| 47 | For the enhanced Matrix client: | |
| 48 | ||
| 49 | ```python | |
| 50 | from ollamatools.tools.enhanced_matrix_tools import ( | |
| 51 | EnhancedMatrixClient, | |
| 52 | connect_enhanced_matrix, | |
| 53 | join_enhanced_matrix_room, | |
| 54 | send_enhanced_matrix_message, | |
| 55 | listen_for_enhanced_matrix_messages, | |
| 56 | create_enhanced_matrix_config, | |
| 57 | disconnect_enhanced_matrix, | |
| 58 | process_with_enhanced_ollama, | |
| 59 | MODULAR_AGENT_AVAILABLE, | |
| 60 | OPENSEARCH_AVAILABLE | |
| 61 | ) | |
| 62 | ``` | |
| 63 | ||
| 64 | ### 2. Create a Matrix Client | |
| 65 | ||
| 66 | You can create a Matrix client in two ways: | |
| 67 | ||
| 68 | #### Option 1: Using the MatrixClient class directly | |
| 69 | ||
| 70 | ```python | |
| 71 | from ollamatools.tools.matrix_tools import MatrixClient | |
| 72 | ||
| 73 | client = MatrixClient( | |
| 74 | homeserver="https://matrix.example.org", | |
| 75 | user_id="@user:example.org", | |
| 76 | password="your-password", | |
| 77 | device_id="ollamatools-matrix", # Optional, for E2E encryption | |
| 78 | ollama_host="localhost:11434", # Optional, defaults to localhost:11434 | |
| 79 | ollama_model="llama3:latest", # Optional, defaults to llama3:latest | |
| 80 | ollama_prompt="You are a helpful assistant." # Optional | |
| 81 | ) | |
| 82 | ||
| 83 | # Connect to the Matrix homeserver | |
| 84 | import asyncio | |
| 85 | async def connect(): | |
| 86 | success = await client.connect() | |
| 87 | if success: | |
| 88 | print("Connected to Matrix homeserver") | |
| 89 | else: | |
| 90 | print("Failed to connect to Matrix homeserver") | |
| 91 | ||
| 92 | asyncio.run(connect()) | |
| 93 | ``` | |
| 94 | ||
| 95 | #### Option 2: Using the connect_to_matrix function | |
| 96 | ||
| 97 | ```python | |
| 98 | from ollamatools.tools.matrix_tools import connect_to_matrix | |
| 99 | ||
| 100 | client = connect_to_matrix( | |
| 101 | homeserver="https://matrix.example.org", | |
| 102 | user_id="@user:example.org", | |
| 103 | password="your-password", | |
| 104 | device_id="ollamatools-matrix" # Optional | |
| 105 | ) | |
| 106 | ||
| 107 | if client: | |
| 108 | print("Connected to Matrix homeserver") | |
| 109 | else: | |
| 110 | print("Failed to connect to Matrix homeserver") | |
| 111 | ``` | |
| 112 | ||
| 113 | ### 3. Join a Matrix Room | |
| 114 | ||
| 115 | ```python | |
| 116 | # Using the MatrixClient class | |
| 117 | async def join_room_async(): | |
| 118 | success = await client.join_room("#room:example.org") | |
| 119 | if success: | |
| 120 | print("Joined room") | |
| 121 | else: | |
| 122 | print("Failed to join room") | |
| 123 | ||
| 124 | asyncio.run(join_room_async()) | |
| 125 | ||
| 126 | # Or using the join_matrix_room function | |
| 127 | from ollamatools.tools.matrix_tools import join_matrix_room | |
| 128 | ||
| 129 | success = join_matrix_room(client, "#room:example.org") | |
| 130 | if success: | |
| 131 | print("Joined room") | |
| 132 | else: | |
| 133 | print("Failed to join room") | |
| 134 | ``` | |
| 135 | ||
| 136 | ### 4. Send a Message to a Room | |
| 137 | ||
| 138 | ```python | |
| 139 | # Using the MatrixClient class | |
| 140 | async def send_message_async(): | |
| 141 | success = await client.send_message("#room:example.org", "Hello from OllamaTools!") | |
| 142 | if success: | |
| 143 | print("Message sent") | |
| 144 | else: | |
| 145 | print("Failed to send message") | |
| 146 | ||
| 147 | asyncio.run(send_message_async()) | |
| 148 | ||
| 149 | # Or using the send_matrix_message function | |
| 150 | from ollamatools.tools.matrix_tools import send_matrix_message | |
| 151 | ||
| 152 | success = send_matrix_message(client, "#room:example.org", "Hello from OllamaTools!") | |
| 153 | if success: | |
| 154 | print("Message sent") | |
| 155 | else: | |
| 156 | print("Failed to send message") | |
| 157 | ``` | |
| 158 | ||
| 159 | ### 5. Listen for Messages | |
| 160 | ||
| 161 | By default, the Matrix client will process incoming messages with Ollama and send the responses back to the room. You can start listening for messages like this: | |
| 162 | ||
| 163 | ```python | |
| 164 | # Using the MatrixClient class | |
| 165 | async def listen_async(): | |
| 166 | await client.listen() | |
| 167 | ||
| 168 | asyncio.run(listen_async()) | |
| 169 | ||
| 170 | # Or using the listen_for_matrix_messages function | |
| 171 | from ollamatools.tools.matrix_tools import listen_for_matrix_messages | |
| 172 | ||
| 173 | listen_for_matrix_messages(client) | |
| 174 | ``` | |
| 175 | ||
| 176 | ### 6. Custom Message Handling | |
| 177 | ||
| 178 | You can also provide a custom message handler to process messages in your own way: | |
| 179 | ||
| 180 | ```python | |
| 181 | async def custom_message_handler(room, event, client): | |
| 182 | message = event.body | |
| 183 | room_id = room.room_id | |
| 184 | sender = event.sender | |
| 185 | ||
| 186 | print(f"Received message in room {room_id} from {sender}: {message}") | |
| 187 | ||
| 188 | # Process the message with Ollama | |
| 189 | response = await client.process_with_ollama(message) | |
| 190 | ||
| 191 | # Add a prefix to the response | |
| 192 | response = f"Response from Ollama: {response}" | |
| 193 | ||
| 194 | # Send the response back to the room | |
| 195 | await client.send_message(room_id, response) | |
| 196 | ||
| 197 | # Set the custom message handler | |
| 198 | client.set_message_callback(custom_message_handler) | |
| 199 | ||
| 200 | # Start listening for messages | |
| 201 | asyncio.run(client.listen()) | |
| 202 | ``` | |
| 203 | ||
| 204 | ## Configuration | |
| 205 | ||
| 206 | You can use the MatrixConfig class to load and save configuration from YAML files: | |
| 207 | ||
| 208 | ```python | |
| 209 | from ollamatools.tools.matrix_tools import ( | |
| 210 | MatrixConfig, | |
| 211 | load_matrix_config, | |
| 212 | save_matrix_config, | |
| 213 | create_default_matrix_config | |
| 214 | ) | |
| 215 | ||
| 216 | # Create a default configuration | |
| 217 | config = create_default_matrix_config() | |
| 218 | ||
| 219 | # Save the configuration to a file | |
| 220 | save_matrix_config(config, "config.yaml") | |
| 221 | ||
| 222 | # Load the configuration from a file | |
| 223 | config = load_matrix_config("config.yaml") | |
| 224 | ``` | |
| 225 | ||
| 226 | A typical configuration file looks like this: | |
| 227 | ||
| 228 | ```yaml | |
| 229 | MATRIX: | |
| 230 | HOMESERVER: "https://matrix.example.org" | |
| 231 | USER: "@user:example.org" | |
| 232 | PASSWORD: "your-password" | |
| 233 | DEVICE_ID: "ollamatools-matrix" | |
| 234 | ROOMS: | |
| 235 | - "#room:example.org" | |
| 236 | - "#another-room:example.org" | |
| 237 | ||
| 238 | OLLAMA: | |
| 239 | HOST: "localhost:11434" | |
| 240 | MODEL: "llama3:latest" | |
| 241 | PROMPT: "You are a helpful assistant." | |
| 242 | ``` | |
| 243 | ||
| 244 | ## Example Scripts | |
| 245 | ||
| 246 | ### Basic Matrix Client | |
| 247 | ||
| 248 | See the `examples/matrix_client_example.py` script for a complete example of how to use the basic Matrix integration. | |
| 249 | ||
| 250 | To run the example: | |
| 251 | ||
| 252 | 1. Create a configuration file: | |
| 253 | ||
| 254 | ```bash | |
| 255 | python examples/matrix_client_example.py --create-config | |
| 256 | ``` | |
| 257 | ||
| 258 | 2. Edit the configuration file at `examples/config/matrix_config.yaml` with your Matrix credentials and Ollama settings. | |
| 259 | ||
| 260 | 3. Run the example: | |
| 261 | ||
| 262 | ```bash | |
| 263 | python examples/matrix_client_example.py | |
| 264 | ``` | |
| 265 | ||
| 266 | ### Enhanced Matrix Client | |
| 267 | ||
| 268 | See the `examples/enhanced_matrix_client_example.py` script for a complete example of how to use the enhanced Matrix integration with advanced features. | |
| 269 | ||
| 270 | To run the example: | |
| 271 | ||
| 272 | 1. Create a configuration file: | |
| 273 | ||
| 274 | ```bash | |
| 275 | python examples/enhanced_matrix_client_example.py --create-config | |
| 276 | ``` | |
| 277 | ||
| 278 | 2. Edit the configuration file at `examples/config/enhanced_matrix_config.yaml` with your Matrix credentials, Ollama settings, and enhanced options. | |
| 279 | ||
| 280 | 3. Run the example: | |
| 281 | ||
| 282 | ```bash | |
| 283 | python examples/enhanced_matrix_client_example.py | |
| 284 | ``` | |
| 285 | ||
| 286 | ## Advanced Usage | |
| 287 | ||
| 288 | ### Basic Matrix Client Features | |
| 289 | ||
| 290 | #### Model Selection | |
| 291 | ||
| 292 | The Matrix client supports switching between different Ollama models. Users can change the model they're using with the `/model` command: | |
| 293 | ||
| 294 | ``` | |
| 295 | /model llama3:latest | |
| 296 | ``` | |
| 297 | ||
| 298 | To enable model selection: | |
| 299 | ||
| 300 | 1. Specify available models in your configuration: | |
| 301 | ||
| 302 | ```yaml | |
| 303 | OLLAMA: | |
| 304 | AVAILABLE_MODELS: | |
| 305 | - "llama3:latest" | |
| 306 | - "mistral:latest" | |
| 307 | - "gemma:latest" | |
| 308 | ``` | |
| 309 | ||
| 310 | 2. Or provide them when creating the client: | |
| 311 | ||
| 312 | ```python | |
| 313 | client = MatrixClient( | |
| 314 | # ... other parameters ... | |
| 315 | available_models=["llama3:latest", "mistral:latest", "gemma:latest"] | |
| 316 | ) | |
| 317 | ``` | |
| 318 | ||
| 319 | 3. Users can list available models with the `/models` command and get help with the `/help` command. | |
| 320 | ||
| 321 | 4. You can also update available models from the Ollama server: | |
| 322 | ||
| 323 | ```python | |
| 324 | await client.update_available_models() | |
| 325 | ``` | |
| 326 | ||
| 327 | #### User Context Tracking | |
| 328 | ||
| 329 | The Matrix client maintains conversation context for each user in each room. This allows for more coherent conversations as the model can remember previous messages. | |
| 330 | ||
| 331 | Context tracking is enabled by default and works automatically when you pass the sender and room_id to the `process_with_ollama` method: | |
| 332 | ||
| 333 | ```python | |
| 334 | response = await client.process_with_ollama(message, sender, room_id) | |
| 335 | ``` | |
| 336 | ||
| 337 | You can control the maximum context length by setting: | |
| 338 | ||
| 339 | ```python | |
| 340 | client.max_context_length = 15 # Default is 10 | |
| 341 | ``` | |
| 342 | ||
| 343 | #### Admin Users | |
| 344 | ||
| 345 | You can designate certain Matrix users as admins, giving them special privileges: | |
| 346 | ||
| 347 | ```python | |
| 348 | client = MatrixClient( | |
| 349 | # ... other parameters ... | |
| 350 | admin_users=["@admin:example.org", "@moderator:example.org"] | |
| 351 | ) | |
| 352 | ``` | |
| 353 | ||
| 354 | Or add them later: | |
| 355 | ||
| 356 | ```python | |
| 357 | client.add_admin_user("@admin:example.org") | |
| 358 | client.remove_admin_user("@admin:example.org") | |
| 359 | ``` | |
| 360 | ||
| 361 | ### Enhanced Matrix Client Features | |
| 362 | ||
| 363 | The enhanced Matrix client includes all the features of the basic client, plus the following advanced features: | |
| 364 | ||
| 365 | #### Modular Structure | |
| 366 | ||
| 367 | The enhanced Matrix client is organized into multiple modules for better maintainability and extensibility: | |
| 368 | ||
| 369 | 1. **enhanced_matrix_core.py**: Contains the core `EnhancedMatrixClient` class and basic functionality | |
| 370 | 2. **enhanced_matrix_search.py**: Contains search-related functionality | |
| 371 | 3. **enhanced_matrix_history.py**: Contains conversation history tracking and analytics | |
| 372 | 4. **enhanced_matrix_tools.py**: Main module that imports and re-exports everything | |
| 373 | ||
| 374 | This modular structure makes it easier to: | |
| 375 | - Understand and maintain the codebase | |
| 376 | - Extend specific functionality without affecting other parts | |
| 377 | - Import only the components you need | |
| 378 | - Avoid circular dependencies | |
| 379 | ||
| 380 | You can import from the main module to get all functionality: | |
| 381 | ||
| 382 | ```python | |
| 383 | from ollamatools.tools.enhanced_matrix_tools import ( | |
| 384 | EnhancedMatrixClient, | |
| 385 | connect_enhanced_matrix, | |
| 386 | # ... other imports | |
| 387 | ) | |
| 388 | ``` | |
| 389 | ||
| 390 | Or import directly from specific modules if you only need certain components: | |
| 391 | ||
| 392 | ```python | |
| 393 | from ollamatools.tools.enhanced_matrix_core import EnhancedMatrixClient | |
| 394 | from ollamatools.tools.enhanced_matrix_search import execute_search_query | |
| 395 | from ollamatools.tools.enhanced_matrix_history import analyze_conversation_topics | |
| 396 | ``` | |
| 397 | ||
| 398 | #### Improved Message Response Logic | |
| 399 | ||
| 400 | The enhanced client has improved message detection logic: | |
| 401 | ||
| 402 | - Case-insensitive username detection | |
| 403 | - Direct mention detection (e.g., @username:server.com) | |
| 404 | - Option to respond to all messages (not just mentions) | |
| 405 | ||
| 406 | ```python | |
| 407 | client = EnhancedMatrixClient( | |
| 408 | # ... other parameters ... | |
| 409 | respond_to_all_messages=True # Respond to all messages, not just mentions | |
| 410 | ) | |
| 411 | ``` | |
| 412 | ||
| 413 | #### Search Integration | |
| 414 | ||
| 415 | The enhanced client can search the web for information using OpenSearchNG: | |
| 416 | ||
| 417 | ```python | |
| 418 | client = EnhancedMatrixClient( | |
| 419 | # ... other parameters ... | |
| 420 | enable_search=True, | |
| 421 | opensearch_config={ | |
| 422 | "BASE_URL": "http://localhost:8081", | |
| 423 | "ENGINES": ["google", "bing", "duckduckgo", "brave"], | |
| 424 | "NUM_RESULTS": 5 | |
| 425 | } | |
| 426 | ) | |
| 427 | ``` | |
| 428 | ||
| 429 | Users can search directly with the `/search` command: | |
| 430 | ||
| 431 | ``` | |
| 432 | /search what is the capital of France? | |
| 433 | ``` | |
| 434 | ||
| 435 | The client can also automatically detect when a message might benefit from search and augment the message with search results. | |
| 436 | ||
| 437 | #### Conversation History Tracking | |
| 438 | ||
| 439 | The enhanced client can track conversation history and provide analytics: | |
| 440 | ||
| 441 | ```python | |
| 442 | client = EnhancedMatrixClient( | |
| 443 | # ... other parameters ... | |
| 444 | enable_conversation_tracking=True, | |
| 445 | max_conversation_history=100 | |
| 446 | ) | |
| 447 | ``` | |
| 448 | ||
| 449 | Users can access conversation history with the following commands: | |
| 450 | ||
| 451 | - `/history [count]` - Show recent conversation history | |
| 452 | - `/topics` - Show conversation topics | |
| 453 | - `/metrics` - Show performance metrics | |
| 454 | ||
| 455 | #### Configuration | |
| 456 | ||
| 457 | You can configure the enhanced client using the configuration file: | |
| 458 | ||
| 459 | ```yaml | |
| 460 | ENHANCED: | |
| 461 | # Enable search functionality (requires OpenSearchNG) | |
| 462 | ENABLE_SEARCH: true | |
| 463 | ||
| 464 | # Enable conversation tracking and history | |
| 465 | ENABLE_CONVERSATION_TRACKING: true | |
| 466 | ||
| 467 | # Maximum number of messages to keep in context for each user | |
| 468 | MAX_CONTEXT_LENGTH: 10 | |
| 469 | ||
| 470 | # Maximum number of conversations to keep in history | |
| 471 | MAX_CONVERSATION_HISTORY: 100 | |
| 472 | ||
| 473 | # Whether to respond to all messages (not just mentions) | |
| 474 | RESPOND_TO_ALL_MESSAGES: false | |
| 475 | ||
| 476 | # OpenSearchNG configuration | |
| 477 | OPENSEARCH: | |
| 478 | BASE_URL: "http://localhost:8081" | |
| 479 | ENGINES: | |
| 480 | - "google" | |
| 481 | - "bing" | |
| 482 | - "duckduckgo" | |
| 483 | - "brave" | |
| 484 | NUM_RESULTS: 5 | |
| 485 | ``` | |
| 486 | ||
| 487 | ### End-to-End Encryption | |
| 488 | ||
| 489 | The Matrix integration supports end-to-end encryption through the matrix-nio[e2e] package. To use E2E encryption, you need to: | |
| 490 | ||
| 491 | 1. Install the E2E dependencies: | |
| 492 | ||
| 493 | ```bash | |
| 494 | pip install matrix-nio[e2e] | |
| 495 | ``` | |
| 496 | ||
| 497 | 2. Provide a device ID when creating the Matrix client: | |
| 498 | ||
| 499 | ```python | |
| 500 | client = MatrixClient( | |
| 501 | homeserver="https://matrix.example.org", | |
| 502 | user_id="@user:example.org", | |
| 503 | password="your-password", | |
| 504 | device_id="ollamatools-matrix" # Required for E2E encryption | |
| 505 | ) | |
| 506 | ``` | |
| 507 | ||
| 508 | ### Streaming Responses | |
| 509 | ||
| 510 | The Matrix client currently doesn't support streaming responses from Ollama, but this feature may be added in the future. | |
| 511 | ||
| 512 | ## Troubleshooting | |
| 513 | ||
| 514 | ### Connection Issues | |
| 515 | ||
| 516 | If you're having trouble connecting to the Matrix homeserver, check: | |
| 517 | ||
| 518 | - The homeserver URL is correct and includes the protocol (https://) | |
| 519 | - Your user ID and password are correct | |
| 520 | - The Matrix server is reachable from your network | |
| 521 | ||
| 522 | ### Message Processing Issues | |
| 523 | ||
| 524 | If messages aren't being processed correctly: | |
| 525 | ||
| 526 | - Check that Ollama is running and accessible at the configured host | |
| 527 | - Verify that the specified model is available in your Ollama installation | |
| 528 | - Enable debug logging to see more detailed information: | |
| 529 | ||
| 530 | ```python | |
| 531 | import logging | |
| 532 | logging.basicConfig(level=logging.DEBUG) | |
| 533 | ``` | |
| 534 | ||
| 535 | ## API Reference | |
| 536 | ||
| 537 | For detailed API documentation, see the docstrings in the following files: | |
| 538 | ||
| 539 | - **Basic Matrix Client**: `ollamatools/tools/matrix_tools.py` | |
| 540 | - **Enhanced Matrix Client**: | |
| 541 | - Main module: `ollamatools/tools/enhanced_matrix_tools.py` | |
| 542 | - Core functionality: `ollamatools/tools/enhanced_matrix_core.py` | |
| 543 | - Search functionality: `ollamatools/tools/enhanced_matrix_search.py` | |
| 544 | - History tracking: `ollamatools/tools/enhanced_matrix_history.py` | |
| 545 | ||
| 546 | Each module contains comprehensive docstrings for all classes, methods, and functions. |
| … | … | @@ -0,0 +1,50 @@ |
| 1 | MATRIX: | |
| 2 | HOMESERVER: "https://prsm.space" | |
| 3 | USER: "@chat7rn:prsm.space" | |
| 4 | PASSWORD: "chat7rn" | |
| 5 | DEVICE_ID: "ollamatools-matrix" | |
| 6 | ROOMS: | |
| 7 | - "#general:prsm.space" | |
| 8 | - "#ai_bot_testing:prsm.space" | |
| 9 | ADMIN_USERS: | |
| 10 | - "@chat7rn:prsm.space" | |
| 11 | ||
| 12 | OLLAMA: | |
| 13 | HOST: "localhost:11434" | |
| 14 | MODEL: "llama3.1:latest" | |
| 15 | PROMPT: "You are a helpful assistant in a Matrix chat room. Be concise and friendly in your responses." | |
| 16 | AVAILABLE_MODELS: | |
| 17 | - "deepseek-r1:7b" | |
| 18 | - "llama3.2:1b" | |
| 19 | - "llama3.1:latest" | |
| 20 | ||
| 21 | ENHANCED: | |
| 22 | # Enable search functionality | |
| 23 | # Note: Requires both OpenSearchNG and modular agent components to be installed | |
| 24 | # If these components are not available, search will be automatically disabled | |
| 25 | ENABLE_SEARCH: false | |
| 26 | ||
| 27 | # Enable conversation tracking and history | |
| 28 | # This feature works without modular agent components | |
| 29 | ENABLE_CONVERSATION_TRACKING: true | |
| 30 | ||
| 31 | # Maximum number of messages to keep in context for each user | |
| 32 | MAX_CONTEXT_LENGTH: 10 | |
| 33 | ||
| 34 | # Maximum number of conversations to keep in history | |
| 35 | MAX_CONVERSATION_HISTORY: 100 | |
| 36 | ||
| 37 | # Whether to respond to all messages (not just mentions) | |
| 38 | # This feature works without modular agent components | |
| 39 | RESPOND_TO_ALL_MESSAGES: false | |
| 40 | ||
| 41 | # OpenSearchNG configuration | |
| 42 | # Note: Only used if search is enabled and OpenSearchNG is available | |
| 43 | OPENSEARCH: | |
| 44 | BASE_URL: "http://localhost:8081" | |
| 45 | ENGINES: | |
| 46 | - "google" | |
| 47 | - "bing" | |
| 48 | - "duckduckgo" | |
| 49 | - "brave" | |
| 50 | NUM_RESULTS: 5 |
| … | … | @@ -0,0 +1,11 @@ |
| 1 | MATRIX: | |
| 2 | HOMESERVER: "https://prsm.space" | |
| 3 | USER: "@chat7rn:prsm.space" | |
| 4 | PASSWORD: "chat7rn" | |
| 5 | DEVICE_ID: "ollamatools-matrix" | |
| 6 | ROOMS: | |
| 7 | - "#general:prsm.space" | |
| 8 | OLLAMA: | |
| 9 | HOST: "localhost:11434" | |
| 10 | MODEL: "llama3.1:latest" | |
| 11 | PROMPT: "You are a helpful assistant in a Matrix chat room. Be concise and friendly in your responses." |
| … | … | @@ -0,0 +1,330 @@ |
| 1 | #!/usr/bin/env python3 | |
| 2 | """ | |
| 3 | Enhanced Matrix Client Example for OllamaTools | |
| 4 | ||
| 5 | This example demonstrates how to use the enhanced Matrix integration in OllamaTools | |
| 6 | to connect to a Matrix homeserver, join rooms, and process messages with Ollama. | |
| 7 | It showcases advanced features like improved message response logic, search integration, | |
| 8 | conversation tracking, and more. | |
| 9 | ||
| 10 | Requirements: | |
| 11 | - matrix-nio: `pip install matrix-nio[e2e]` | |
| 12 | - ollama: `pip install ollama` | |
| 13 | - pyyaml: `pip install pyyaml` | |
| 14 | ||
| 15 | Optional Dependencies (for advanced features): | |
| 16 | - ollamatools.agent.modular: For search integration and natural language understanding | |
| 17 | - ollamatools.opensearch_web_tools: For web search functionality | |
| 18 | ||
| 19 | Note: If the modular agent components are not available, some advanced features | |
| 20 | like search integration will be automatically disabled, but the client will still | |
| 21 | function with basic features like improved message response logic and conversation tracking. | |
| 22 | ||
| 23 | Usage: | |
| 24 | 1. Edit the configuration below or create a config.yaml file | |
| 25 | 2. Run the script: `python enhanced_matrix_client_example.py` | |
| 26 | """ | |
| 27 | ||
| 28 | import os | |
| 29 | import sys | |
| 30 | import yaml | |
| 31 | import logging | |
| 32 | import asyncio | |
| 33 | import argparse | |
| 34 | import random | |
| 35 | from typing import Dict, List, Optional | |
| 36 | ||
| 37 | # Add the parent directory to the path so we can import ollamatools | |
| 38 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) | |
| 39 | ||
| 40 | from ollamatools.tools.enhanced_matrix_tools import ( | |
| 41 | EnhancedMatrixClient, | |
| 42 | connect_enhanced_matrix, | |
| 43 | create_enhanced_matrix_config, | |
| 44 | load_matrix_config, | |
| 45 | save_matrix_config, | |
| 46 | disconnect_enhanced_matrix, | |
| 47 | MODULAR_AGENT_AVAILABLE, | |
| 48 | OPENSEARCH_AVAILABLE | |
| 49 | ) | |
| 50 | ||
| 51 | # Configure logging | |
| 52 | logging.basicConfig( | |
| 53 | level=logging.INFO, | |
| 54 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' | |
| 55 | ) | |
| 56 | logger = logging.getLogger(__name__) | |
| 57 | ||
| 58 | # Default configuration file path | |
| 59 | DEFAULT_CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'config', 'enhanced_matrix_config.yaml') | |
| 60 | ||
| 61 | def parse_arguments(): | |
| 62 | """Parse command line arguments.""" | |
| 63 | parser = argparse.ArgumentParser(description='Enhanced Matrix client example for OllamaTools') | |
| 64 | parser.add_argument( | |
| 65 | '-c', '--config', | |
| 66 | help='Path to the configuration file', | |
| 67 | default=DEFAULT_CONFIG_PATH | |
| 68 | ) | |
| 69 | parser.add_argument( | |
| 70 | '--create-config', | |
| 71 | help='Create a default configuration file', | |
| 72 | action='store_true' | |
| 73 | ) | |
| 74 | parser.add_argument( | |
| 75 | '-d', '--debug', | |
| 76 | help='Enable debug logging', | |
| 77 | action='store_true' | |
| 78 | ) | |
| 79 | parser.add_argument( | |
| 80 | '--update-models', | |
| 81 | help='Update available models from Ollama server', | |
| 82 | action='store_true' | |
| 83 | ) | |
| 84 | return parser.parse_args() | |
| 85 | ||
| 86 | def create_config_file(config_path: str) -> None: | |
| 87 | """Create a default configuration file.""" | |
| 88 | config = create_enhanced_matrix_config() | |
| 89 | ||
| 90 | # Create the directory if it doesn't exist | |
| 91 | os.makedirs(os.path.dirname(config_path), exist_ok=True) | |
| 92 | ||
| 93 | # Save the configuration | |
| 94 | save_matrix_config(config, config_path) | |
| 95 | ||
| 96 | logger.info(f"Created default configuration file at {config_path}") | |
| 97 | logger.info("Please edit this file with your Matrix credentials and Ollama settings.") | |
| 98 | ||
| 99 | sys.exit(0) | |
| 100 | ||
| 101 | def load_config(config_path: str) -> Dict: | |
| 102 | """Load the configuration from a file.""" | |
| 103 | try: | |
| 104 | config = load_matrix_config(config_path) | |
| 105 | return config | |
| 106 | except FileNotFoundError: | |
| 107 | logger.error(f"Configuration file not found: {config_path}") | |
| 108 | logger.info("You can create a default configuration file with --create-config") | |
| 109 | sys.exit(1) | |
| 110 | except Exception as e: | |
| 111 | logger.error(f"Error loading configuration: {e}") | |
| 112 | sys.exit(1) | |
| 113 | ||
| 114 | async def custom_message_handler(room, event, client): | |
| 115 | """ | |
| 116 | Custom message handler for Matrix events. | |
| 117 | ||
| 118 | This is an example of how to create a custom handler for Matrix messages. | |
| 119 | It processes the message with Ollama and sends the response back to the room. | |
| 120 | ||
| 121 | Args: | |
| 122 | room: The Matrix room where the message was received | |
| 123 | event: The Matrix event (message) | |
| 124 | client: The MatrixClient instance | |
| 125 | """ | |
| 126 | message = event.body | |
| 127 | room_id = room.room_id | |
| 128 | sender = event.sender | |
| 129 | ||
| 130 | logger.info(f"Custom handler received message in room {room_id} from {sender}: {message}") | |
| 131 | ||
| 132 | # Process the message with Ollama, passing sender and room_id for context tracking | |
| 133 | response = await client.process_with_ollama(message, sender, room_id) | |
| 134 | ||
| 135 | # Add a prefix to the response | |
| 136 | response = f"Response from Ollama: {response}" | |
| 137 | ||
| 138 | # Send the response back to the room | |
| 139 | await client.send_message(room_id, response) | |
| 140 | ||
| 141 | def get_random_greeting() -> str: | |
| 142 | """Generate a random greeting message.""" | |
| 143 | greetings = [ | |
| 144 | "Hello everyone! I'm here to assist with your questions.", | |
| 145 | "Greetings! I'm your friendly Matrix bot powered by Ollama.", | |
| 146 | "Hi there! I'm online and ready to chat.", | |
| 147 | "Good day! I'm here to help with anything you need.", | |
| 148 | "Hey folks! I've just connected and I'm ready to assist.", | |
| 149 | "Hello! I'm your AI assistant, ready to help with your questions.", | |
| 150 | "Greetings, humans! AI assistant at your service.", | |
| 151 | "Hi everyone! I'm here and ready to chat.", | |
| 152 | "Hello Matrix world! How can I assist you today?", | |
| 153 | "Greetings! Your friendly neighborhood AI has arrived." | |
| 154 | ] | |
| 155 | return random.choice(greetings) | |
| 156 | ||
| 157 | async def main_async(config: Dict, update_models: bool = False) -> None: | |
| 158 | """ | |
| 159 | Main async function to run the Enhanced Matrix client. | |
| 160 | ||
| 161 | Args: | |
| 162 | config: The configuration dictionary | |
| 163 | update_models: Whether to update available models from Ollama server | |
| 164 | """ | |
| 165 | # Create a client variable in the outer scope so we can access it in the finally block | |
| 166 | client = None | |
| 167 | # Extract configuration | |
| 168 | matrix_config = config.get('MATRIX', {}) | |
| 169 | ollama_config = config.get('OLLAMA', {}) | |
| 170 | enhanced_config = config.get('ENHANCED', {}) | |
| 171 | ||
| 172 | homeserver = matrix_config.get('HOMESERVER') | |
| 173 | user_id = matrix_config.get('USER') | |
| 174 | password = matrix_config.get('PASSWORD') | |
| 175 | device_id = matrix_config.get('DEVICE_ID') | |
| 176 | rooms = matrix_config.get('ROOMS', []) | |
| 177 | admin_users = matrix_config.get('ADMIN_USERS', []) | |
| 178 | ||
| 179 | ollama_host = ollama_config.get('HOST', 'localhost:11434') | |
| 180 | ollama_model = ollama_config.get('MODEL', 'llama3:latest') | |
| 181 | ollama_prompt = ollama_config.get('PROMPT') | |
| 182 | available_models = ollama_config.get('AVAILABLE_MODELS', [ollama_model]) | |
| 183 | ||
| 184 | # Extract enhanced configuration | |
| 185 | enable_search = enhanced_config.get('ENABLE_SEARCH', True) | |
| 186 | enable_conversation_tracking = enhanced_config.get('ENABLE_CONVERSATION_TRACKING', True) | |
| 187 | max_context_length = enhanced_config.get('MAX_CONTEXT_LENGTH', 10) | |
| 188 | max_conversation_history = enhanced_config.get('MAX_CONVERSATION_HISTORY', 100) | |
| 189 | respond_to_all_messages = enhanced_config.get('RESPOND_TO_ALL_MESSAGES', False) | |
| 190 | opensearch_config = enhanced_config.get('OPENSEARCH', None) | |
| 191 | ||
| 192 | try: | |
| 193 | # Create and connect the Enhanced Matrix client | |
| 194 | client = EnhancedMatrixClient( | |
| 195 | homeserver=homeserver, | |
| 196 | user_id=user_id, | |
| 197 | password=password, | |
| 198 | device_id=device_id, | |
| 199 | ollama_host=ollama_host, | |
| 200 | ollama_model=ollama_model, | |
| 201 | ollama_prompt=ollama_prompt, | |
| 202 | available_models=available_models, | |
| 203 | admin_users=admin_users, | |
| 204 | opensearch_config=opensearch_config, | |
| 205 | enable_search=enable_search, | |
| 206 | enable_conversation_tracking=enable_conversation_tracking, | |
| 207 | max_context_length=max_context_length, | |
| 208 | max_conversation_history=max_conversation_history, | |
| 209 | respond_to_all_messages=respond_to_all_messages | |
| 210 | ) | |
| 211 | ||
| 212 | # Connect to the Matrix homeserver | |
| 213 | success = await client.connect() | |
| 214 | ||
| 215 | if not success: | |
| 216 | logger.error("Failed to connect to Matrix homeserver") | |
| 217 | return | |
| 218 | ||
| 219 | # Update available models if requested | |
| 220 | if update_models: | |
| 221 | logger.info("Updating available models from Ollama server...") | |
| 222 | await client.update_available_models() | |
| 223 | logger.info(f"Available models: {', '.join(client.available_models)}") | |
| 224 | ||
| 225 | # Join rooms and track successful joins | |
| 226 | successful_joins = [] | |
| 227 | for room in rooms: | |
| 228 | if isinstance(room, dict) and 'ROOM' in room: | |
| 229 | room_id = room['ROOM'] | |
| 230 | else: | |
| 231 | room_id = room | |
| 232 | ||
| 233 | join_success = await client.join_room(room_id) | |
| 234 | if join_success: | |
| 235 | successful_joins.append(room_id) | |
| 236 | logger.info(f"Successfully joined room {room_id}") | |
| 237 | ||
| 238 | # Send a single greeting to the first successfully joined room | |
| 239 | if successful_joins: | |
| 240 | first_room = successful_joins[0] | |
| 241 | greeting = get_random_greeting() | |
| 242 | await client.send_message(first_room, greeting) | |
| 243 | logger.info(f"Sent greeting to room {first_room}: {greeting}") | |
| 244 | ||
| 245 | # Optional: Set a custom message handler | |
| 246 | # client.set_message_callback(custom_message_handler) | |
| 247 | ||
| 248 | # Start listening for messages | |
| 249 | logger.info("Starting to listen for messages...") | |
| 250 | logger.info("The bot will respond to the following commands:") | |
| 251 | logger.info(" /help - Show help message") | |
| 252 | logger.info(" /models - List available models") | |
| 253 | logger.info(" /model <model_name> - Change to a different model") | |
| 254 | ||
| 255 | # Log enhanced commands if enabled | |
| 256 | if enable_search: | |
| 257 | if MODULAR_AGENT_AVAILABLE and OPENSEARCH_AVAILABLE: | |
| 258 | logger.info(" /search <query> - Search the web for information") | |
| 259 | else: | |
| 260 | logger.warning("Search functionality is disabled because required components are not available") | |
| 261 | logger.info("To enable search functionality, make sure the modular agent components and OpenSearchNG are available") | |
| 262 | ||
| 263 | if enable_conversation_tracking: | |
| 264 | logger.info(" /history [count] - Show recent conversation history") | |
| 265 | logger.info(" /topics - Show conversation topics") | |
| 266 | logger.info(" /metrics - Show performance metrics") | |
| 267 | ||
| 268 | # Extract username for logging | |
| 269 | username = user_id.split(':')[0].lstrip('@') | |
| 270 | logger.info(f"The bot will also respond when mentioned by username: {username}") | |
| 271 | ||
| 272 | if respond_to_all_messages: | |
| 273 | logger.info("The bot will respond to all messages (not just mentions)") | |
| 274 | ||
| 275 | # Log availability of advanced features | |
| 276 | if not MODULAR_AGENT_AVAILABLE: | |
| 277 | if enhanced_config.get('ENABLE_SEARCH', True): | |
| 278 | logger.warning("Modular agent components are not available. Some advanced features will be disabled.") | |
| 279 | else: | |
| 280 | logger.info("Modular agent components are not available, but search is disabled in config.") | |
| 281 | logger.info("Basic features like improved message response logic and conversation tracking will still work.") | |
| 282 | ||
| 283 | if not OPENSEARCH_AVAILABLE: | |
| 284 | logger.warning("OpenSearchNG is not available. Search functionality will be disabled.") | |
| 285 | ||
| 286 | # Print instructions for stopping the client | |
| 287 | logger.info("Press Ctrl+C to stop the client") | |
| 288 | ||
| 289 | # Start listening for messages | |
| 290 | await client.listen() | |
| 291 | except KeyboardInterrupt: | |
| 292 | logger.info("Interrupted by user") | |
| 293 | except Exception as e: | |
| 294 | logger.error(f"Error in main_async: {e}") | |
| 295 | finally: | |
| 296 | # Clean up resources | |
| 297 | if client: | |
| 298 | logger.info("Disconnecting from Matrix homeserver...") | |
| 299 | try: | |
| 300 | await client.disconnect() | |
| 301 | logger.info("Disconnected from Matrix homeserver") | |
| 302 | except Exception as e: | |
| 303 | logger.error(f"Error disconnecting from Matrix homeserver: {e}") | |
| 304 | ||
| 305 | def main(): | |
| 306 | """Main function to run the Matrix client.""" | |
| 307 | args = parse_arguments() | |
| 308 | ||
| 309 | # Set log level | |
| 310 | if args.debug: | |
| 311 | logging.getLogger().setLevel(logging.DEBUG) | |
| 312 | ||
| 313 | # Create a default configuration file if requested | |
| 314 | if args.create_config: | |
| 315 | create_config_file(args.config) | |
| 316 | ||
| 317 | # Load the configuration | |
| 318 | config = load_config(args.config) | |
| 319 | ||
| 320 | # Run the async main function | |
| 321 | try: | |
| 322 | # Use asyncio.run to handle the event loop properly | |
| 323 | asyncio.run(main_async(config, args.update_models)) | |
| 324 | except KeyboardInterrupt: | |
| 325 | logger.info("Interrupted by user") | |
| 326 | except Exception as e: | |
| 327 | logger.error(f"Error: {e}") | |
| 328 | ||
| 329 | if __name__ == '__main__': | |
| 330 | main() |
| … | … | @@ -0,0 +1,227 @@ |
| 1 | #!/usr/bin/env python3 | |
| 2 | """ | |
| 3 | Matrix Client Example for OllamaTools | |
| 4 | ||
| 5 | This example demonstrates how to use the Matrix integration in OllamaTools | |
| 6 | to connect to a Matrix homeserver, join rooms, and process messages with Ollama. | |
| 7 | ||
| 8 | Requirements: | |
| 9 | - matrix-nio: `pip install matrix-nio[e2e]` | |
| 10 | - ollama: `pip install ollama` | |
| 11 | - pyyaml: `pip install pyyaml` | |
| 12 | ||
| 13 | Usage: | |
| 14 | 1. Edit the configuration below or create a config.yaml file | |
| 15 | 2. Run the script: `python matrix_client_example.py` | |
| 16 | """ | |
| 17 | ||
| 18 | import os | |
| 19 | import sys | |
| 20 | import yaml | |
| 21 | import logging | |
| 22 | import asyncio | |
| 23 | import argparse | |
| 24 | import random | |
| 25 | from typing import Dict, Optional | |
| 26 | ||
| 27 | # Add the parent directory to the path so we can import ollamatools | |
| 28 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) | |
| 29 | ||
| 30 | from ollamatools.tools.matrix_tools import ( | |
| 31 | MatrixClient, | |
| 32 | MatrixConfig, | |
| 33 | connect_to_matrix, | |
| 34 | join_matrix_room, | |
| 35 | send_matrix_message, | |
| 36 | listen_for_matrix_messages, | |
| 37 | load_matrix_config, | |
| 38 | save_matrix_config, | |
| 39 | create_default_matrix_config | |
| 40 | ) | |
| 41 | ||
| 42 | # Configure logging | |
| 43 | logging.basicConfig( | |
| 44 | level=logging.INFO, | |
| 45 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' | |
| 46 | ) | |
| 47 | logger = logging.getLogger(__name__) | |
| 48 | ||
| 49 | # Default configuration file path | |
| 50 | DEFAULT_CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'config', 'matrix_config.yaml') | |
| 51 | ||
| 52 | def parse_arguments(): | |
| 53 | """Parse command line arguments.""" | |
| 54 | parser = argparse.ArgumentParser(description='Matrix client example for OllamaTools') | |
| 55 | parser.add_argument( | |
| 56 | '-c', '--config', | |
| 57 | help='Path to the configuration file', | |
| 58 | default=DEFAULT_CONFIG_PATH | |
| 59 | ) | |
| 60 | parser.add_argument( | |
| 61 | '--create-config', | |
| 62 | help='Create a default configuration file', | |
| 63 | action='store_true' | |
| 64 | ) | |
| 65 | parser.add_argument( | |
| 66 | '-d', '--debug', | |
| 67 | help='Enable debug logging', | |
| 68 | action='store_true' | |
| 69 | ) | |
| 70 | return parser.parse_args() | |
| 71 | ||
| 72 | def create_config_file(config_path: str) -> None: | |
| 73 | """Create a default configuration file.""" | |
| 74 | config = create_default_matrix_config() | |
| 75 | ||
| 76 | # Create the directory if it doesn't exist | |
| 77 | os.makedirs(os.path.dirname(config_path), exist_ok=True) | |
| 78 | ||
| 79 | # Save the configuration | |
| 80 | save_matrix_config(config, config_path) | |
| 81 | ||
| 82 | logger.info(f"Created default configuration file at {config_path}") | |
| 83 | logger.info("Please edit this file with your Matrix credentials and Ollama settings.") | |
| 84 | ||
| 85 | sys.exit(0) | |
| 86 | ||
| 87 | def load_config(config_path: str) -> Dict: | |
| 88 | """Load the configuration from a file.""" | |
| 89 | try: | |
| 90 | config = load_matrix_config(config_path) | |
| 91 | return config | |
| 92 | except FileNotFoundError: | |
| 93 | logger.error(f"Configuration file not found: {config_path}") | |
| 94 | logger.info("You can create a default configuration file with --create-config") | |
| 95 | sys.exit(1) | |
| 96 | except Exception as e: | |
| 97 | logger.error(f"Error loading configuration: {e}") | |
| 98 | sys.exit(1) | |
| 99 | ||
| 100 | async def custom_message_handler(room, event, client): | |
| 101 | """ | |
| 102 | Custom message handler for Matrix events. | |
| 103 | ||
| 104 | This is an example of how to create a custom handler for Matrix messages. | |
| 105 | It processes the message with Ollama and sends the response back to the room. | |
| 106 | ||
| 107 | Args: | |
| 108 | room: The Matrix room where the message was received | |
| 109 | event: The Matrix event (message) | |
| 110 | client: The MatrixClient instance | |
| 111 | """ | |
| 112 | message = event.body | |
| 113 | room_id = room.room_id | |
| 114 | sender = event.sender | |
| 115 | ||
| 116 | logger.info(f"Custom handler received message in room {room_id} from {sender}: {message}") | |
| 117 | ||
| 118 | # Process the message with Ollama | |
| 119 | response = await client.process_with_ollama(message) | |
| 120 | ||
| 121 | # Add a prefix to the response | |
| 122 | response = f"Response from Ollama: {response}" | |
| 123 | ||
| 124 | # Send the response back to the room | |
| 125 | await client.send_message(room_id, response) | |
| 126 | ||
| 127 | def get_random_greeting() -> str: | |
| 128 | """Generate a random greeting message.""" | |
| 129 | greetings = [ | |
| 130 | "Hello everyone! I'm here to assist with your questions.", | |
| 131 | "Greetings! I'm your friendly Matrix bot powered by Ollama.", | |
| 132 | "Hi there! I'm online and ready to chat.", | |
| 133 | "Good day! I'm here to help with anything you need.", | |
| 134 | "Hey folks! I've just connected and I'm ready to assist.", | |
| 135 | "Hello! I'm your AI assistant, ready to help with your questions.", | |
| 136 | "Greetings, humans! AI assistant at your service.", | |
| 137 | "Hi everyone! I'm here and ready to chat.", | |
| 138 | "Hello Matrix world! How can I assist you today?", | |
| 139 | "Greetings! Your friendly neighborhood AI has arrived." | |
| 140 | ] | |
| 141 | return random.choice(greetings) | |
| 142 | ||
| 143 | async def main_async(config: Dict) -> None: | |
| 144 | """ | |
| 145 | Main async function to run the Matrix client. | |
| 146 | ||
| 147 | Args: | |
| 148 | config: The configuration dictionary | |
| 149 | """ | |
| 150 | # Extract configuration | |
| 151 | matrix_config = config.get('MATRIX', {}) | |
| 152 | ollama_config = config.get('OLLAMA', {}) | |
| 153 | ||
| 154 | homeserver = matrix_config.get('HOMESERVER') | |
| 155 | user_id = matrix_config.get('USER') | |
| 156 | password = matrix_config.get('PASSWORD') | |
| 157 | device_id = matrix_config.get('DEVICE_ID') | |
| 158 | rooms = matrix_config.get('ROOMS', []) | |
| 159 | ||
| 160 | ollama_host = ollama_config.get('HOST', 'localhost:11434') | |
| 161 | ollama_model = ollama_config.get('MODEL', 'llama3:latest') | |
| 162 | ollama_prompt = ollama_config.get('PROMPT') | |
| 163 | ||
| 164 | # Create and connect the Matrix client | |
| 165 | client = MatrixClient( | |
| 166 | homeserver=homeserver, | |
| 167 | user_id=user_id, | |
| 168 | password=password, | |
| 169 | device_id=device_id, | |
| 170 | ollama_host=ollama_host, | |
| 171 | ollama_model=ollama_model, | |
| 172 | ollama_prompt=ollama_prompt | |
| 173 | ) | |
| 174 | ||
| 175 | # Connect to the Matrix homeserver | |
| 176 | success = await client.connect() | |
| 177 | ||
| 178 | if not success: | |
| 179 | logger.error("Failed to connect to Matrix homeserver") | |
| 180 | return | |
| 181 | ||
| 182 | # Join rooms and send a random greeting | |
| 183 | for room in rooms: | |
| 184 | if isinstance(room, dict) and 'ROOM' in room: | |
| 185 | room_id = room['ROOM'] | |
| 186 | else: | |
| 187 | room_id = room | |
| 188 | ||
| 189 | join_success = await client.join_room(room_id) | |
| 190 | if join_success: | |
| 191 | # Send a random greeting message | |
| 192 | greeting = get_random_greeting() | |
| 193 | await client.send_message(room_id, greeting) | |
| 194 | logger.info(f"Sent greeting to room {room_id}: {greeting}") | |
| 195 | ||
| 196 | # Optional: Set a custom message handler | |
| 197 | # client.set_message_callback(custom_message_handler) | |
| 198 | ||
| 199 | # Start listening for messages | |
| 200 | logger.info("Starting to listen for messages...") | |
| 201 | await client.listen() | |
| 202 | ||
| 203 | def main(): | |
| 204 | """Main function to run the Matrix client.""" | |
| 205 | args = parse_arguments() | |
| 206 | ||
| 207 | # Set log level | |
| 208 | if args.debug: | |
| 209 | logging.getLogger().setLevel(logging.DEBUG) | |
| 210 | ||
| 211 | # Create a default configuration file if requested | |
| 212 | if args.create_config: | |
| 213 | create_config_file(args.config) | |
| 214 | ||
| 215 | # Load the configuration | |
| 216 | config = load_config(args.config) | |
| 217 | ||
| 218 | # Run the async main function | |
| 219 | try: | |
| 220 | asyncio.run(main_async(config)) | |
| 221 | except KeyboardInterrupt: | |
| 222 | logger.info("Interrupted by user") | |
| 223 | except Exception as e: | |
| 224 | logger.error(f"Error: {e}") | |
| 225 | ||
| 226 | if __name__ == '__main__': | |
| 227 | main() |
| … | … | @@ -0,0 +1,181 @@ |
| 1 | """ | |
| 2 | Example demonstrating the OllamaTools plugin system. | |
| 3 | ||
| 4 | This example shows how to create, register, and use plugins with the OllamaTools | |
| 5 | plugin system. | |
| 6 | """ | |
| 7 | ||
| 8 | import os | |
| 9 | import sys | |
| 10 | import logging | |
| 11 | from typing import Dict, Any, List | |
| 12 | ||
| 13 | # Configure logging | |
| 14 | logging.basicConfig( | |
| 15 | level=logging.INFO, | |
| 16 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' | |
| 17 | ) | |
| 18 | logger = logging.getLogger(__name__) | |
| 19 | ||
| 20 | # Import plugin system components using relative imports | |
| 21 | import sys | |
| 22 | import os | |
| 23 | ||
| 24 | # Add the parent directory to the Python path | |
| 25 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) | |
| 26 | ||
| 27 | from ollamatools.plugins import OllamaToolsPlugin, SharedPlugin, MatrixPlugin, AgentPlugin | |
| 28 | from ollamatools.plugins.registry import PluginRegistry | |
| 29 | from ollamatools.plugins.errors import PluginError, handle_plugin_exception | |
| 30 | from ollamatools.config import PluginConfig, load_plugin_config | |
| 31 | ||
| 32 | ||
| 33 | # Define a simple plugin | |
| 34 | class GreetingPlugin(SharedPlugin): | |
| 35 | """Plugin that provides greeting capabilities.""" | |
| 36 | ||
| 37 | def __init__(self, config=None): | |
| 38 | super().__init__(config) | |
| 39 | logger.info("GreetingPlugin created") | |
| 40 | ||
| 41 | def initialize(self, context): | |
| 42 | logger.info("GreetingPlugin initialized with context") | |
| 43 | self.language = self.config.get("language", "en") | |
| 44 | ||
| 45 | def shutdown(self): | |
| 46 | logger.info("GreetingPlugin shutting down") | |
| 47 | ||
| 48 | @property | |
| 49 | def version(self): | |
| 50 | return "1.0.0" | |
| 51 | ||
| 52 | def get_capabilities(self): | |
| 53 | return ["greeting"] | |
| 54 | ||
| 55 | def greet(self, name): | |
| 56 | """ | |
| 57 | Greet a person by name. | |
| 58 | ||
| 59 | Args: | |
| 60 | name: The name of the person to greet | |
| 61 | ||
| 62 | Returns: | |
| 63 | str: The greeting message | |
| 64 | """ | |
| 65 | if self.language == "en": | |
| 66 | return f"Hello, {name}!" | |
| 67 | elif self.language == "es": | |
| 68 | return f"¡Hola, {name}!" | |
| 69 | elif self.language == "fr": | |
| 70 | return f"Bonjour, {name}!" | |
| 71 | else: | |
| 72 | return f"Hello, {name}!" | |
| 73 | ||
| 74 | ||
| 75 | # Define a plugin that depends on another plugin | |
| 76 | class AdvancedGreetingPlugin(SharedPlugin): | |
| 77 | """Plugin that extends the greeting capabilities.""" | |
| 78 | ||
| 79 | @property | |
| 80 | def dependencies(self): | |
| 81 | return ["GreetingPlugin"] | |
| 82 | ||
| 83 | def initialize(self, context): | |
| 84 | logger.info("AdvancedGreetingPlugin initialized") | |
| 85 | self.context = context | |
| 86 | self.formal = self.config.get("formal", False) | |
| 87 | ||
| 88 | def get_capabilities(self): | |
| 89 | return ["advanced_greeting"] | |
| 90 | ||
| 91 | def formal_greet(self, name, title=""): | |
| 92 | """ | |
| 93 | Greet a person formally. | |
| 94 | ||
| 95 | Args: | |
| 96 | name: The name of the person to greet | |
| 97 | title: Optional title (Mr., Ms., Dr., etc.) | |
| 98 | ||
| 99 | Returns: | |
| 100 | str: The formal greeting message | |
| 101 | """ | |
| 102 | # Get the basic greeting plugin | |
| 103 | greeting_plugin = self.context.get("registry").get_plugin("GreetingPlugin") | |
| 104 | ||
| 105 | # Get the basic greeting | |
| 106 | basic_greeting = greeting_plugin.greet(f"{title} {name}").strip("!") | |
| 107 | ||
| 108 | if self.formal: | |
| 109 | return f"{basic_greeting}, it's a pleasure to meet you." | |
| 110 | else: | |
| 111 | return f"{basic_greeting}, how are you doing today?" | |
| 112 | ||
| 113 | ||
| 114 | def main(): | |
| 115 | """Run the plugin system example.""" | |
| 116 | try: | |
| 117 | # Create a plugin registry | |
| 118 | registry = PluginRegistry() | |
| 119 | logger.info("Created plugin registry") | |
| 120 | ||
| 121 | # Create plugin configurations | |
| 122 | greeting_config = PluginConfig({ | |
| 123 | "language": "en" | |
| 124 | }) | |
| 125 | ||
| 126 | advanced_config = PluginConfig({ | |
| 127 | "formal": True | |
| 128 | }) | |
| 129 | ||
| 130 | # Register plugins with configurations | |
| 131 | registry.register_plugin(GreetingPlugin, greeting_config.to_dict()) | |
| 132 | registry.register_plugin(AdvancedGreetingPlugin, advanced_config.to_dict()) | |
| 133 | logger.info("Registered plugins") | |
| 134 | ||
| 135 | # Create a context for plugin initialization | |
| 136 | context = { | |
| 137 | "registry": registry, | |
| 138 | "app_name": "PluginExample" | |
| 139 | } | |
| 140 | ||
| 141 | # Initialize all plugins | |
| 142 | initialized_plugins = registry.initialize_all(context) | |
| 143 | logger.info(f"Initialized {len(initialized_plugins)} plugins") | |
| 144 | ||
| 145 | # Use the plugins | |
| 146 | greeting_plugin = registry.get_plugin("GreetingPlugin") | |
| 147 | advanced_plugin = registry.get_plugin("AdvancedGreetingPlugin") | |
| 148 | ||
| 149 | # Basic greeting | |
| 150 | greeting = greeting_plugin.greet("World") | |
| 151 | logger.info(f"Basic greeting: {greeting}") | |
| 152 | ||
| 153 | # Advanced greeting | |
| 154 | formal_greeting = advanced_plugin.formal_greet("Smith", "Mr.") | |
| 155 | logger.info(f"Advanced greeting: {formal_greeting}") | |
| 156 | ||
| 157 | # Change configuration and see the effect | |
| 158 | greeting_plugin.config["language"] = "es" | |
| 159 | spanish_greeting = greeting_plugin.greet("Mundo") | |
| 160 | logger.info(f"Spanish greeting: {spanish_greeting}") | |
| 161 | ||
| 162 | # Shutdown plugins | |
| 163 | registry.shutdown() | |
| 164 | logger.info("Plugins shut down") | |
| 165 | ||
| 166 | return 0 | |
| 167 | ||
| 168 | except PluginError as e: | |
| 169 | # Handle plugin-specific errors | |
| 170 | error_details = handle_plugin_exception(e) | |
| 171 | logger.error(f"Plugin error: {error_details['error_message']}") | |
| 172 | return 1 | |
| 173 | ||
| 174 | except Exception as e: | |
| 175 | # Handle other errors | |
| 176 | logger.exception("Unexpected error") | |
| 177 | return 1 | |
| 178 | ||
| 179 | ||
| 180 | if __name__ == "__main__": | |
| 181 | sys.exit(main()) |
| … | … | @@ -0,0 +1,753 @@ |
| 1 | #!/usr/bin/env python3 | |
| 2 | """ | |
| 3 | Matrix Agent Plugin - Proof of Concept | |
| 4 | ||
| 5 | This file demonstrates how the matrix client could be integrated with the modular agent | |
| 6 | using the proposed unified plugin architecture. It implements a plugin that connects | |
| 7 | a Matrix client to a ModularOllamaAgent, allowing the agent to process messages from | |
| 8 | Matrix rooms. | |
| 9 | ||
| 10 | This is a proof-of-concept implementation and is not intended for production use. | |
| 11 | """ | |
| 12 | ||
| 13 | import os | |
| 14 | import sys | |
| 15 | import json | |
| 16 | import time | |
| 17 | import logging | |
| 18 | import asyncio | |
| 19 | from typing import Dict, Any, List, Optional, Union, Set, Tuple | |
| 20 | ||
| 21 | # Base plugin interface | |
| 22 | class OllamaToolsPlugin: | |
| 23 | """Base interface for all OllamaTools plugins.""" | |
| 24 | ||
| 25 | def __init__(self, config=None): | |
| 26 | """Initialize the plugin with optional configuration.""" | |
| 27 | self.config = config or {} | |
| 28 | ||
| 29 | def initialize(self, context): | |
| 30 | """Initialize the plugin with the runtime context.""" | |
| 31 | pass | |
| 32 | ||
| 33 | def shutdown(self): | |
| 34 | """Clean up resources when shutting down.""" | |
| 35 | pass | |
| 36 | ||
| 37 | @property | |
| 38 | def name(self): | |
| 39 | """Get the plugin name.""" | |
| 40 | return self.__class__.__name__ | |
| 41 | ||
| 42 | @property | |
| 43 | def version(self): | |
| 44 | """Get the plugin version.""" | |
| 45 | return "1.0.0" | |
| 46 | ||
| 47 | @property | |
| 48 | def dependencies(self): | |
| 49 | """Get plugin dependencies.""" | |
| 50 | return [] | |
| 51 | ||
| 52 | # Matrix plugin interface | |
| 53 | class MatrixPlugin(OllamaToolsPlugin): | |
| 54 | """Base class for Matrix plugins.""" | |
| 55 | ||
| 56 | async def on_message(self, room, event, client): | |
| 57 | """Handle an incoming Matrix message.""" | |
| 58 | return False | |
| 59 | ||
| 60 | def get_commands(self): | |
| 61 | """Get Matrix commands provided by this plugin.""" | |
| 62 | return {} | |
| 63 | ||
| 64 | async def on_room_join(self, room, client): | |
| 65 | """Handle room join event.""" | |
| 66 | pass | |
| 67 | ||
| 68 | async def on_client_start(self, client): | |
| 69 | """Handle client start event.""" | |
| 70 | pass | |
| 71 | ||
| 72 | async def on_client_stop(self, client): | |
| 73 | """Handle client stop event.""" | |
| 74 | pass | |
| 75 | ||
| 76 | # Agent plugin interface | |
| 77 | class AgentPlugin(OllamaToolsPlugin): | |
| 78 | """Base class for agent plugins.""" | |
| 79 | ||
| 80 | def process_message(self, message, context): | |
| 81 | """Process a message and return a response.""" | |
| 82 | return None | |
| 83 | ||
| 84 | def get_commands(self): | |
| 85 | """Get commands provided by this plugin.""" | |
| 86 | return {} | |
| 87 | ||
| 88 | def on_function_call(self, function_name, args, context): | |
| 89 | """Handle a function call.""" | |
| 90 | return None | |
| 91 | ||
| 92 | # Shared plugin interface | |
| 93 | class SharedPlugin(OllamaToolsPlugin): | |
| 94 | """Base class for plugins that can be used by multiple components.""" | |
| 95 | ||
| 96 | def get_capabilities(self): | |
| 97 | """Get capabilities provided by this plugin.""" | |
| 98 | return [] | |
| 99 | ||
| 100 | # Plugin registry | |
| 101 | class PluginRegistry: | |
| 102 | """Registry for OllamaTools plugins.""" | |
| 103 | ||
| 104 | def __init__(self): | |
| 105 | """Initialize the plugin registry.""" | |
| 106 | self.plugins = {} | |
| 107 | self.initialized_plugins = {} | |
| 108 | ||
| 109 | def register_plugin(self, plugin_class, config=None): | |
| 110 | """Register a plugin class.""" | |
| 111 | plugin_name = plugin_class.__name__ | |
| 112 | self.plugins[plugin_name] = { | |
| 113 | "class": plugin_class, | |
| 114 | "config": config | |
| 115 | } | |
| 116 | return self | |
| 117 | ||
| 118 | def initialize_plugin(self, plugin_name, context): | |
| 119 | """Initialize a plugin and its dependencies.""" | |
| 120 | if plugin_name in self.initialized_plugins: | |
| 121 | return self.initialized_plugins[plugin_name] | |
| 122 | ||
| 123 | if plugin_name not in self.plugins: | |
| 124 | raise ValueError(f"Plugin {plugin_name} not found") | |
| 125 | ||
| 126 | plugin_info = self.plugins[plugin_name] | |
| 127 | plugin_class = plugin_info["class"] | |
| 128 | plugin_config = plugin_info["config"] | |
| 129 | ||
| 130 | # Create plugin instance | |
| 131 | plugin_instance = plugin_class(plugin_config) | |
| 132 | ||
| 133 | # Initialize dependencies first | |
| 134 | for dependency in plugin_instance.dependencies: | |
| 135 | self.initialize_plugin(dependency, context) | |
| 136 | ||
| 137 | # Initialize the plugin | |
| 138 | plugin_instance.initialize(context) | |
| 139 | self.initialized_plugins[plugin_name] = plugin_instance | |
| 140 | ||
| 141 | return plugin_instance | |
| 142 | ||
| 143 | def initialize_all(self, context): | |
| 144 | """Initialize all registered plugins.""" | |
| 145 | for plugin_name in list(self.plugins.keys()): | |
| 146 | self.initialize_plugin(plugin_name, context) | |
| 147 | ||
| 148 | def get_plugin(self, plugin_name): | |
| 149 | """Get an initialized plugin.""" | |
| 150 | return self.initialized_plugins.get(plugin_name) | |
| 151 | ||
| 152 | def shutdown(self): | |
| 153 | """Shutdown all initialized plugins.""" | |
| 154 | for plugin_name, plugin in list(self.initialized_plugins.items()): | |
| 155 | try: | |
| 156 | plugin.shutdown() | |
| 157 | except Exception as e: | |
| 158 | print(f"Error shutting down plugin {plugin_name}: {e}") | |
| 159 | ||
| 160 | self.initialized_plugins = {} | |
| 161 | ||
| 162 | # Conversation history plugin | |
| 163 | class ConversationHistoryPlugin(SharedPlugin): | |
| 164 | """Plugin for conversation history tracking.""" | |
| 165 | ||
| 166 | def __init__(self, config=None): | |
| 167 | super().__init__(config) | |
| 168 | self.history_file = self.config.get("history_file") | |
| 169 | self.max_history = self.config.get("max_history", 100) | |
| 170 | self.conversation_history = [] | |
| 171 | ||
| 172 | def initialize(self, context): | |
| 173 | """Initialize the conversation history plugin.""" | |
| 174 | if not self.history_file: | |
| 175 | # Set default history file | |
| 176 | self.history_file = os.path.join( | |
| 177 | os.path.expanduser('~'), | |
| 178 | '.ollamatools', | |
| 179 | 'conversation_history.json' | |
| 180 | ) | |
| 181 | ||
| 182 | self._load_conversation_history() | |
| 183 | ||
| 184 | def _load_conversation_history(self): | |
| 185 | """Load conversation history from file.""" | |
| 186 | if not os.path.exists(self.history_file): | |
| 187 | return | |
| 188 | ||
| 189 | try: | |
| 190 | with open(self.history_file, 'r') as f: | |
| 191 | self.conversation_history = json.load(f) | |
| 192 | print(f"Loaded {len(self.conversation_history)} conversations from history") | |
| 193 | except Exception as e: | |
| 194 | print(f"Error loading conversation history: {e}") | |
| 195 | self.conversation_history = [] | |
| 196 | ||
| 197 | def _save_conversation_history(self): | |
| 198 | """Save conversation history to file.""" | |
| 199 | try: | |
| 200 | # Create directory if it doesn't exist | |
| 201 | os.makedirs(os.path.dirname(self.history_file), exist_ok=True) | |
| 202 | ||
| 203 | with open(self.history_file, 'w') as f: | |
| 204 | json.dump(self.conversation_history, f, indent=2) | |
| 205 | print(f"Saved {len(self.conversation_history)} conversations to history") | |
| 206 | except Exception as e: | |
| 207 | print(f"Error saving conversation history: {e}") | |
| 208 | ||
| 209 | def record_conversation(self, user_message, assistant_response, metadata=None): | |
| 210 | """Record a conversation.""" | |
| 211 | conversation = { | |
| 212 | "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), | |
| 213 | "user_message": user_message, | |
| 214 | "assistant_response": assistant_response, | |
| 215 | "metadata": metadata or {} | |
| 216 | } | |
| 217 | ||
| 218 | self.conversation_history.append(conversation) | |
| 219 | ||
| 220 | # Trim conversation history if it gets too long | |
| 221 | if len(self.conversation_history) > self.max_history: | |
| 222 | self.conversation_history = self.conversation_history[-self.max_history:] | |
| 223 | ||
| 224 | self._save_conversation_history() | |
| 225 | ||
| 226 | return conversation | |
| 227 | ||
| 228 | def get_recent_conversations(self, n=5): | |
| 229 | """Get recent conversations.""" | |
| 230 | return self.conversation_history[-n:] if len(self.conversation_history) >= n else self.conversation_history | |
| 231 | ||
| 232 | def analyze_topics(self): | |
| 233 | """Analyze conversation topics.""" | |
| 234 | if not self.conversation_history: | |
| 235 | return {} | |
| 236 | ||
| 237 | # Simple keyword-based topic analysis | |
| 238 | topics = { | |
| 239 | "help": 0, | |
| 240 | "search": 0, | |
| 241 | "file": 0, | |
| 242 | "command": 0, | |
| 243 | "code": 0, | |
| 244 | "error": 0, | |
| 245 | "issue": 0, | |
| 246 | "question": 0, | |
| 247 | "other": 0 | |
| 248 | } | |
| 249 | ||
| 250 | for conv in self.conversation_history: | |
| 251 | message = conv["user_message"].lower() | |
| 252 | ||
| 253 | # Check for keywords | |
| 254 | if "help" in message or "how" in message: | |
| 255 | topics["help"] += 1 | |
| 256 | if "search" in message or "find" in message or "look" in message: | |
| 257 | topics["search"] += 1 | |
| 258 | if "file" in message or "read" in message or "write" in message: | |
| 259 | topics["file"] += 1 | |
| 260 | if "command" in message or "run" in message or "execute" in message: | |
| 261 | topics["command"] += 1 | |
| 262 | if "code" in message or "program" in message or "script" in message: | |
| 263 | topics["code"] += 1 | |
| 264 | if "error" in message or "bug" in message or "fix" in message: | |
| 265 | topics["error"] += 1 | |
| 266 | if "issue" in message or "problem" in message: | |
| 267 | topics["issue"] += 1 | |
| 268 | if "?" in message: | |
| 269 | topics["question"] += 1 | |
| 270 | ||
| 271 | # If no keywords matched, count as other | |
| 272 | if not any(count > 0 for count in topics.values()): | |
| 273 | topics["other"] += 1 | |
| 274 | ||
| 275 | # Remove topics with zero count | |
| 276 | return {k: v for k, v in topics.items() if v > 0} | |
| 277 | ||
| 278 | def get_metrics(self): | |
| 279 | """Get performance metrics.""" | |
| 280 | if not self.conversation_history: | |
| 281 | return {"error": "No conversation history available"} | |
| 282 | ||
| 283 | # Calculate metrics | |
| 284 | total_conversations = len(self.conversation_history) | |
| 285 | ||
| 286 | # Calculate response times if available | |
| 287 | response_times = [] | |
| 288 | for conv in self.conversation_history: | |
| 289 | if "metadata" in conv and "response_time" in conv["metadata"]: | |
| 290 | response_times.append(conv["metadata"]["response_time"]) | |
| 291 | ||
| 292 | avg_response_time = sum(response_times) / len(response_times) if response_times else 0 | |
| 293 | ||
| 294 | # Calculate token usage if available | |
| 295 | total_tokens = 0 | |
| 296 | for conv in self.conversation_history: | |
| 297 | if "metadata" in conv and "tokens" in conv["metadata"]: | |
| 298 | total_tokens += conv["metadata"]["tokens"] | |
| 299 | ||
| 300 | avg_tokens_per_conversation = total_tokens / total_conversations if total_tokens > 0 else 0 | |
| 301 | ||
| 302 | return { | |
| 303 | "total_conversations": total_conversations, | |
| 304 | "avg_response_time": avg_response_time, | |
| 305 | "total_tokens": total_tokens, | |
| 306 | "avg_tokens_per_conversation": avg_tokens_per_conversation | |
| 307 | } | |
| 308 | ||
| 309 | def get_capabilities(self): | |
| 310 | """Get capabilities provided by this plugin.""" | |
| 311 | return ["conversation_history", "topic_analysis", "performance_metrics"] | |
| 312 | ||
| 313 | def shutdown(self): | |
| 314 | """Save conversation history when shutting down.""" | |
| 315 | self._save_conversation_history() | |
| 316 | ||
| 317 | # Matrix agent adapter plugin | |
| 318 | class MatrixAgentAdapterPlugin(MatrixPlugin): | |
| 319 | """Plugin that adapts an agent for use with Matrix.""" | |
| 320 | ||
| 321 | def __init__(self, config=None): | |
| 322 | super().__init__(config) | |
| 323 | self.agent_class = self.config.get("agent_class") | |
| 324 | self.agent_config = self.config.get("agent_config", {}) | |
| 325 | self.agent = None | |
| 326 | self.registry = None | |
| 327 | ||
| 328 | @property | |
| 329 | def dependencies(self): | |
| 330 | """Get plugin dependencies.""" | |
| 331 | return ["ConversationHistoryPlugin"] | |
| 332 | ||
| 333 | def initialize(self, context): | |
| 334 | """Initialize the plugin with an agent instance.""" | |
| 335 | if not self.agent_class: | |
| 336 | raise ValueError("Agent class not specified") | |
| 337 | ||
| 338 | # Import the agent class if it's a string | |
| 339 | if isinstance(self.agent_class, str): | |
| 340 | module_path, class_name = self.agent_class.rsplit('.', 1) | |
| 341 | module = __import__(module_path, fromlist=[class_name]) | |
| 342 | self.agent_class = getattr(module, class_name) | |
| 343 | ||
| 344 | # Create the agent instance | |
| 345 | self.agent = self.agent_class(**self.agent_config) | |
| 346 | ||
| 347 | # Get the plugin registry | |
| 348 | self.registry = context.get("plugin_registry") | |
| 349 | ||
| 350 | print(f"Initialized MatrixAgentAdapterPlugin with {self.agent_class.__name__}") | |
| 351 | ||
| 352 | async def on_message(self, room, event, client): | |
| 353 | """Handle an incoming Matrix message using the agent.""" | |
| 354 | # Skip messages from the bot itself | |
| 355 | if event.sender == client.user_id: | |
| 356 | return False | |
| 357 | ||
| 358 | message = event.body | |
| 359 | sender = event.sender | |
| 360 | room_id = room.room_id | |
| 361 | ||
| 362 | print(f"Processing message from {sender} in room {room_id}: {message}") | |
| 363 | ||
| 364 | # Record start time for performance tracking | |
| 365 | start_time = time.time() | |
| 366 | ||
| 367 | # Process the message with the agent | |
| 368 | try: | |
| 369 | response = self.agent.chat(message) | |
| 370 | ||
| 371 | # Calculate response time | |
| 372 | response_time = time.time() - start_time | |
| 373 | ||
| 374 | # Record the conversation if history plugin is available | |
| 375 | history_plugin = self.registry.get_plugin("ConversationHistoryPlugin") | |
| 376 | if history_plugin: | |
| 377 | metadata = { | |
| 378 | "sender": sender, | |
| 379 | "room_id": room_id, | |
| 380 | "timestamp": event.server_timestamp, | |
| 381 | "response_time": response_time, | |
| 382 | "tokens": len(message.split()) + len(response.split()) # Rough estimate | |
| 383 | } | |
| 384 | history_plugin.record_conversation(message, response, metadata) | |
| 385 | ||
| 386 | # Send the response | |
| 387 | await client.send_message(room_id, response) | |
| 388 | ||
| 389 | return True | |
| 390 | except Exception as e: | |
| 391 | print(f"Error processing message with agent: {e}") | |
| 392 | await client.send_message(room_id, f"Error processing message: {str(e)}") | |
| 393 | return True | |
| 394 | ||
| 395 | async def on_client_start(self, client): | |
| 396 | """Handle client start event.""" | |
| 397 | print(f"Matrix client started with user ID: {client.user_id}") | |
| 398 | ||
| 399 | def get_commands(self): | |
| 400 | """Get commands from the agent.""" | |
| 401 | commands = {} | |
| 402 | ||
| 403 | # Add history commands | |
| 404 | commands["/history"] = self._handle_history_command | |
| 405 | commands["/topics"] = self._handle_topics_command | |
| 406 | commands["/metrics"] = self._handle_metrics_command | |
| 407 | ||
| 408 | # Add agent commands if available | |
| 409 | if hasattr(self.agent, "get_commands"): | |
| 410 | agent_commands = self.agent.get_commands() | |
| 411 | commands.update(agent_commands) | |
| 412 | ||
| 413 | return commands | |
| 414 | ||
| 415 | async def _handle_history_command(self, client, room_id, args): | |
| 416 | """Handle the /history command.""" | |
| 417 | history_plugin = self.registry.get_plugin("ConversationHistoryPlugin") | |
| 418 | if not history_plugin: | |
| 419 | await client.send_message(room_id, "Conversation history tracking is not available.") | |
| 420 | return | |
| 421 | ||
| 422 | # Parse count argument | |
| 423 | count = 5 | |
| 424 | if args and args[0].isdigit(): | |
| 425 | count = int(args[0]) | |
| 426 | ||
| 427 | # Get recent conversations | |
| 428 | conversations = history_plugin.get_recent_conversations(count) | |
| 429 | ||
| 430 | if not conversations: | |
| 431 | await client.send_message(room_id, "No conversation history available.") | |
| 432 | return | |
| 433 | ||
| 434 | # Format the history | |
| 435 | formatted_history = ["Recent Conversations:"] | |
| 436 | ||
| 437 | for i, conv in enumerate(conversations, 1): | |
| 438 | timestamp = conv.get("timestamp", "Unknown time") | |
| 439 | user_message = conv.get("user_message", "No message") | |
| 440 | metadata = conv.get("metadata", {}) | |
| 441 | sender = metadata.get("sender", "Unknown sender") | |
| 442 | ||
| 443 | # Truncate message if it's too long | |
| 444 | if len(user_message) > 100: | |
| 445 | user_message = user_message[:100] + "..." | |
| 446 | ||
| 447 | formatted_history.append(f"\n{i}. {timestamp}") | |
| 448 | formatted_history.append(f" From: {sender}") | |
| 449 | formatted_history.append(f" Message: {user_message}") | |
| 450 | ||
| 451 | # Send the formatted history | |
| 452 | await client.send_message(room_id, "\n".join(formatted_history)) | |
| 453 | ||
| 454 | async def _handle_topics_command(self, client, room_id, args): | |
| 455 | """Handle the /topics command.""" | |
| 456 | history_plugin = self.registry.get_plugin("ConversationHistoryPlugin") | |
| 457 | if not history_plugin: | |
| 458 | await client.send_message(room_id, "Conversation history tracking is not available.") | |
| 459 | return | |
| 460 | ||
| 461 | # Analyze topics | |
| 462 | topics = history_plugin.analyze_topics() | |
| 463 | ||
| 464 | if not topics: | |
| 465 | await client.send_message(room_id, "No topics found in conversation history.") | |
| 466 | return | |
| 467 | ||
| 468 | # Format the topics | |
| 469 | formatted_topics = ["Conversation Topics:"] | |
| 470 | ||
| 471 | for topic, count in sorted(topics.items(), key=lambda x: x[1], reverse=True): | |
| 472 | formatted_topics.append(f"- {topic}: {count} mentions") | |
| 473 | ||
| 474 | # Send the formatted topics | |
| 475 | await client.send_message(room_id, "\n".join(formatted_topics)) | |
| 476 | ||
| 477 | async def _handle_metrics_command(self, client, room_id, args): | |
| 478 | """Handle the /metrics command.""" | |
| 479 | history_plugin = self.registry.get_plugin("ConversationHistoryPlugin") | |
| 480 | if not history_plugin: | |
| 481 | await client.send_message(room_id, "Conversation history tracking is not available.") | |
| 482 | return | |
| 483 | ||
| 484 | # Get metrics | |
| 485 | metrics = history_plugin.get_metrics() | |
| 486 | ||
| 487 | if "error" in metrics: | |
| 488 | await client.send_message(room_id, metrics["error"]) | |
| 489 | return | |
| 490 | ||
| 491 | # Format the metrics | |
| 492 | formatted_metrics = ["Performance Metrics:"] | |
| 493 | formatted_metrics.append(f"- Total conversations: {metrics['total_conversations']}") | |
| 494 | formatted_metrics.append(f"- Average response time: {metrics['avg_response_time']:.2f}s") | |
| 495 | formatted_metrics.append(f"- Estimated total tokens: {metrics['total_tokens']}") | |
| 496 | formatted_metrics.append(f"- Average tokens per conversation: {metrics['avg_tokens_per_conversation']:.1f}") | |
| 497 | ||
| 498 | # Send the formatted metrics | |
| 499 | await client.send_message(room_id, "\n".join(formatted_metrics)) | |
| 500 | ||
| 501 | def shutdown(self): | |
| 502 | """Clean up the agent.""" | |
| 503 | if hasattr(self.agent, "shutdown"): | |
| 504 | self.agent.shutdown() | |
| 505 | ||
| 506 | # Enhanced Matrix client with plugin support | |
| 507 | class PluginEnabledMatrixClient: | |
| 508 | """Enhanced Matrix client with plugin support.""" | |
| 509 | ||
| 510 | def __init__(self, homeserver, user_id, password, **kwargs): | |
| 511 | """Initialize the Matrix client with plugins.""" | |
| 512 | self.homeserver = homeserver | |
| 513 | self.user_id = user_id | |
| 514 | self.password = password | |
| 515 | self.kwargs = kwargs | |
| 516 | ||
| 517 | # Initialize plugin registry | |
| 518 | self.plugin_registry = PluginRegistry() | |
| 519 | ||
| 520 | # Initialize client | |
| 521 | self.client = None | |
| 522 | self.connected = False | |
| 523 | ||
| 524 | def register_plugin(self, plugin_class, config=None): | |
| 525 | """Register a custom plugin.""" | |
| 526 | self.plugin_registry.register_plugin(plugin_class, config) | |
| 527 | return self | |
| 528 | ||
| 529 | async def connect(self): | |
| 530 | """Connect to the Matrix homeserver and initialize plugins.""" | |
| 531 | try: | |
| 532 | # In a real implementation, this would connect to Matrix | |
| 533 | # For this proof of concept, we'll just simulate it | |
| 534 | print(f"Connecting to Matrix homeserver: {self.homeserver}") | |
| 535 | print(f"User ID: {self.user_id}") | |
| 536 | ||
| 537 | # Simulate connection delay | |
| 538 | await asyncio.sleep(1) | |
| 539 | ||
| 540 | # Set connected state | |
| 541 | self.connected = True | |
| 542 | ||
| 543 | # Initialize plugins | |
| 544 | context = { | |
| 545 | "client": self, | |
| 546 | "plugin_registry": self.plugin_registry | |
| 547 | } | |
| 548 | ||
| 549 | self.plugin_registry.initialize_all(context) | |
| 550 | ||
| 551 | # Notify plugins that client has started | |
| 552 | for plugin_name, plugin in self.plugin_registry.initialized_plugins.items(): | |
| 553 | if isinstance(plugin, MatrixPlugin): | |
| 554 | await plugin.on_client_start(self) | |
| 555 | ||
| 556 | print(f"Connected to Matrix homeserver: {self.homeserver}") | |
| 557 | return True | |
| 558 | except Exception as e: | |
| 559 | print(f"Error connecting to Matrix homeserver: {e}") | |
| 560 | return False | |
| 561 | ||
| 562 | async def join_room(self, room_id): | |
| 563 | """Join a Matrix room.""" | |
| 564 | if not self.connected: | |
| 565 | print("Not connected to Matrix homeserver") | |
| 566 | return False | |
| 567 | ||
| 568 | try: | |
| 569 | # In a real implementation, this would join a room | |
| 570 | # For this proof of concept, we'll just simulate it | |
| 571 | print(f"Joining room: {room_id}") | |
| 572 | ||
| 573 | # Simulate join delay | |
| 574 | await asyncio.sleep(0.5) | |
| 575 | ||
| 576 | # Notify plugins that room has been joined | |
| 577 | for plugin_name, plugin in self.plugin_registry.initialized_plugins.items(): | |
| 578 | if isinstance(plugin, MatrixPlugin): | |
| 579 | await plugin.on_room_join({"room_id": room_id}, self) | |
| 580 | ||
| 581 | print(f"Joined room: {room_id}") | |
| 582 | return True | |
| 583 | except Exception as e: | |
| 584 | print(f"Error joining room: {e}") | |
| 585 | return False | |
| 586 | ||
| 587 | async def send_message(self, room_id, message): | |
| 588 | """Send a message to a Matrix room.""" | |
| 589 | if not self.connected: | |
| 590 | print("Not connected to Matrix homeserver") | |
| 591 | return False | |
| 592 | ||
| 593 | try: | |
| 594 | # In a real implementation, this would send a message | |
| 595 | # For this proof of concept, we'll just simulate it | |
| 596 | print(f"Sending message to room {room_id}: {message}") | |
| 597 | ||
| 598 | # Simulate send delay | |
| 599 | await asyncio.sleep(0.2) | |
| 600 | ||
| 601 | print(f"Message sent to room {room_id}") | |
| 602 | return True | |
| 603 | except Exception as e: | |
| 604 | print(f"Error sending message: {e}") | |
| 605 | return False | |
| 606 | ||
| 607 | async def process_message(self, room_id, sender, message): | |
| 608 | """Process an incoming message.""" | |
| 609 | if not self.connected: | |
| 610 | print("Not connected to Matrix homeserver") | |
| 611 | return | |
| 612 | ||
| 613 | # Create a simulated event | |
| 614 | event = type('Event', (), { | |
| 615 | 'body': message, | |
| 616 | 'sender': sender, | |
| 617 | 'server_timestamp': int(time.time() * 1000) | |
| 618 | }) | |
| 619 | ||
| 620 | # Create a simulated room | |
| 621 | room = type('Room', (), { | |
| 622 | 'room_id': room_id | |
| 623 | }) | |
| 624 | ||
| 625 | # Process the message with plugins | |
| 626 | for plugin_name, plugin in self.plugin_registry.initialized_plugins.items(): | |
| 627 | if isinstance(plugin, MatrixPlugin): | |
| 628 | if await plugin.on_message(room, event, self): | |
| 629 | return | |
| 630 | ||
| 631 | # If no plugin handled the message, print a default response | |
| 632 | print(f"No plugin handled message from {sender} in room {room_id}: {message}") | |
| 633 | ||
| 634 | async def process_command(self, room_id, sender, command): | |
| 635 | """Process a command.""" | |
| 636 | if not self.connected: | |
| 637 | print("Not connected to Matrix homeserver") | |
| 638 | return | |
| 639 | ||
| 640 | # Parse the command | |
| 641 | parts = command.split() | |
| 642 | cmd = parts[0].lower() | |
| 643 | args = parts[1:] if len(parts) > 1 else [] | |
| 644 | ||
| 645 | # Get commands from plugins | |
| 646 | all_commands = {} | |
| 647 | for plugin_name, plugin in self.plugin_registry.initialized_plugins.items(): | |
| 648 | if isinstance(plugin, MatrixPlugin): | |
| 649 | commands = plugin.get_commands() | |
| 650 | all_commands.update(commands) | |
| 651 | ||
| 652 | # Check if the command exists | |
| 653 | if cmd in all_commands: | |
| 654 | # Execute the command | |
| 655 | handler = all_commands[cmd] | |
| 656 | await handler(self, room_id, args) | |
| 657 | else: | |
| 658 | # Unknown command | |
| 659 | await self.send_message(room_id, f"Unknown command: {cmd}") | |
| 660 | ||
| 661 | async def disconnect(self): | |
| 662 | """Disconnect from Matrix and shutdown plugins.""" | |
| 663 | if not self.connected: | |
| 664 | print("Not connected to Matrix homeserver") | |
| 665 | return | |
| 666 | ||
| 667 | try: | |
| 668 | # In a real implementation, this would disconnect from Matrix | |
| 669 | # For this proof of concept, we'll just simulate it | |
| 670 | print(f"Disconnecting from Matrix homeserver: {self.homeserver}") | |
| 671 | ||
| 672 | # Notify plugins that client is stopping | |
| 673 | for plugin_name, plugin in self.plugin_registry.initialized_plugins.items(): | |
| 674 | if isinstance(plugin, MatrixPlugin): | |
| 675 | await plugin.on_client_stop(self) | |
| 676 | ||
| 677 | # Shutdown plugins | |
| 678 | self.plugin_registry.shutdown() | |
| 679 | ||
| 680 | # Set connected state | |
| 681 | self.connected = False | |
| 682 | ||
| 683 | print(f"Disconnected from Matrix homeserver: {self.homeserver}") | |
| 684 | except Exception as e: | |
| 685 | print(f"Error disconnecting from Matrix homeserver: {e}") | |
| 686 | ||
| 687 | # Example usage | |
| 688 | async def main(): | |
| 689 | """Run the example.""" | |
| 690 | try: | |
| 691 | # Create a Matrix client with plugins | |
| 692 | client = PluginEnabledMatrixClient( | |
| 693 | homeserver="https://matrix.example.org", | |
| 694 | user_id="@ollamabot:example.org", | |
| 695 | password="password" | |
| 696 | ) | |
| 697 | ||
| 698 | # Register plugins | |
| 699 | client.register_plugin( | |
| 700 | ConversationHistoryPlugin, | |
| 701 | { | |
| 702 | "history_file": "matrix_conversation_history.json", | |
| 703 | "max_history": 100 | |
| 704 | } | |
| 705 | ) | |
| 706 | ||
| 707 | client.register_plugin( | |
| 708 | MatrixAgentAdapterPlugin, | |
| 709 | { | |
| 710 | "agent_class": "ollamatools.agent.ModularOllamaAgent", | |
| 711 | "agent_config": { | |
| 712 | "model": "llama3:latest", | |
| 713 | "permission_prompts": True | |
| 714 | } | |
| 715 | } | |
| 716 | ) | |
| 717 | ||
| 718 | # Connect to Matrix | |
| 719 | success = await client.connect() | |
| 720 | if not success: | |
| 721 | print("Failed to connect to Matrix homeserver") | |
| 722 | return | |
| 723 | ||
| 724 | # Join a room | |
| 725 | room_id = "#ollamatools:example.org" | |
| 726 | success = await client.join_room(room_id) | |
| 727 | if not success: | |
| 728 | print(f"Failed to join room: {room_id}") | |
| 729 | await client.disconnect() | |
| 730 | return | |
| 731 | ||
| 732 | # Send a welcome message | |
| 733 | await client.send_message(room_id, "Hello! I'm the OllamaTools Matrix bot. Type /help for available commands.") | |
| 734 | ||
| 735 | # Simulate some messages | |
| 736 | await client.process_message(room_id, "@user1:example.org", "Hello bot!") | |
| 737 | await client.process_message(room_id, "@user2:example.org", "What can you do?") | |
| 738 | await client.process_message(room_id, "@user1:example.org", "Tell me about Python.") | |
| 739 | ||
| 740 | # Simulate some commands | |
| 741 | await client.process_command(room_id, "@user1:example.org", "/history") | |
| 742 | await client.process_command(room_id, "@user2:example.org", "/topics") | |
| 743 | await client.process_command(room_id, "@user1:example.org", "/metrics") | |
| 744 | ||
| 745 | # Disconnect from Matrix | |
| 746 | await client.disconnect() | |
| 747 | ||
| 748 | except Exception as e: | |
| 749 | print(f"Error in main function: {e}") | |
| 750 | ||
| 751 | if __name__ == "__main__": | |
| 752 | # Run the example | |
| 753 | asyncio.run(main()) |
| … | … | @@ -0,0 +1,467 @@ |
| 1 | # Code Review: OllamaTools Matrix Integration | |
| 2 | ||
| 3 | ## Overview | |
| 4 | ||
| 5 | This document provides a comprehensive code review of the Matrix integration tools in the OllamaTools project. The review covers the architecture, code quality, error handling, security, performance, and documentation of the codebase. | |
| 6 | ||
| 7 | ## Files Reviewed | |
| 8 | ||
| 9 | 1. `examples/enhanced_matrix_client_example.py` | |
| 10 | 2. `ollamatools/tools/matrix_tools.py` | |
| 11 | 3. `ollamatools/tools/enhanced_matrix_tools.py` | |
| 12 | 4. `ollamatools/tools/enhanced_matrix_core.py` | |
| 13 | 5. `ollamatools/tools/enhanced_matrix_search.py` | |
| 14 | 6. `ollamatools/tools/enhanced_matrix_history.py` | |
| 15 | ||
| 16 | ## 1. Architecture and Design | |
| 17 | ||
| 18 | ### 1.1 Overall Architecture | |
| 19 | ||
| 20 | The Matrix integration is well-structured with a clear separation of concerns: | |
| 21 | ||
| 22 | - Base functionality in `matrix_tools.py` | |
| 23 | - Enhanced functionality split across multiple modules: | |
| 24 | - Core client functionality in `enhanced_matrix_core.py` | |
| 25 | - Search functionality in `enhanced_matrix_search.py` | |
| 26 | - History and analytics in `enhanced_matrix_history.py` | |
| 27 | - Public API in `enhanced_matrix_tools.py` | |
| 28 | ||
| 29 | This modular approach allows for good maintainability and extensibility. | |
| 30 | ||
| 31 | ### 1.2 Design Patterns | |
| 32 | ||
| 33 | The codebase uses several good design patterns: | |
| 34 | ||
| 35 | - **Inheritance**: `EnhancedMatrixClient` extends `MatrixClient` to add functionality | |
| 36 | - **Dependency Injection**: Configuration is passed to clients rather than hardcoded | |
| 37 | - **Facade Pattern**: `enhanced_matrix_tools.py` provides a simplified interface to the underlying modules | |
| 38 | - **Optional Dependencies**: The code gracefully handles missing optional dependencies | |
| 39 | ||
| 40 | ## 2. Code Quality and Best Practices | |
| 41 | ||
| 42 | ### 2.1 `matrix_tools.py` | |
| 43 | ||
| 44 | **Strengths:** | |
| 45 | ||
| 46 | - Well-documented classes and methods with clear docstrings | |
| 47 | - Consistent error handling with try/except blocks | |
| 48 | - Good separation of concerns between client functionality and configuration | |
| 49 | ||
| 50 | **Issues:** | |
| 51 | ||
| 52 | 1. **Lines 196-204**: The `process_with_ollama` method doesn't handle rate limiting or timeout scenarios. | |
| 53 | ||
| 54 | ```python | |
| 55 | async def process_with_ollama(self, message: str, sender: str = None, room_id: str = None) -> str: | |
| 56 | if not OLLAMA_AVAILABLE: | |
| 57 | return "Ollama is not available. Please install the 'ollama' package with 'pip install ollama'." | |
| 58 | ||
| 59 | try: | |
| 60 | ollama_client = OllamaAsync(host=self.ollama_host) | |
| 61 | ``` | |
| 62 | ||
| 63 | 2. **Lines 246-250**: The context management could be improved with a more sophisticated approach than a simple list. | |
| 64 | ||
| 65 | ```python | |
| 66 | # Store user message in context | |
| 67 | if sender and room_id: | |
| 68 | context_key = f"{sender}:{room_id}" | |
| 69 | self.user_contexts[context_key].append(user_message) | |
| 70 | # Trim context if it gets too long | |
| 71 | ``` | |
| 72 | ||
| 73 | 3. **Lines 307-315**: The username detection is simplistic and might fail with certain username patterns. | |
| 74 | ||
| 75 | ```python | |
| 76 | # Extract username from user_id (e.g., "@user:example.com" -> "user") | |
| 77 | username = self.user_id.split(':')[0].lstrip('@') | |
| 78 | ||
| 79 | # Check if the message contains the bot's username (case insensitive) | |
| 80 | if username.lower() in message.lower(): | |
| 81 | logger.info(f"Username mention detected in message: {message}") | |
| 82 | # Process with Ollama and send response | |
| 83 | response = await self.process_with_ollama(message, sender, room_id) | |
| 84 | await self.send_message(room_id, response) | |
| 85 | return | |
| 86 | ``` | |
| 87 | ||
| 88 | ### 2.2 `enhanced_matrix_core.py` | |
| 89 | ||
| 90 | **Strengths:** | |
| 91 | ||
| 92 | - Good extension of the base class with clear additional functionality | |
| 93 | - Improved username detection with multiple methods | |
| 94 | - Well-structured command handling | |
| 95 | ||
| 96 | **Issues:** | |
| 97 | ||
| 98 | 1. **Lines 123-126**: The initialization of `function_feedback` is conditional but there's no check before using it elsewhere. | |
| 99 | ||
| 100 | ```python | |
| 101 | # Initialize function selection feedback | |
| 102 | if MODULAR_AGENT_AVAILABLE: | |
| 103 | from ollamatools.agent.modular.feedback import FunctionSelectionFeedback | |
| 104 | self.function_feedback = FunctionSelectionFeedback() | |
| 105 | ``` | |
| 106 | ||
| 107 | 2. **Lines 186-193**: The search context handling could be more robust with validation of the returned context. | |
| 108 | ||
| 109 | ```python | |
| 110 | # Check if the message might benefit from search | |
| 111 | search_context = "" | |
| 112 | if self.enable_search and MODULAR_AGENT_AVAILABLE and OPENSEARCH_AVAILABLE: | |
| 113 | from ollamatools.tools.enhanced_matrix_search import execute_search_for_message | |
| 114 | search_context = await execute_search_for_message(message, self.opensearch_config) | |
| 115 | ``` | |
| 116 | ||
| 117 | 3. **Lines 196-204**: Error handling in the message processing could be more granular to provide better error messages. | |
| 118 | ||
| 119 | ```python | |
| 120 | # Process with Ollama | |
| 121 | try: | |
| 122 | if search_context: | |
| 123 | # Add search context to the message | |
| 124 | augmented_message = f"{message}\n\nSearch context: {search_context}" | |
| 125 | response = await self.process_with_ollama(augmented_message, sender, room_id) | |
| 126 | else: | |
| 127 | response = await self.process_with_ollama(message, sender, room_id) | |
| 128 | ``` | |
| 129 | ||
| 130 | ### 2.3 `enhanced_matrix_search.py` | |
| 131 | ||
| 132 | **Strengths:** | |
| 133 | ||
| 134 | - Good error handling with informative error messages | |
| 135 | - Clean separation of search detection and execution | |
| 136 | - Graceful degradation when dependencies are missing | |
| 137 | ||
| 138 | **Issues:** | |
| 139 | ||
| 140 | 1. **Lines 67-77**: The factual intent detection doesn't have a fallback mechanism if the detection fails. | |
| 141 | ||
| 142 | ```python | |
| 143 | try: | |
| 144 | factual_intent = detect_factual_intent(message) | |
| 145 | if factual_intent: | |
| 146 | logger.info(f"Factual intent detected in message: {message}") | |
| 147 | try: | |
| 148 | results = await execute_search_query(message, opensearch_config) | |
| 149 | search_context = extract_search_context(results) | |
| 150 | logger.info(f"Search context: {search_context[:100]}...") | |
| 151 | return search_context | |
| 152 | except Exception as e: | |
| 153 | logger.error(f"Error executing search: {e}") | |
| 154 | return "" | |
| 155 | ``` | |
| 156 | ||
| 157 | 2. **Lines 95-107**: The search results handling doesn't validate the structure of the returned results. | |
| 158 | ||
| 159 | ```python | |
| 160 | try: | |
| 161 | # Execute the search | |
| 162 | results = opensearch_query( | |
| 163 | query=query, | |
| 164 | config=opensearch_config | |
| 165 | ) | |
| 166 | ||
| 167 | return results | |
| 168 | except Exception as e: | |
| 169 | logger.error(f"Error executing search: {e}") | |
| 170 | return {"error": str(e), "results": []} | |
| 171 | ``` | |
| 172 | ||
| 173 | ### 2.4 `enhanced_matrix_history.py` | |
| 174 | ||
| 175 | **Strengths:** | |
| 176 | ||
| 177 | - Good file I/O handling with appropriate error handling | |
| 178 | - Well-structured analytics functions | |
| 179 | - Clear separation between data storage and analysis | |
| 180 | ||
| 181 | **Issues:** | |
| 182 | ||
| 183 | 1. **Lines 48-67**: The conversation history is stored as plain JSON without any encryption or privacy considerations. | |
| 184 | ||
| 185 | ```python | |
| 186 | def save_conversation_history(history: List[Dict[str, Any]], history_file: str) -> None: | |
| 187 | """ | |
| 188 | Save conversation history to file. | |
| 189 | ||
| 190 | Args: | |
| 191 | history: The conversation history | |
| 192 | history_file: Path to save the history to | |
| 193 | """ | |
| 194 | try: | |
| 195 | # Create directory if it doesn't exist | |
| 196 | os.makedirs(os.path.dirname(history_file), exist_ok=True) | |
| 197 | ||
| 198 | with open(history_file, 'w') as f: | |
| 199 | json.dump(history, f, indent=2) | |
| 200 | logger.debug(f"Saved {len(history)} conversations to history") | |
| 201 | except Exception as e: | |
| 202 | logger.error(f"Failed to save conversation history: {str(e)}") | |
| 203 | ``` | |
| 204 | ||
| 205 | 2. **Lines 95-133**: The topic analysis is very simplistic and could benefit from more sophisticated NLP techniques. | |
| 206 | ||
| 207 | ```python | |
| 208 | def analyze_conversation_topics(conversations: List[Dict[str, Any]]) -> Dict[str, int]: | |
| 209 | """ | |
| 210 | Analyze common topics in conversation history. | |
| 211 | ||
| 212 | Args: | |
| 213 | conversations: List of conversation records | |
| 214 | ||
| 215 | Returns: | |
| 216 | Dict[str, int]: Dictionary of topics and their frequency | |
| 217 | """ | |
| 218 | if not conversations: | |
| 219 | return {} | |
| 220 | ||
| 221 | # Simple keyword-based topic analysis | |
| 222 | topics = { | |
| 223 | "help": 0, | |
| 224 | "search": 0, | |
| 225 | "file": 0, | |
| 226 | "command": 0, | |
| 227 | "code": 0, | |
| 228 | "error": 0, | |
| 229 | "issue": 0, | |
| 230 | "question": 0, | |
| 231 | "other": 0 | |
| 232 | } | |
| 233 | ``` | |
| 234 | ||
| 235 | 3. **Lines 135-183**: The performance metrics calculation doesn't handle edge cases like empty conversations. | |
| 236 | ||
| 237 | ```python | |
| 238 | def get_performance_metrics(conversations: List[Dict[str, Any]]) -> Dict[str, Any]: | |
| 239 | """ | |
| 240 | Get performance metrics from conversation history. | |
| 241 | ||
| 242 | Args: | |
| 243 | conversations: List of conversation records | |
| 244 | ||
| 245 | Returns: | |
| 246 | Dict[str, Any]: Performance metrics | |
| 247 | """ | |
| 248 | if not conversations: | |
| 249 | return {"error": "No conversation history available"} | |
| 250 | ||
| 251 | # Calculate metrics | |
| 252 | total_conversations = len(conversations) | |
| 253 | total_response_time = sum(conv.get("response_time", 0) for conv in conversations) | |
| 254 | avg_response_time = total_response_time / total_conversations if total_conversations > 0 else 0 | |
| 255 | ``` | |
| 256 | ||
| 257 | ### 2.5 `enhanced_matrix_tools.py` | |
| 258 | ||
| 259 | **Strengths:** | |
| 260 | ||
| 261 | - Clean API design with well-documented functions | |
| 262 | - Good encapsulation of the underlying modules | |
| 263 | - Consistent error handling approach | |
| 264 | ||
| 265 | **Issues:** | |
| 266 | ||
| 267 | 1. **Lines 50-51**: The imports from other modules could lead to circular dependencies. | |
| 268 | ||
| 269 | ```python | |
| 270 | # Import from core module | |
| 271 | from ollamatools.tools.enhanced_matrix_core import ( | |
| 272 | EnhancedMatrixClient, | |
| 273 | _connect_enhanced_matrix | |
| 274 | ) | |
| 275 | ``` | |
| 276 | ||
| 277 | 2. **Lines 124-125**: The configuration creation doesn't validate the values. | |
| 278 | ||
| 279 | ```python | |
| 280 | def create_enhanced_matrix_config() -> Dict: | |
| 281 | """ | |
| 282 | Create a default Enhanced Matrix configuration. | |
| 283 | ``` | |
| 284 | ||
| 285 | ### 2.6 `enhanced_matrix_client_example.py` | |
| 286 | ||
| 287 | **Strengths:** | |
| 288 | ||
| 289 | - Well-documented example with clear instructions | |
| 290 | - Good command-line argument handling | |
| 291 | - Comprehensive error handling | |
| 292 | ||
| 293 | **Issues:** | |
| 294 | ||
| 295 | 1. **Lines 36-37**: The sys.path manipulation is not ideal for production code. | |
| 296 | ||
| 297 | ```python | |
| 298 | # Add the parent directory to the path so we can import ollamatools | |
| 299 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) | |
| 300 | ``` | |
| 301 | ||
| 302 | 2. **Lines 155-166**: The custom message handler is defined but never used in the example. | |
| 303 | ||
| 304 | ```python | |
| 305 | async def custom_message_handler(room, event, client): | |
| 306 | """ | |
| 307 | Custom message handler for Matrix events. | |
| 308 | ||
| 309 | This is an example of how to create a custom handler for Matrix messages. | |
| 310 | It processes the message with Ollama and sends the response back to the room. | |
| 311 | ||
| 312 | Args: | |
| 313 | room: The Matrix room where the message was received | |
| 314 | event: The Matrix event (message) | |
| 315 | client: The MatrixClient instance | |
| 316 | """ | |
| 317 | ``` | |
| 318 | ||
| 319 | 3. **Lines 168-183**: The random greeting function could be improved with more context-aware greetings. | |
| 320 | ||
| 321 | ```python | |
| 322 | def get_random_greeting() -> str: | |
| 323 | """Generate a random greeting message.""" | |
| 324 | greetings = [ | |
| 325 | "Hello everyone! I'm here to assist with your questions.", | |
| 326 | "Greetings! I'm your friendly Matrix bot powered by Ollama.", | |
| 327 | "Hi there! I'm online and ready to chat.", | |
| 328 | "Good day! I'm here to help with anything you need.", | |
| 329 | "Hey folks! I've just connected and I'm ready to assist.", | |
| 330 | "Hello! I'm your AI assistant, ready to help with your questions.", | |
| 331 | "Greetings, humans! AI assistant at your service.", | |
| 332 | "Hi everyone! I'm here and ready to chat.", | |
| 333 | "Hello Matrix world! How can I assist you today?", | |
| 334 | "Greetings! Your friendly neighborhood AI has arrived." | |
| 335 | ] | |
| 336 | return random.choice(greetings) | |
| 337 | ``` | |
| 338 | ||
| 339 | ## 3. Error Handling and Robustness | |
| 340 | ||
| 341 | ### 3.1 Exception Handling | |
| 342 | ||
| 343 | The codebase generally has good exception handling with try/except blocks around critical operations. However, there are some areas for improvement: | |
| 344 | ||
| 345 | 1. **In `matrix_tools.py`, lines 196-204**: The exception handling in `process_with_ollama` could be more specific to handle different types of errors (network issues, API errors, etc.). | |
| 346 | ||
| 347 | 2. **In `enhanced_matrix_core.py`, lines 196-217**: The error handling in the message processing could provide more specific error messages based on the type of exception. | |
| 348 | ||
| 349 | 3. **In `enhanced_matrix_search.py`, lines 67-77**: The nested try/except blocks could be simplified for better readability. | |
| 350 | ||
| 351 | ### 3.2 Input Validation | |
| 352 | ||
| 353 | Input validation is generally good, but there are some areas that could be improved: | |
| 354 | ||
| 355 | 1. **In `matrix_tools.py`, lines 307-315**: The username detection logic could be more robust with regex pattern matching. | |
| 356 | ||
| 357 | 2. **In `enhanced_matrix_history.py`, lines 185-208**: The history command handler doesn't validate that the count is positive. | |
| 358 | ||
| 359 | ### 3.3 Graceful Degradation | |
| 360 | ||
| 361 | The codebase handles missing dependencies well with feature flags and graceful degradation: | |
| 362 | ||
| 363 | 1. **In `enhanced_matrix_search.py`, lines 15-36**: Good handling of optional dependencies with clear logging. | |
| 364 | ||
| 365 | 2. **In `enhanced_matrix_core.py`, lines 123-126**: Conditional initialization of features based on available dependencies. | |
| 366 | ||
| 367 | ## 4. Security Considerations | |
| 368 | ||
| 369 | ### 4.1 Authentication and Authorization | |
| 370 | ||
| 371 | 1. **In `matrix_tools.py`, lines 196-204**: The Ollama API doesn't appear to have authentication, which could be a security concern. | |
| 372 | ||
| 373 | 2. **In `matrix_tools.py`, lines 339-358**: The model change command checks if the model is available but doesn't validate if the user has permission to use it. | |
| 374 | ||
| 375 | ### 4.2 Data Privacy | |
| 376 | ||
| 377 | 1. **In `enhanced_matrix_history.py`, lines 48-67**: Conversation history is stored as plain JSON without encryption, which could expose sensitive information. | |
| 378 | ||
| 379 | 2. **In `enhanced_matrix_core.py`, lines 186-193**: Search queries might contain sensitive information that is sent to external services. | |
| 380 | ||
| 381 | ### 4.3 Input Sanitization | |
| 382 | ||
| 383 | 1. **In `matrix_tools.py`, lines 307-315**: The username detection doesn't sanitize input, which could lead to false positives. | |
| 384 | ||
| 385 | 2. **In `enhanced_matrix_search.py`, lines 95-107**: The search query is passed directly to the search function without sanitization. | |
| 386 | ||
| 387 | ## 5. Performance Considerations | |
| 388 | ||
| 389 | ### 5.1 Resource Usage | |
| 390 | ||
| 391 | 1. **In `matrix_tools.py`, lines 246-250**: The context management could lead to memory issues with long-running bots. | |
| 392 | ||
| 393 | 2. **In `enhanced_matrix_history.py`, lines 48-67**: The conversation history is loaded entirely into memory, which could be problematic with large histories. | |
| 394 | ||
| 395 | ### 5.2 Efficiency | |
| 396 | ||
| 397 | 1. **In `enhanced_matrix_core.py`, lines 186-193**: The search is performed synchronously, which could block the message handling loop. | |
| 398 | ||
| 399 | 2. **In `enhanced_matrix_history.py`, lines 95-133**: The topic analysis is performed on every request, which could be cached for better performance. | |
| 400 | ||
| 401 | ## 6. Documentation and Comments | |
| 402 | ||
| 403 | ### 6.1 Code Documentation | |
| 404 | ||
| 405 | The codebase has excellent documentation with clear docstrings for classes and methods. Examples: | |
| 406 | ||
| 407 | 1. **In `matrix_tools.py`, lines 31-59**: Good class documentation with clear explanation of purpose and parameters. | |
| 408 | ||
| 409 | 2. **In `enhanced_matrix_core.py`, lines 19-33**: Clear module documentation with dependency information. | |
| 410 | ||
| 411 | ### 6.2 Comments | |
| 412 | ||
| 413 | The code includes helpful comments that explain complex logic: | |
| 414 | ||
| 415 | 1. **In `enhanced_matrix_core.py`, lines 186-193**: Good comments explaining the search context handling. | |
| 416 | ||
| 417 | 2. **In `enhanced_matrix_history.py`, lines 95-133**: Clear comments explaining the topic analysis approach. | |
| 418 | ||
| 419 | ### 6.3 Examples | |
| 420 | ||
| 421 | The example code is well-documented and provides clear usage instructions: | |
| 422 | ||
| 423 | 1. **In `enhanced_matrix_client_example.py`, lines 1-29**: Excellent module documentation with requirements and usage instructions. | |
| 424 | ||
| 425 | ## 7. Recommendations | |
| 426 | ||
| 427 | ### 7.1 Architecture and Design | |
| 428 | ||
| 429 | 1. Consider using a more formal plugin architecture for the enhanced features to make it easier to add new capabilities. | |
| 430 | ||
| 431 | 2. Implement a more sophisticated context management system that can handle longer conversations more efficiently. | |
| 432 | ||
| 433 | ### 7.2 Code Quality | |
| 434 | ||
| 435 | 1. Add more specific exception handling for different types of errors (network, API, authentication, etc.). | |
| 436 | ||
| 437 | 2. Improve the username detection with regex pattern matching to avoid false positives. | |
| 438 | ||
| 439 | 3. Add validation for configuration values to ensure they are within acceptable ranges. | |
| 440 | ||
| 441 | ### 7.3 Security | |
| 442 | ||
| 443 | 1. Add encryption for the conversation history to protect sensitive information. | |
| 444 | ||
| 445 | 2. Implement more robust authentication and authorization for model changes and other administrative commands. | |
| 446 | ||
| 447 | 3. Add input sanitization for search queries and other user inputs. | |
| 448 | ||
| 449 | ### 7.4 Performance | |
| 450 | ||
| 451 | 1. Implement caching for frequently used data like topic analysis results. | |
| 452 | ||
| 453 | 2. Consider using a database for conversation history instead of JSON files for better scalability. | |
| 454 | ||
| 455 | 3. Make search operations asynchronous to avoid blocking the message handling loop. | |
| 456 | ||
| 457 | ### 7.5 Documentation | |
| 458 | ||
| 459 | 1. Add more examples of custom message handlers and how to extend the functionality. | |
| 460 | ||
| 461 | 2. Document the security considerations and best practices for deploying the bot in production. | |
| 462 | ||
| 463 | ## 8. Conclusion | |
| 464 | ||
| 465 | The OllamaTools Matrix integration is a well-designed and well-documented codebase with good separation of concerns and error handling. The modular architecture makes it easy to understand and extend. The main areas for improvement are in security, performance optimization, and more robust error handling. | |
| 466 | ||
| 467 | The codebase demonstrates good software engineering practices and provides a solid foundation for building Matrix bots with Ollama integration. With the recommended improvements, it could be even more robust and secure for production use. |
| … | … | @@ -0,0 +1 @@ |
| 1 | Subproject commit 44f8aafb2644ad801e94178ae51d66b3ecf2ac32 |
| … | … | @@ -0,0 +1,40 @@ |
| 1 | """ | |
| 2 | OllamaTools Configuration System. | |
| 3 | ||
| 4 | This package provides configuration management for OllamaTools and its plugins. | |
| 5 | """ | |
| 6 | ||
| 7 | import os | |
| 8 | import logging | |
| 9 | from typing import Dict, Any, Optional | |
| 10 | ||
| 11 | # Configure logging | |
| 12 | logger = logging.getLogger(__name__) | |
| 13 | ||
| 14 | # Default configuration directory | |
| 15 | DEFAULT_CONFIG_DIR = os.path.join( | |
| 16 | os.path.expanduser('~'), | |
| 17 | '.ollamatools' | |
| 18 | ) | |
| 19 | ||
| 20 | # Create default configuration directory if it doesn't exist | |
| 21 | os.makedirs(DEFAULT_CONFIG_DIR, exist_ok=True) | |
| 22 | os.makedirs(os.path.join(DEFAULT_CONFIG_DIR, 'plugins'), exist_ok=True) | |
| 23 | ||
| 24 | # Import the Config class from core_config.py | |
| 25 | from ollamatools.config.core_config import Config | |
| 26 | ||
| 27 | # Import key components | |
| 28 | from ollamatools.config.plugin_config import PluginConfig, load_plugin_config, create_default_config | |
| 29 | from ollamatools.config.schemas import get_schema_for_plugin, SCHEMA_REGISTRY | |
| 30 | ||
| 31 | # Export all components | |
| 32 | __all__ = [ | |
| 33 | 'Config', | |
| 34 | 'PluginConfig', | |
| 35 | 'load_plugin_config', | |
| 36 | 'create_default_config', | |
| 37 | 'get_schema_for_plugin', | |
| 38 | 'SCHEMA_REGISTRY', | |
| 39 | 'DEFAULT_CONFIG_DIR' | |
| 40 | ] |
| … | … | @@ -0,0 +1,107 @@ |
| 1 | """ | |
| 2 | Core configuration management for OllamaTools. | |
| 3 | ||
| 4 | This module contains the Config class, which handles configuration settings for | |
| 5 | the OllamaTools library. | |
| 6 | """ | |
| 7 | ||
| 8 | import json | |
| 9 | import os | |
| 10 | from typing import Optional, Dict, Any | |
| 11 | ||
| 12 | from pydantic import BaseModel, validator, Field | |
| 13 | ||
| 14 | ||
| 15 | class Config(BaseModel): | |
| 16 | """ | |
| 17 | Configuration class for OllamaTools. | |
| 18 | ||
| 19 | This class manages configuration settings for the OllamaTools library using | |
| 20 | Pydantic for validation. It provides methods for loading and saving configuration | |
| 21 | to a file. | |
| 22 | """ | |
| 23 | ||
| 24 | model: str = "llama3.1" | |
| 25 | host: str = "http://localhost:11434" | |
| 26 | permission_prompts: bool = True | |
| 27 | sudo_always_prompt: bool = True | |
| 28 | timeout_seconds: int = 60 | |
| 29 | max_file_size: Optional[int] = None | |
| 30 | ||
| 31 | @validator('timeout_seconds') | |
| 32 | def validate_timeout(cls, v): | |
| 33 | """Validate that timeout is positive.""" | |
| 34 | if v <= 0: | |
| 35 | raise ValueError("Timeout must be positive") | |
| 36 | return v | |
| 37 | ||
| 38 | @validator('max_file_size') | |
| 39 | def validate_max_file_size(cls, v): | |
| 40 | """Validate that max_file_size is positive if provided.""" | |
| 41 | if v is not None and v <= 0: | |
| 42 | raise ValueError("Max file size must be positive") | |
| 43 | return v | |
| 44 | ||
| 45 | @classmethod | |
| 46 | def load_from_file(cls, file_path: str) -> "Config": | |
| 47 | """ | |
| 48 | Load configuration from a JSON file. | |
| 49 | ||
| 50 | Args: | |
| 51 | file_path: Path to the configuration file | |
| 52 | ||
| 53 | Returns: | |
| 54 | Config: Configuration object | |
| 55 | ||
| 56 | Raises: | |
| 57 | FileNotFoundError: If the file does not exist | |
| 58 | json.JSONDecodeError: If the file contains invalid JSON | |
| 59 | ValueError: If the configuration is invalid | |
| 60 | """ | |
| 61 | if not os.path.exists(file_path): | |
| 62 | raise FileNotFoundError(f"Configuration file not found: {file_path}") | |
| 63 | ||
| 64 | with open(file_path, 'r') as f: | |
| 65 | try: | |
| 66 | config_data = json.load(f) | |
| 67 | return cls(**config_data) | |
| 68 | except json.JSONDecodeError: | |
| 69 | raise json.JSONDecodeError(f"Invalid JSON in configuration file: {file_path}", "", 0) | |
| 70 | ||
| 71 | def save_to_file(self, file_path: str) -> None: | |
| 72 | """ | |
| 73 | Save configuration to a JSON file. | |
| 74 | ||
| 75 | Args: | |
| 76 | file_path: Path to the configuration file | |
| 77 | ||
| 78 | Raises: | |
| 79 | PermissionError: If the file cannot be written | |
| 80 | """ | |
| 81 | try: | |
| 82 | # Create directory if it doesn't exist | |
| 83 | os.makedirs(os.path.dirname(os.path.abspath(file_path)), exist_ok=True) | |
| 84 | ||
| 85 | with open(file_path, 'w') as f: | |
| 86 | json.dump(self.dict(), f, indent=2) | |
| 87 | except PermissionError: | |
| 88 | raise PermissionError(f"Cannot write to configuration file: {file_path}") | |
| 89 | ||
| 90 | def to_dict(self) -> Dict[str, Any]: | |
| 91 | """ | |
| 92 | Convert configuration to a dictionary. | |
| 93 | ||
| 94 | Returns: | |
| 95 | Dict[str, Any]: Configuration as a dictionary | |
| 96 | """ | |
| 97 | return self.dict(exclude_none=True) | |
| 98 | ||
| 99 | @classmethod | |
| 100 | def default_config_path(cls) -> str: | |
| 101 | """ | |
| 102 | Get the default configuration file path. | |
| 103 | ||
| 104 | Returns: | |
| 105 | str: Default configuration file path | |
| 106 | """ | |
| 107 | return os.path.expanduser("~/.config/ollamatools/config.json") |
| … | … | @@ -0,0 +1,344 @@ |
| 1 | """ | |
| 2 | Configuration system for the OllamaTools plugin architecture. | |
| 3 | ||
| 4 | This module provides classes and functions for managing plugin configuration. | |
| 5 | """ | |
| 6 | ||
| 7 | import os | |
| 8 | import sys | |
| 9 | import yaml | |
| 10 | import json | |
| 11 | import logging | |
| 12 | from typing import Dict, Any, List, Optional, Set, Union, Callable | |
| 13 | ||
| 14 | from ollamatools.plugins.errors import PluginConfigurationError | |
| 15 | ||
| 16 | # Configure logging | |
| 17 | logger = logging.getLogger(__name__) | |
| 18 | ||
| 19 | class PluginConfig: | |
| 20 | """Configuration manager for plugins.""" | |
| 21 | ||
| 22 | def __init__(self, config=None, schema=None): | |
| 23 | """ | |
| 24 | Initialize the configuration manager. | |
| 25 | ||
| 26 | Args: | |
| 27 | config: Initial configuration dictionary or path to config file | |
| 28 | schema: Optional schema for validation | |
| 29 | """ | |
| 30 | self.schema = schema or {} | |
| 31 | self.config = {} | |
| 32 | ||
| 33 | if config: | |
| 34 | self.load_config(config) | |
| 35 | ||
| 36 | def load_config(self, config): | |
| 37 | """ | |
| 38 | Load configuration from a dictionary, JSON, or YAML file. | |
| 39 | ||
| 40 | Args: | |
| 41 | config: Configuration source (dict, path to JSON/YAML file) | |
| 42 | ||
| 43 | Returns: | |
| 44 | PluginConfig: Self for method chaining | |
| 45 | ||
| 46 | Raises: | |
| 47 | PluginConfigurationError: If configuration loading fails | |
| 48 | """ | |
| 49 | try: | |
| 50 | if isinstance(config, dict): | |
| 51 | # Load from dictionary | |
| 52 | self.config = config | |
| 53 | elif isinstance(config, str) and os.path.exists(config): | |
| 54 | # Load from file | |
| 55 | with open(config, 'r') as f: | |
| 56 | if config.endswith('.json'): | |
| 57 | self.config = json.load(f) | |
| 58 | elif config.endswith(('.yaml', '.yml')): | |
| 59 | self.config = yaml.safe_load(f) | |
| 60 | else: | |
| 61 | raise PluginConfigurationError( | |
| 62 | f"Unsupported configuration file format: {config}", | |
| 63 | details={"file": config} | |
| 64 | ) | |
| 65 | else: | |
| 66 | raise PluginConfigurationError( | |
| 67 | f"Invalid configuration source: {config}", | |
| 68 | details={"source": str(config)} | |
| 69 | ) | |
| 70 | ||
| 71 | # Validate the configuration | |
| 72 | if self.schema: | |
| 73 | self.validate() | |
| 74 | ||
| 75 | return self | |
| 76 | ||
| 77 | except PluginConfigurationError: | |
| 78 | # Re-raise plugin configuration errors | |
| 79 | raise | |
| 80 | except Exception as e: | |
| 81 | # Wrap other exceptions | |
| 82 | raise PluginConfigurationError( | |
| 83 | f"Failed to load configuration: {str(e)}", | |
| 84 | details={"source": str(config)} | |
| 85 | ) from e | |
| 86 | ||
| 87 | def validate(self): | |
| 88 | """ | |
| 89 | Validate the configuration against the schema. | |
| 90 | ||
| 91 | Returns: | |
| 92 | bool: True if valid | |
| 93 | ||
| 94 | Raises: | |
| 95 | PluginConfigurationError: If validation fails | |
| 96 | """ | |
| 97 | # Simple schema validation | |
| 98 | for key, schema_item in self.schema.items(): | |
| 99 | # Check required fields | |
| 100 | if schema_item.get('required', False) and key not in self.config: | |
| 101 | raise PluginConfigurationError( | |
| 102 | f"Missing required configuration key: {key}", | |
| 103 | details={"key": key, "schema": schema_item} | |
| 104 | ) | |
| 105 | ||
| 106 | # Check type if the key exists | |
| 107 | if key in self.config: | |
| 108 | value = self.config[key] | |
| 109 | expected_type = schema_item.get('type') | |
| 110 | ||
| 111 | if expected_type and not isinstance(value, expected_type): | |
| 112 | raise PluginConfigurationError( | |
| 113 | f"Invalid type for configuration key {key}. Expected {expected_type.__name__}, got {type(value).__name__}", | |
| 114 | details={"key": key, "expected_type": expected_type.__name__, "actual_type": type(value).__name__} | |
| 115 | ) | |
| 116 | ||
| 117 | # Check enum values | |
| 118 | enum_values = schema_item.get('enum') | |
| 119 | if enum_values and value not in enum_values: | |
| 120 | raise PluginConfigurationError( | |
| 121 | f"Invalid value for configuration key {key}. Expected one of {enum_values}, got {value}", | |
| 122 | details={"key": key, "expected_values": enum_values, "actual_value": value} | |
| 123 | ) | |
| 124 | ||
| 125 | # Check custom validation | |
| 126 | validator = schema_item.get('validator') | |
| 127 | if validator and callable(validator): | |
| 128 | if not validator(value): | |
| 129 | raise PluginConfigurationError( | |
| 130 | f"Validation failed for configuration key {key}", | |
| 131 | details={"key": key, "value": value} | |
| 132 | ) | |
| 133 | ||
| 134 | return True | |
| 135 | ||
| 136 | def get(self, key, default=None): | |
| 137 | """ | |
| 138 | Get a configuration value. | |
| 139 | ||
| 140 | Args: | |
| 141 | key: Configuration key | |
| 142 | default: Default value if key is not found | |
| 143 | ||
| 144 | Returns: | |
| 145 | Any: Configuration value or default | |
| 146 | """ | |
| 147 | return self.config.get(key, default) | |
| 148 | ||
| 149 | def set(self, key, value): | |
| 150 | """ | |
| 151 | Set a configuration value. | |
| 152 | ||
| 153 | Args: | |
| 154 | key: Configuration key | |
| 155 | value: Configuration value | |
| 156 | ||
| 157 | Returns: | |
| 158 | PluginConfig: Self for method chaining | |
| 159 | ||
| 160 | Raises: | |
| 161 | PluginConfigurationError: If validation fails | |
| 162 | """ | |
| 163 | self.config[key] = value | |
| 164 | ||
| 165 | # Validate if schema exists for this key | |
| 166 | if self.schema and key in self.schema: | |
| 167 | schema_item = self.schema[key] | |
| 168 | ||
| 169 | # Check type | |
| 170 | expected_type = schema_item.get('type') | |
| 171 | if expected_type and not isinstance(value, expected_type): | |
| 172 | raise PluginConfigurationError( | |
| 173 | f"Invalid type for configuration key {key}. Expected {expected_type.__name__}, got {type(value).__name__}", | |
| 174 | details={"key": key, "expected_type": expected_type.__name__, "actual_type": type(value).__name__} | |
| 175 | ) | |
| 176 | ||
| 177 | # Check enum values | |
| 178 | enum_values = schema_item.get('enum') | |
| 179 | if enum_values and value not in enum_values: | |
| 180 | raise PluginConfigurationError( | |
| 181 | f"Invalid value for configuration key {key}. Expected one of {enum_values}, got {value}", | |
| 182 | details={"key": key, "expected_values": enum_values, "actual_value": value} | |
| 183 | ) | |
| 184 | ||
| 185 | # Check custom validation | |
| 186 | validator = schema_item.get('validator') | |
| 187 | if validator and callable(validator): | |
| 188 | if not validator(value): | |
| 189 | raise PluginConfigurationError( | |
| 190 | f"Validation failed for configuration key {key}", | |
| 191 | details={"key": key, "value": value} | |
| 192 | ) | |
| 193 | ||
| 194 | return self | |
| 195 | ||
| 196 | def merge(self, other_config): | |
| 197 | """ | |
| 198 | Merge another configuration into this one. | |
| 199 | ||
| 200 | Args: | |
| 201 | other_config: Another PluginConfig or dictionary | |
| 202 | ||
| 203 | Returns: | |
| 204 | PluginConfig: Self for method chaining | |
| 205 | """ | |
| 206 | if isinstance(other_config, PluginConfig): | |
| 207 | other_dict = other_config.to_dict() | |
| 208 | else: | |
| 209 | other_dict = other_config | |
| 210 | ||
| 211 | # Deep merge | |
| 212 | self._deep_merge(self.config, other_dict) | |
| 213 | ||
| 214 | # Validate the merged configuration | |
| 215 | if self.schema: | |
| 216 | self.validate() | |
| 217 | ||
| 218 | return self | |
| 219 | ||
| 220 | def _deep_merge(self, target, source): | |
| 221 | """ | |
| 222 | Deep merge two dictionaries. | |
| 223 | ||
| 224 | Args: | |
| 225 | target: Target dictionary | |
| 226 | source: Source dictionary | |
| 227 | """ | |
| 228 | for key, value in source.items(): | |
| 229 | if key in target and isinstance(target[key], dict) and isinstance(value, dict): | |
| 230 | self._deep_merge(target[key], value) | |
| 231 | else: | |
| 232 | target[key] = value | |
| 233 | ||
| 234 | def to_dict(self): | |
| 235 | """ | |
| 236 | Convert the configuration to a dictionary. | |
| 237 | ||
| 238 | Returns: | |
| 239 | Dict[str, Any]: Configuration dictionary | |
| 240 | """ | |
| 241 | return self.config.copy() | |
| 242 | ||
| 243 | def save(self, file_path, format='yaml'): | |
| 244 | """ | |
| 245 | Save the configuration to a file. | |
| 246 | ||
| 247 | Args: | |
| 248 | file_path: Path to save the configuration to | |
| 249 | format: File format ('json' or 'yaml') | |
| 250 | ||
| 251 | Returns: | |
| 252 | PluginConfig: Self for method chaining | |
| 253 | ||
| 254 | Raises: | |
| 255 | PluginConfigurationError: If saving fails | |
| 256 | """ | |
| 257 | try: | |
| 258 | # Create directory if it doesn't exist | |
| 259 | os.makedirs(os.path.dirname(file_path), exist_ok=True) | |
| 260 | ||
| 261 | with open(file_path, 'w') as f: | |
| 262 | if format.lower() == 'json': | |
| 263 | json.dump(self.config, f, indent=2) | |
| 264 | elif format.lower() in ('yaml', 'yml'): | |
| 265 | yaml.dump(self.config, f, default_flow_style=False) | |
| 266 | else: | |
| 267 | raise PluginConfigurationError( | |
| 268 | f"Unsupported configuration format: {format}", | |
| 269 | details={"format": format} | |
| 270 | ) | |
| 271 | ||
| 272 | return self | |
| 273 | ||
| 274 | except PluginConfigurationError: | |
| 275 | # Re-raise plugin configuration errors | |
| 276 | raise | |
| 277 | except Exception as e: | |
| 278 | # Wrap other exceptions | |
| 279 | raise PluginConfigurationError( | |
| 280 | f"Failed to save configuration: {str(e)}", | |
| 281 | details={"file_path": file_path, "format": format} | |
| 282 | ) from e | |
| 283 | ||
| 284 | ||
| 285 | def create_default_config(plugin_name, config_type='shared'): | |
| 286 | """ | |
| 287 | Create a default configuration for a plugin. | |
| 288 | ||
| 289 | Args: | |
| 290 | plugin_name: Name of the plugin | |
| 291 | config_type: Type of configuration ('shared', 'matrix', 'agent') | |
| 292 | ||
| 293 | Returns: | |
| 294 | Dict[str, Any]: Default configuration | |
| 295 | """ | |
| 296 | # Base configuration | |
| 297 | config = { | |
| 298 | "enabled": True, | |
| 299 | "name": plugin_name | |
| 300 | } | |
| 301 | ||
| 302 | # Add type-specific defaults | |
| 303 | if config_type == 'shared': | |
| 304 | pass # No additional defaults | |
| 305 | elif config_type == 'matrix': | |
| 306 | config.update({ | |
| 307 | "commands_enabled": True, | |
| 308 | "command_prefix": "/" | |
| 309 | }) | |
| 310 | elif config_type == 'agent': | |
| 311 | config.update({ | |
| 312 | "functions_enabled": True | |
| 313 | }) | |
| 314 | ||
| 315 | return config | |
| 316 | ||
| 317 | ||
| 318 | def load_plugin_config(plugin_name, config_dir=None): | |
| 319 | """ | |
| 320 | Load configuration for a plugin from the default location. | |
| 321 | ||
| 322 | Args: | |
| 323 | plugin_name: Name of the plugin | |
| 324 | config_dir: Optional custom configuration directory | |
| 325 | ||
| 326 | Returns: | |
| 327 | PluginConfig: Plugin configuration | |
| 328 | """ | |
| 329 | if not config_dir: | |
| 330 | # Use default location | |
| 331 | config_dir = os.path.join( | |
| 332 | os.path.expanduser('~'), | |
| 333 | '.ollamatools', | |
| 334 | 'plugins' | |
| 335 | ) | |
| 336 | ||
| 337 | # Check for plugin-specific config file | |
| 338 | config_file = os.path.join(config_dir, f"{plugin_name.lower()}.yaml") | |
| 339 | ||
| 340 | if os.path.exists(config_file): | |
| 341 | return PluginConfig(config_file) | |
| 342 | else: | |
| 343 | # Return default configuration | |
| 344 | return PluginConfig(create_default_config(plugin_name)) |
| … | … | @@ -0,0 +1,163 @@ |
| 1 | """ | |
| 2 | Configuration schemas for OllamaTools plugins. | |
| 3 | ||
| 4 | This module provides schema definitions for validating plugin configurations. | |
| 5 | """ | |
| 6 | ||
| 7 | from typing import Dict, Any, List, Optional, Set, Union, Callable | |
| 8 | ||
| 9 | # Schema for the ConversationHistoryPlugin | |
| 10 | CONVERSATION_HISTORY_SCHEMA = { | |
| 11 | "history_file": { | |
| 12 | "type": str, | |
| 13 | "required": False, | |
| 14 | "description": "Path to the conversation history file" | |
| 15 | }, | |
| 16 | "max_history": { | |
| 17 | "type": int, | |
| 18 | "required": False, | |
| 19 | "default": 100, | |
| 20 | "description": "Maximum number of conversations to keep in history" | |
| 21 | }, | |
| 22 | "enable_topic_analysis": { | |
| 23 | "type": bool, | |
| 24 | "required": False, | |
| 25 | "default": True, | |
| 26 | "description": "Whether to enable topic analysis" | |
| 27 | }, | |
| 28 | "enable_metrics": { | |
| 29 | "type": bool, | |
| 30 | "required": False, | |
| 31 | "default": True, | |
| 32 | "description": "Whether to enable performance metrics" | |
| 33 | } | |
| 34 | } | |
| 35 | ||
| 36 | # Schema for the SearchPlugin | |
| 37 | SEARCH_PLUGIN_SCHEMA = { | |
| 38 | "base_url": { | |
| 39 | "type": str, | |
| 40 | "required": False, | |
| 41 | "default": "http://localhost:8081", | |
| 42 | "description": "Base URL for the OpenSearchNG service" | |
| 43 | }, | |
| 44 | "engines": { | |
| 45 | "type": list, | |
| 46 | "required": False, | |
| 47 | "default": ["google", "bing", "duckduckgo", "brave"], | |
| 48 | "description": "List of search engines to use" | |
| 49 | }, | |
| 50 | "num_results": { | |
| 51 | "type": int, | |
| 52 | "required": False, | |
| 53 | "default": 5, | |
| 54 | "description": "Number of search results to return" | |
| 55 | }, | |
| 56 | "enable_auto_search": { | |
| 57 | "type": bool, | |
| 58 | "required": False, | |
| 59 | "default": True, | |
| 60 | "description": "Whether to enable automatic search for factual queries" | |
| 61 | } | |
| 62 | } | |
| 63 | ||
| 64 | # Schema for the MatrixAgentAdapterPlugin | |
| 65 | MATRIX_AGENT_ADAPTER_SCHEMA = { | |
| 66 | "agent_class": { | |
| 67 | "type": (str, object), # Can be a string path or a class object | |
| 68 | "required": True, | |
| 69 | "description": "Agent class to use (string path or class object)" | |
| 70 | }, | |
| 71 | "agent_config": { | |
| 72 | "type": dict, | |
| 73 | "required": False, | |
| 74 | "default": {}, | |
| 75 | "description": "Configuration for the agent" | |
| 76 | } | |
| 77 | } | |
| 78 | ||
| 79 | # Schema for the MatrixCorePlugin | |
| 80 | MATRIX_CORE_PLUGIN_SCHEMA = { | |
| 81 | "homeserver": { | |
| 82 | "type": str, | |
| 83 | "required": True, | |
| 84 | "description": "URL of the Matrix homeserver" | |
| 85 | }, | |
| 86 | "user_id": { | |
| 87 | "type": str, | |
| 88 | "required": True, | |
| 89 | "description": "Matrix user ID" | |
| 90 | }, | |
| 91 | "password": { | |
| 92 | "type": str, | |
| 93 | "required": True, | |
| 94 | "description": "Matrix password" | |
| 95 | }, | |
| 96 | "device_id": { | |
| 97 | "type": str, | |
| 98 | "required": False, | |
| 99 | "description": "Device ID for E2E encryption" | |
| 100 | }, | |
| 101 | "ssl": { | |
| 102 | "type": bool, | |
| 103 | "required": False, | |
| 104 | "default": True, | |
| 105 | "description": "Whether to use SSL for the Matrix connection" | |
| 106 | }, | |
| 107 | "respond_to_all_messages": { | |
| 108 | "type": bool, | |
| 109 | "required": False, | |
| 110 | "default": False, | |
| 111 | "description": "Whether to respond to all messages (not just mentions)" | |
| 112 | } | |
| 113 | } | |
| 114 | ||
| 115 | # Schema for the ConversationContextPlugin | |
| 116 | CONVERSATION_CONTEXT_SCHEMA = { | |
| 117 | "max_context_length": { | |
| 118 | "type": int, | |
| 119 | "required": False, | |
| 120 | "default": 10, | |
| 121 | "description": "Maximum number of messages to keep in context" | |
| 122 | }, | |
| 123 | "context_strategy": { | |
| 124 | "type": str, | |
| 125 | "required": False, | |
| 126 | "default": "sliding_window", | |
| 127 | "enum": ["sliding_window", "token_based", "time_based", "relevance_based"], | |
| 128 | "description": "Strategy for managing conversation context" | |
| 129 | }, | |
| 130 | "token_limit": { | |
| 131 | "type": int, | |
| 132 | "required": False, | |
| 133 | "default": 4096, | |
| 134 | "description": "Maximum number of tokens to keep in context (for token_based strategy)" | |
| 135 | }, | |
| 136 | "time_window": { | |
| 137 | "type": int, | |
| 138 | "required": False, | |
| 139 | "default": 3600, | |
| 140 | "description": "Time window in seconds for context (for time_based strategy)" | |
| 141 | } | |
| 142 | } | |
| 143 | ||
| 144 | # Schema registry | |
| 145 | SCHEMA_REGISTRY = { | |
| 146 | "ConversationHistoryPlugin": CONVERSATION_HISTORY_SCHEMA, | |
| 147 | "SearchPlugin": SEARCH_PLUGIN_SCHEMA, | |
| 148 | "MatrixAgentAdapterPlugin": MATRIX_AGENT_ADAPTER_SCHEMA, | |
| 149 | "MatrixCorePlugin": MATRIX_CORE_PLUGIN_SCHEMA, | |
| 150 | "ConversationContextPlugin": CONVERSATION_CONTEXT_SCHEMA | |
| 151 | } | |
| 152 | ||
| 153 | def get_schema_for_plugin(plugin_name): | |
| 154 | """ | |
| 155 | Get the configuration schema for a plugin. | |
| 156 | ||
| 157 | Args: | |
| 158 | plugin_name: Name of the plugin | |
| 159 | ||
| 160 | Returns: | |
| 161 | Dict[str, Any]: Schema dictionary or empty dict if not found | |
| 162 | """ | |
| 163 | return SCHEMA_REGISTRY.get(plugin_name, {}) |
| … | … | @@ -0,0 +1,147 @@ |
| 1 | # OllamaTools Plugin System | |
| 2 | ||
| 3 | This directory contains the core components of the OllamaTools plugin system. | |
| 4 | ||
| 5 | ## Overview | |
| 6 | ||
| 7 | The plugin system provides a flexible way to extend OllamaTools functionality through plugins. Plugins can be used to add new features, integrate with external services, or customize behavior. | |
| 8 | ||
| 9 | ## Plugin Types | |
| 10 | ||
| 11 | The plugin system supports several types of plugins: | |
| 12 | ||
| 13 | - **SharedPlugin**: Plugins that can be used by multiple components | |
| 14 | - **MatrixPlugin**: Plugins specifically for Matrix integration | |
| 15 | - **AgentPlugin**: Plugins for extending agent functionality | |
| 16 | ||
| 17 | ## Creating a Plugin | |
| 18 | ||
| 19 | To create a plugin, you need to: | |
| 20 | ||
| 21 | 1. Create a class that inherits from one of the plugin base classes | |
| 22 | 2. Implement the required methods | |
| 23 | 3. Register the plugin with the plugin registry | |
| 24 | ||
| 25 | ### Example Plugin | |
| 26 | ||
| 27 | ```python | |
| 28 | from ollamatools.plugins import SharedPlugin | |
| 29 | ||
| 30 | class MyPlugin(SharedPlugin): | |
| 31 | """Example plugin that demonstrates the plugin system.""" | |
| 32 | ||
| 33 | def __init__(self, config=None): | |
| 34 | super().__init__(config) | |
| 35 | # Initialize your plugin | |
| 36 | ||
| 37 | def initialize(self, context): | |
| 38 | # Set up your plugin with the runtime context | |
| 39 | pass | |
| 40 | ||
| 41 | def shutdown(self): | |
| 42 | # Clean up resources | |
| 43 | pass | |
| 44 | ||
| 45 | @property | |
| 46 | def version(self): | |
| 47 | return "1.0.0" | |
| 48 | ||
| 49 | @property | |
| 50 | def dependencies(self): | |
| 51 | # List any other plugins this plugin depends on | |
| 52 | return [] | |
| 53 | ||
| 54 | def get_capabilities(self): | |
| 55 | # Return a list of capabilities provided by this plugin | |
| 56 | return ["my_capability"] | |
| 57 | ``` | |
| 58 | ||
| 59 | ## Registering a Plugin | |
| 60 | ||
| 61 | Plugins can be registered in several ways: | |
| 62 | ||
| 63 | ### 1. Direct Registration | |
| 64 | ||
| 65 | ```python | |
| 66 | from ollamatools.plugins.registry import PluginRegistry | |
| 67 | from my_plugin import MyPlugin | |
| 68 | ||
| 69 | # Create a registry | |
| 70 | registry = PluginRegistry() | |
| 71 | ||
| 72 | # Register the plugin | |
| 73 | registry.register_plugin(MyPlugin) | |
| 74 | ||
| 75 | # Initialize the plugin | |
| 76 | context = {"some_context": "value"} | |
| 77 | registry.initialize_plugin("MyPlugin", context) | |
| 78 | ``` | |
| 79 | ||
| 80 | ### 2. Entry Points | |
| 81 | ||
| 82 | You can also register plugins using entry points in your `setup.py`: | |
| 83 | ||
| 84 | ```python | |
| 85 | setup( | |
| 86 | name="my_plugin_package", | |
| 87 | # ... | |
| 88 | entry_points={ | |
| 89 | "ollamatools.plugins": [ | |
| 90 | "my_plugin = my_plugin:MyPlugin", | |
| 91 | ], | |
| 92 | }, | |
| 93 | ) | |
| 94 | ``` | |
| 95 | ||
| 96 | Then discover plugins: | |
| 97 | ||
| 98 | ```python | |
| 99 | registry = PluginRegistry() | |
| 100 | registry.discover_plugins() | |
| 101 | ``` | |
| 102 | ||
| 103 | ## Plugin Configuration | |
| 104 | ||
| 105 | Plugins can be configured using the configuration system: | |
| 106 | ||
| 107 | ```python | |
| 108 | from ollamatools.config import load_plugin_config | |
| 109 | ||
| 110 | # Load configuration for a plugin | |
| 111 | config = load_plugin_config("MyPlugin") | |
| 112 | ||
| 113 | # Use the configuration | |
| 114 | value = config.get("some_setting", "default_value") | |
| 115 | ``` | |
| 116 | ||
| 117 | ## Error Handling | |
| 118 | ||
| 119 | The plugin system includes comprehensive error handling: | |
| 120 | ||
| 121 | ```python | |
| 122 | from ollamatools.plugins.errors import PluginError, handle_plugin_exception | |
| 123 | ||
| 124 | try: | |
| 125 | # Do something with a plugin | |
| 126 | plugin.some_method() | |
| 127 | except PluginError as e: | |
| 128 | # Handle plugin-specific errors | |
| 129 | error_details = handle_plugin_exception(e) | |
| 130 | print(f"Plugin error: {error_details['error_message']}") | |
| 131 | ``` | |
| 132 | ||
| 133 | ## Plugin Lifecycle | |
| 134 | ||
| 135 | 1. **Registration**: Plugin classes are registered with the registry | |
| 136 | 2. **Initialization**: Plugins are instantiated and initialized with context | |
| 137 | 3. **Operation**: Plugins provide functionality through their methods | |
| 138 | 4. **Shutdown**: Plugins clean up resources when no longer needed | |
| 139 | ||
| 140 | ## Best Practices | |
| 141 | ||
| 142 | 1. Keep plugins focused on a single responsibility | |
| 143 | 2. Document plugin capabilities and configuration options | |
| 144 | 3. Handle errors gracefully | |
| 145 | 4. Clean up resources in the shutdown method | |
| 146 | 5. Use dependency injection through the context parameter | |
| 147 | 6. Follow the plugin interface contract |
| … | … | @@ -0,0 +1,177 @@ |
| 1 | """ | |
| 2 | OllamaTools Plugin System. | |
| 3 | ||
| 4 | This module provides the base interfaces and registry for the OllamaTools plugin system. | |
| 5 | """ | |
| 6 | ||
| 7 | import os | |
| 8 | import sys | |
| 9 | import importlib | |
| 10 | import logging | |
| 11 | from typing import Dict, Any, List, Optional, Set, Type, Callable, Union | |
| 12 | ||
| 13 | # Configure logging | |
| 14 | logger = logging.getLogger(__name__) | |
| 15 | ||
| 16 | class OllamaToolsPlugin: | |
| 17 | """Base interface for all OllamaTools plugins.""" | |
| 18 | ||
| 19 | def __init__(self, config=None): | |
| 20 | """ | |
| 21 | Initialize the plugin with optional configuration. | |
| 22 | ||
| 23 | Args: | |
| 24 | config: Optional configuration dictionary for the plugin | |
| 25 | """ | |
| 26 | self.config = config or {} | |
| 27 | ||
| 28 | def initialize(self, context): | |
| 29 | """ | |
| 30 | Initialize the plugin with the runtime context. | |
| 31 | ||
| 32 | Args: | |
| 33 | context: Runtime context dictionary containing references to other components | |
| 34 | """ | |
| 35 | pass | |
| 36 | ||
| 37 | def shutdown(self): | |
| 38 | """Clean up resources when shutting down.""" | |
| 39 | pass | |
| 40 | ||
| 41 | @property | |
| 42 | def name(self): | |
| 43 | """ | |
| 44 | Get the plugin name. | |
| 45 | ||
| 46 | Returns: | |
| 47 | str: The plugin name | |
| 48 | """ | |
| 49 | return self.__class__.__name__ | |
| 50 | ||
| 51 | @property | |
| 52 | def version(self): | |
| 53 | """ | |
| 54 | Get the plugin version. | |
| 55 | ||
| 56 | Returns: | |
| 57 | str: The plugin version | |
| 58 | """ | |
| 59 | return "1.0.0" | |
| 60 | ||
| 61 | @property | |
| 62 | def dependencies(self): | |
| 63 | """ | |
| 64 | Get plugin dependencies. | |
| 65 | ||
| 66 | Returns: | |
| 67 | List[str]: List of plugin names that this plugin depends on | |
| 68 | """ | |
| 69 | return [] | |
| 70 | ||
| 71 | ||
| 72 | class MatrixPlugin(OllamaToolsPlugin): | |
| 73 | """Base class for Matrix plugins.""" | |
| 74 | ||
| 75 | async def on_message(self, room, event, client): | |
| 76 | """ | |
| 77 | Handle an incoming Matrix message. | |
| 78 | ||
| 79 | Args: | |
| 80 | room: The room the message was sent in | |
| 81 | event: The message event | |
| 82 | client: The Matrix client | |
| 83 | ||
| 84 | Returns: | |
| 85 | bool: True if the message was handled, False otherwise | |
| 86 | """ | |
| 87 | return False | |
| 88 | ||
| 89 | def get_commands(self): | |
| 90 | """ | |
| 91 | Get Matrix commands provided by this plugin. | |
| 92 | ||
| 93 | Returns: | |
| 94 | Dict[str, Callable]: Dictionary of command names to handler functions | |
| 95 | """ | |
| 96 | return {} | |
| 97 | ||
| 98 | async def on_room_join(self, room, client): | |
| 99 | """ | |
| 100 | Handle room join event. | |
| 101 | ||
| 102 | Args: | |
| 103 | room: The room that was joined | |
| 104 | client: The Matrix client | |
| 105 | """ | |
| 106 | pass | |
| 107 | ||
| 108 | async def on_client_start(self, client): | |
| 109 | """ | |
| 110 | Handle client start event. | |
| 111 | ||
| 112 | Args: | |
| 113 | client: The Matrix client | |
| 114 | """ | |
| 115 | pass | |
| 116 | ||
| 117 | async def on_client_stop(self, client): | |
| 118 | """ | |
| 119 | Handle client stop event. | |
| 120 | ||
| 121 | Args: | |
| 122 | client: The Matrix client | |
| 123 | """ | |
| 124 | pass | |
| 125 | ||
| 126 | ||
| 127 | class AgentPlugin(OllamaToolsPlugin): | |
| 128 | """Base class for agent plugins.""" | |
| 129 | ||
| 130 | def process_message(self, message, context): | |
| 131 | """ | |
| 132 | Process a message and return a response. | |
| 133 | ||
| 134 | Args: | |
| 135 | message: The user message | |
| 136 | context: Processing context | |
| 137 | ||
| 138 | Returns: | |
| 139 | Optional[str]: Response message if handled, None otherwise | |
| 140 | """ | |
| 141 | return None | |
| 142 | ||
| 143 | def get_commands(self): | |
| 144 | """ | |
| 145 | Get commands provided by this plugin. | |
| 146 | ||
| 147 | Returns: | |
| 148 | Dict[str, Callable]: Dictionary of command names to handler functions | |
| 149 | """ | |
| 150 | return {} | |
| 151 | ||
| 152 | def on_function_call(self, function_name, args, context): | |
| 153 | """ | |
| 154 | Handle a function call. | |
| 155 | ||
| 156 | Args: | |
| 157 | function_name: The name of the function being called | |
| 158 | args: The arguments to the function | |
| 159 | context: Processing context | |
| 160 | ||
| 161 | Returns: | |
| 162 | Any: The result of the function call | |
| 163 | """ | |
| 164 | return None | |
| 165 | ||
| 166 | ||
| 167 | class SharedPlugin(OllamaToolsPlugin): | |
| 168 | """Base class for plugins that can be used by multiple components.""" | |
| 169 | ||
| 170 | def get_capabilities(self): | |
| 171 | """ | |
| 172 | Get capabilities provided by this plugin. | |
| 173 | ||
| 174 | Returns: | |
| 175 | List[str]: List of capability names | |
| 176 | """ | |
| 177 | return [] |
| … | … | @@ -0,0 +1,142 @@ |
| 1 | """ | |
| 2 | Error handling for the OllamaTools plugin system. | |
| 3 | ||
| 4 | This module provides exception classes and utility functions for handling | |
| 5 | errors in the OllamaTools plugin system. | |
| 6 | """ | |
| 7 | ||
| 8 | import sys | |
| 9 | import logging | |
| 10 | import traceback | |
| 11 | from typing import Dict, Any, Optional | |
| 12 | ||
| 13 | # Configure logging | |
| 14 | logger = logging.getLogger(__name__) | |
| 15 | ||
| 16 | class PluginError(Exception): | |
| 17 | """Base class for all plugin-related errors.""" | |
| 18 | ||
| 19 | def __init__(self, message, details=None): | |
| 20 | """ | |
| 21 | Initialize the error. | |
| 22 | ||
| 23 | Args: | |
| 24 | message: Error message | |
| 25 | details: Optional dictionary with additional error details | |
| 26 | """ | |
| 27 | super().__init__(message) | |
| 28 | self.details = details or {} | |
| 29 | ||
| 30 | ||
| 31 | class PluginNotFoundError(PluginError): | |
| 32 | """Error raised when a plugin is not found.""" | |
| 33 | pass | |
| 34 | ||
| 35 | ||
| 36 | class PluginInitializationError(PluginError): | |
| 37 | """Error raised when a plugin fails to initialize.""" | |
| 38 | pass | |
| 39 | ||
| 40 | ||
| 41 | class PluginDependencyError(PluginError): | |
| 42 | """Error raised when a plugin dependency cannot be satisfied.""" | |
| 43 | pass | |
| 44 | ||
| 45 | ||
| 46 | class PluginExecutionError(PluginError): | |
| 47 | """Error raised when a plugin operation fails during execution.""" | |
| 48 | pass | |
| 49 | ||
| 50 | ||
| 51 | class PluginConfigurationError(PluginError): | |
| 52 | """Error raised when a plugin has invalid configuration.""" | |
| 53 | pass | |
| 54 | ||
| 55 | ||
| 56 | def format_plugin_error(error: PluginError) -> str: | |
| 57 | """ | |
| 58 | Format a plugin error into a user-friendly message. | |
| 59 | ||
| 60 | Args: | |
| 61 | error: The plugin error | |
| 62 | ||
| 63 | Returns: | |
| 64 | str: Formatted error message | |
| 65 | """ | |
| 66 | message = str(error) | |
| 67 | ||
| 68 | if isinstance(error, PluginNotFoundError): | |
| 69 | return f"Plugin not found: {message}" | |
| 70 | ||
| 71 | if isinstance(error, PluginInitializationError): | |
| 72 | return f"Failed to initialize plugin: {message}" | |
| 73 | ||
| 74 | if isinstance(error, PluginDependencyError): | |
| 75 | dependency = error.details.get("dependency", "unknown") | |
| 76 | return f"Plugin dependency error: {message} (missing: {dependency})" | |
| 77 | ||
| 78 | if isinstance(error, PluginExecutionError): | |
| 79 | plugin = error.details.get("plugin", "unknown") | |
| 80 | operation = error.details.get("operation", "unknown operation") | |
| 81 | return f"Plugin execution error in {plugin}.{operation}: {message}" | |
| 82 | ||
| 83 | if isinstance(error, PluginConfigurationError): | |
| 84 | plugin = error.details.get("plugin", "unknown") | |
| 85 | return f"Invalid configuration for plugin {plugin}: {message}" | |
| 86 | ||
| 87 | # Generic plugin error | |
| 88 | return f"Plugin error: {message}" | |
| 89 | ||
| 90 | ||
| 91 | def log_plugin_error(error: PluginError, level=logging.ERROR) -> None: | |
| 92 | """ | |
| 93 | Log a plugin error with appropriate context. | |
| 94 | ||
| 95 | Args: | |
| 96 | error: The plugin error | |
| 97 | level: Logging level | |
| 98 | """ | |
| 99 | # Format the error message | |
| 100 | message = format_plugin_error(error) | |
| 101 | ||
| 102 | # Add details if available | |
| 103 | if error.details: | |
| 104 | details_str = ", ".join(f"{k}={v}" for k, v in error.details.items()) | |
| 105 | message = f"{message} [Details: {details_str}]" | |
| 106 | ||
| 107 | # Log the error | |
| 108 | logger.log(level, message, exc_info=True) | |
| 109 | ||
| 110 | ||
| 111 | def handle_plugin_exception(error: Exception) -> Dict[str, Any]: | |
| 112 | """ | |
| 113 | Handle a plugin exception and return error details. | |
| 114 | ||
| 115 | Args: | |
| 116 | error: The exception to handle | |
| 117 | ||
| 118 | Returns: | |
| 119 | Dict[str, Any]: Error details | |
| 120 | """ | |
| 121 | error_type = type(error).__name__ | |
| 122 | error_message = str(error) | |
| 123 | error_traceback = traceback.format_exc() | |
| 124 | ||
| 125 | # Log the error | |
| 126 | if isinstance(error, PluginError): | |
| 127 | log_plugin_error(error) | |
| 128 | else: | |
| 129 | logger.error(f"Unexpected error: {error_type}: {error_message}", exc_info=True) | |
| 130 | ||
| 131 | # Create error details | |
| 132 | error_details = { | |
| 133 | "error_type": error_type, | |
| 134 | "error_message": error_message, | |
| 135 | "traceback": error_traceback | |
| 136 | } | |
| 137 | ||
| 138 | # Add plugin-specific details | |
| 139 | if isinstance(error, PluginError) and error.details: | |
| 140 | error_details["details"] = error.details | |
| 141 | ||
| 142 | return error_details |
| … | … | @@ -0,0 +1,198 @@ |
| 1 | """ | |
| 2 | Plugin Registry for OllamaTools. | |
| 3 | ||
| 4 | This module provides the PluginRegistry class for managing OllamaTools plugins. | |
| 5 | """ | |
| 6 | ||
| 7 | import os | |
| 8 | import sys | |
| 9 | import importlib | |
| 10 | import logging | |
| 11 | import pkg_resources | |
| 12 | from typing import Dict, Any, List, Optional, Set, Type, Callable, Union | |
| 13 | ||
| 14 | from ollamatools.plugins import OllamaToolsPlugin | |
| 15 | from ollamatools.plugins.errors import ( | |
| 16 | PluginError, | |
| 17 | PluginInitializationError, | |
| 18 | PluginDependencyError, | |
| 19 | PluginNotFoundError, | |
| 20 | handle_plugin_exception | |
| 21 | ) | |
| 22 | ||
| 23 | # Configure logging | |
| 24 | logger = logging.getLogger(__name__) | |
| 25 | ||
| 26 | class PluginRegistry: | |
| 27 | """Registry for OllamaTools plugins.""" | |
| 28 | ||
| 29 | def __init__(self): | |
| 30 | """Initialize the plugin registry.""" | |
| 31 | self.plugins = {} | |
| 32 | self.initialized_plugins = {} | |
| 33 | ||
| 34 | def register_plugin(self, plugin_class, config=None): | |
| 35 | """ | |
| 36 | Register a plugin class. | |
| 37 | ||
| 38 | Args: | |
| 39 | plugin_class: The plugin class to register | |
| 40 | config: Optional configuration for the plugin | |
| 41 | ||
| 42 | Returns: | |
| 43 | PluginRegistry: Self for method chaining | |
| 44 | """ | |
| 45 | plugin_name = plugin_class.__name__ | |
| 46 | self.plugins[plugin_name] = { | |
| 47 | "class": plugin_class, | |
| 48 | "config": config | |
| 49 | } | |
| 50 | logger.debug(f"Registered plugin: {plugin_name}") | |
| 51 | return self | |
| 52 | ||
| 53 | def discover_plugins(self): | |
| 54 | """ | |
| 55 | Discover plugins using entry points. | |
| 56 | ||
| 57 | Returns: | |
| 58 | PluginRegistry: Self for method chaining | |
| 59 | """ | |
| 60 | try: | |
| 61 | for entry_point in pkg_resources.iter_entry_points('ollamatools.plugins'): | |
| 62 | try: | |
| 63 | plugin_class = entry_point.load() | |
| 64 | self.register_plugin(plugin_class) | |
| 65 | logger.info(f"Discovered plugin: {plugin_class.__name__} from entry point {entry_point.name}") | |
| 66 | except Exception as e: | |
| 67 | logger.error(f"Error loading plugin from entry point {entry_point.name}: {e}") | |
| 68 | except Exception as e: | |
| 69 | logger.error(f"Error discovering plugins: {e}") | |
| 70 | ||
| 71 | return self | |
| 72 | ||
| 73 | def initialize_plugin(self, plugin_name, context): | |
| 74 | """ | |
| 75 | Initialize a plugin and its dependencies. | |
| 76 | ||
| 77 | Args: | |
| 78 | plugin_name: The name of the plugin to initialize | |
| 79 | context: Runtime context for the plugin | |
| 80 | ||
| 81 | Returns: | |
| 82 | OllamaToolsPlugin: The initialized plugin instance | |
| 83 | ||
| 84 | Raises: | |
| 85 | PluginNotFoundError: If the plugin is not found | |
| 86 | PluginInitializationError: If the plugin fails to initialize | |
| 87 | PluginDependencyError: If a dependency cannot be initialized | |
| 88 | """ | |
| 89 | # Return already initialized plugin | |
| 90 | if plugin_name in self.initialized_plugins: | |
| 91 | return self.initialized_plugins[plugin_name] | |
| 92 | ||
| 93 | # Check if plugin exists | |
| 94 | if plugin_name not in self.plugins: | |
| 95 | raise PluginNotFoundError(f"Plugin {plugin_name} not found") | |
| 96 | ||
| 97 | logger.debug(f"Initializing plugin: {plugin_name}") | |
| 98 | ||
| 99 | plugin_info = self.plugins[plugin_name] | |
| 100 | plugin_class = plugin_info["class"] | |
| 101 | plugin_config = plugin_info["config"] | |
| 102 | ||
| 103 | try: | |
| 104 | # Create plugin instance | |
| 105 | plugin_instance = plugin_class(plugin_config) | |
| 106 | ||
| 107 | # Initialize dependencies first | |
| 108 | for dependency in plugin_instance.dependencies: | |
| 109 | try: | |
| 110 | self.initialize_plugin(dependency, context) | |
| 111 | except PluginError as e: | |
| 112 | raise PluginDependencyError( | |
| 113 | f"Failed to initialize dependency {dependency} for plugin {plugin_name}", | |
| 114 | details={"plugin": plugin_name, "dependency": dependency, "original_error": str(e)} | |
| 115 | ) from e | |
| 116 | ||
| 117 | # Initialize the plugin | |
| 118 | plugin_instance.initialize(context) | |
| 119 | self.initialized_plugins[plugin_name] = plugin_instance | |
| 120 | ||
| 121 | logger.info(f"Initialized plugin: {plugin_name}") | |
| 122 | return plugin_instance | |
| 123 | ||
| 124 | except PluginError: | |
| 125 | # Re-raise plugin errors | |
| 126 | raise | |
| 127 | except Exception as e: | |
| 128 | # Wrap other exceptions | |
| 129 | raise PluginInitializationError( | |
| 130 | f"Failed to initialize plugin {plugin_name}: {str(e)}", | |
| 131 | details={"plugin": plugin_name} | |
| 132 | ) from e | |
| 133 | ||
| 134 | def initialize_all(self, context): | |
| 135 | """ | |
| 136 | Initialize all registered plugins. | |
| 137 | ||
| 138 | Args: | |
| 139 | context: Runtime context for the plugins | |
| 140 | ||
| 141 | Returns: | |
| 142 | Dict[str, OllamaToolsPlugin]: Dictionary of initialized plugins | |
| 143 | """ | |
| 144 | for plugin_name in list(self.plugins.keys()): | |
| 145 | try: | |
| 146 | self.initialize_plugin(plugin_name, context) | |
| 147 | except PluginError as e: | |
| 148 | logger.error(f"Error initializing plugin {plugin_name}: {e}") | |
| 149 | handle_plugin_exception(e) | |
| 150 | except Exception as e: | |
| 151 | logger.error(f"Unexpected error initializing plugin {plugin_name}: {e}") | |
| 152 | ||
| 153 | return self.initialized_plugins | |
| 154 | ||
| 155 | def get_plugin(self, plugin_name): | |
| 156 | """ | |
| 157 | Get an initialized plugin. | |
| 158 | ||
| 159 | Args: | |
| 160 | plugin_name: The name of the plugin to get | |
| 161 | ||
| 162 | Returns: | |
| 163 | Optional[OllamaToolsPlugin]: The plugin instance or None if not found | |
| 164 | """ | |
| 165 | return self.initialized_plugins.get(plugin_name) | |
| 166 | ||
| 167 | def get_plugins_by_type(self, plugin_type): | |
| 168 | """ | |
| 169 | Get all initialized plugins of a specific type. | |
| 170 | ||
| 171 | Args: | |
| 172 | plugin_type: The plugin type class | |
| 173 | ||
| 174 | Returns: | |
| 175 | Dict[str, OllamaToolsPlugin]: Dictionary of plugin names to instances | |
| 176 | """ | |
| 177 | return { | |
| 178 | name: plugin for name, plugin in self.initialized_plugins.items() | |
| 179 | if isinstance(plugin, plugin_type) | |
| 180 | } | |
| 181 | ||
| 182 | def shutdown(self): | |
| 183 | """ | |
| 184 | Shutdown all initialized plugins. | |
| 185 | ||
| 186 | Returns: | |
| 187 | PluginRegistry: Self for method chaining | |
| 188 | """ | |
| 189 | for plugin_name, plugin in list(self.initialized_plugins.items()): | |
| 190 | try: | |
| 191 | logger.debug(f"Shutting down plugin: {plugin_name}") | |
| 192 | plugin.shutdown() | |
| 193 | logger.info(f"Shut down plugin: {plugin_name}") | |
| 194 | except Exception as e: | |
| 195 | logger.error(f"Error shutting down plugin {plugin_name}: {e}") | |
| 196 | ||
| 197 | self.initialized_plugins = {} | |
| 198 | return self |
| … | … | @@ -5,6 +5,8 @@ This package contains various tool modules for use with OllamaTools: |
| 5 | 5 | - system_tools: Execute Linux commands and file operations |
| 6 | 6 | - web_tools: Make web requests and process responses |
| 7 | 7 | - doc_tools: Generate and process documentation |
| 8 | - matrix_tools: Connect to Matrix homeservers and chat rooms | |
| 9 | - enhanced_matrix_tools: Enhanced Matrix client with modular agent functionality | |
| 8 | 10 | """ |
| 9 | 11 | |
| 10 | 12 | from ollamatools.tools.system_tools import ( |
| … | … | @@ -26,6 +28,31 @@ from ollamatools.tools.doc_tools import ( |
| 26 | 28 | summarize_text |
| 27 | 29 | ) |
| 28 | 30 | |
| 31 | from ollamatools.tools.matrix_tools import ( | |
| 32 | MatrixClient, | |
| 33 | MatrixConfig, | |
| 34 | connect_to_matrix, | |
| 35 | join_matrix_room, | |
| 36 | send_matrix_message, | |
| 37 | listen_for_matrix_messages, | |
| 38 | load_matrix_config, | |
| 39 | save_matrix_config, | |
| 40 | create_default_matrix_config | |
| 41 | ) | |
| 42 | ||
| 43 | from ollamatools.tools.enhanced_matrix_tools import ( | |
| 44 | EnhancedMatrixClient, | |
| 45 | connect_enhanced_matrix, | |
| 46 | join_enhanced_matrix_room, | |
| 47 | send_enhanced_matrix_message, | |
| 48 | listen_for_enhanced_matrix_messages, | |
| 49 | create_enhanced_matrix_config, | |
| 50 | disconnect_enhanced_matrix, | |
| 51 | process_with_enhanced_ollama, | |
| 52 | MODULAR_AGENT_AVAILABLE, | |
| 53 | OPENSEARCH_AVAILABLE | |
| 54 | ) | |
| 55 | ||
| 29 | 56 | __all__ = [ |
| 30 | 57 | 'execute_command', |
| 31 | 58 | 'read_file', |
| … | … | @@ -36,5 +63,24 @@ __all__ = [ |
| 36 | 63 | 'search_web', |
| 37 | 64 | 'generate_markdown', |
| 38 | 65 | 'extract_context', |
| 39 | 'summarize_text' | |
| 66 | 'summarize_text', | |
| 67 | 'MatrixClient', | |
| 68 | 'MatrixConfig', | |
| 69 | 'connect_to_matrix', | |
| 70 | 'join_matrix_room', | |
| 71 | 'send_matrix_message', | |
| 72 | 'listen_for_matrix_messages', | |
| 73 | 'load_matrix_config', | |
| 74 | 'save_matrix_config', | |
| 75 | 'create_default_matrix_config', | |
| 76 | 'EnhancedMatrixClient', | |
| 77 | 'connect_enhanced_matrix', | |
| 78 | 'join_enhanced_matrix_room', | |
| 79 | 'send_enhanced_matrix_message', | |
| 80 | 'listen_for_enhanced_matrix_messages', | |
| 81 | 'create_enhanced_matrix_config', | |
| 82 | 'disconnect_enhanced_matrix', | |
| 83 | 'process_with_enhanced_ollama', | |
| 84 | 'MODULAR_AGENT_AVAILABLE', | |
| 85 | 'OPENSEARCH_AVAILABLE' | |
| 40 | 86 | ] |
| … | … | @@ -0,0 +1,466 @@ |
| 1 | """ | |
| 2 | Core functionality for the Enhanced Matrix integration in OllamaTools. | |
| 3 | ||
| 4 | This module provides the core EnhancedMatrixClient class and basic functionality | |
| 5 | for connecting to Matrix homeservers, joining rooms, and handling messages. | |
| 6 | ||
| 7 | Dependencies: | |
| 8 | - matrix-nio: For Matrix protocol support | |
| 9 | - matrix-nio[e2e]: For end-to-end encryption support (optional) | |
| 10 | - pyyaml: For configuration handling | |
| 11 | - ollama: For Ollama API access | |
| 12 | """ | |
| 13 | ||
| 14 | import os | |
| 15 | import sys | |
| 16 | import yaml | |
| 17 | import json | |
| 18 | import time | |
| 19 | import logging | |
| 20 | import asyncio | |
| 21 | import re | |
| 22 | from typing import Dict, List, Optional, Union, Callable, Any, Set, Tuple | |
| 23 | from collections import defaultdict | |
| 24 | ||
| 25 | # Import base matrix tools | |
| 26 | from ollamatools.tools.matrix_tools import ( | |
| 27 | MatrixClient, | |
| 28 | MatrixConfig, | |
| 29 | RoomMessageText | |
| 30 | ) | |
| 31 | ||
| 32 | # Configure logging | |
| 33 | logger = logging.getLogger(__name__) | |
| 34 | ||
| 35 | # Import flags from other modules | |
| 36 | from ollamatools.tools.enhanced_matrix_search import MODULAR_AGENT_AVAILABLE, OPENSEARCH_AVAILABLE | |
| 37 | ||
| 38 | class EnhancedMatrixClient(MatrixClient): | |
| 39 | """ | |
| 40 | Enhanced Matrix client with advanced features from modular and extended agents. | |
| 41 | ||
| 42 | This client extends the base MatrixClient with: | |
| 43 | - Improved message response logic | |
| 44 | - Natural language understanding | |
| 45 | - Search integration | |
| 46 | - Conversation history tracking and summarization | |
| 47 | - Topic analysis | |
| 48 | - Performance metrics | |
| 49 | """ | |
| 50 | ||
| 51 | # Additional command patterns | |
| 52 | SEARCH_COMMAND_PATTERN = re.compile(r'^/search\s+(.+)$') | |
| 53 | HISTORY_COMMAND_PATTERN = re.compile(r'^/history(?:\s+(\d+))?$') | |
| 54 | TOPICS_COMMAND_PATTERN = re.compile(r'^/topics$') | |
| 55 | METRICS_COMMAND_PATTERN = re.compile(r'^/metrics$') | |
| 56 | ||
| 57 | def __init__( | |
| 58 | self, | |
| 59 | homeserver: str, | |
| 60 | user_id: str, | |
| 61 | password: str, | |
| 62 | device_id: str = None, | |
| 63 | ollama_host: str = "localhost:11434", | |
| 64 | ollama_model: str = "llama3:latest", | |
| 65 | ollama_prompt: str = None, | |
| 66 | available_models: List[str] = None, | |
| 67 | admin_users: List[str] = None, | |
| 68 | opensearch_config: Dict[str, Any] = None, | |
| 69 | ssl: bool = True, | |
| 70 | enable_search: bool = True, | |
| 71 | enable_conversation_tracking: bool = True, | |
| 72 | max_context_length: int = 10, | |
| 73 | max_conversation_history: int = 100, | |
| 74 | respond_to_all_messages: bool = False | |
| 75 | ): | |
| 76 | """ | |
| 77 | Initialize a new Enhanced Matrix client. | |
| 78 | ||
| 79 | Args: | |
| 80 | homeserver: The URL of the Matrix homeserver | |
| 81 | user_id: The Matrix user ID (e.g., @user:example.com) | |
| 82 | password: The password for the Matrix account | |
| 83 | device_id: A unique identifier for this device (for E2E encryption) | |
| 84 | ollama_host: The host address for the Ollama API | |
| 85 | ollama_model: The Ollama model to use for generating responses | |
| 86 | ollama_prompt: An optional system prompt for the Ollama model | |
| 87 | available_models: List of available Ollama models | |
| 88 | admin_users: List of Matrix user IDs that can change models for all users | |
| 89 | opensearch_config: Configuration for OpenSearchNG | |
| 90 | ssl: Whether to use SSL for the Matrix connection | |
| 91 | enable_search: Whether to enable search functionality | |
| 92 | enable_conversation_tracking: Whether to enable conversation tracking | |
| 93 | max_context_length: Maximum number of messages to keep in context | |
| 94 | max_conversation_history: Maximum number of conversations to keep in history | |
| 95 | respond_to_all_messages: Whether to respond to all messages (not just mentions) | |
| 96 | """ | |
| 97 | # Initialize the base class | |
| 98 | super().__init__( | |
| 99 | homeserver=homeserver, | |
| 100 | user_id=user_id, | |
| 101 | password=password, | |
| 102 | device_id=device_id, | |
| 103 | ollama_host=ollama_host, | |
| 104 | ollama_model=ollama_model, | |
| 105 | ollama_prompt=ollama_prompt, | |
| 106 | available_models=available_models, | |
| 107 | admin_users=admin_users, | |
| 108 | ssl=ssl | |
| 109 | ) | |
| 110 | ||
| 111 | # Set enhanced options | |
| 112 | self.enable_search = enable_search and OPENSEARCH_AVAILABLE and MODULAR_AGENT_AVAILABLE | |
| 113 | self.enable_conversation_tracking = enable_conversation_tracking | |
| 114 | self.max_context_length = max_context_length | |
| 115 | self.max_conversation_history = max_conversation_history | |
| 116 | self.respond_to_all_messages = respond_to_all_messages | |
| 117 | ||
| 118 | # Initialize OpenSearch configuration | |
| 119 | from ollamatools.tools.enhanced_matrix_search import get_default_opensearch_config | |
| 120 | self.opensearch_config = opensearch_config or get_default_opensearch_config() if OPENSEARCH_AVAILABLE else None | |
| 121 | ||
| 122 | # Initialize conversation history tracking | |
| 123 | self.conversation_history = [] | |
| 124 | self.conversation_history_file = os.path.join( | |
| 125 | os.path.expanduser('~'), | |
| 126 | '.ollamatools', | |
| 127 | 'matrix_conversation_history.json' | |
| 128 | ) | |
| 129 | ||
| 130 | # Initialize function selection feedback | |
| 131 | if MODULAR_AGENT_AVAILABLE: | |
| 132 | from ollamatools.agent.modular.feedback import FunctionSelectionFeedback | |
| 133 | self.function_feedback = FunctionSelectionFeedback() | |
| 134 | ||
| 135 | # Load conversation history if enabled | |
| 136 | if self.enable_conversation_tracking: | |
| 137 | from ollamatools.tools.enhanced_matrix_history import load_conversation_history | |
| 138 | self.conversation_history = load_conversation_history(self.conversation_history_file) | |
| 139 | ||
| 140 | async def _message_callback(self, room: Any, event: Any) -> None: | |
| 141 | """ | |
| 142 | Enhanced internal callback for handling incoming messages. | |
| 143 | ||
| 144 | This extends the base callback with: | |
| 145 | - Improved username detection | |
| 146 | - Natural language understanding | |
| 147 | - Search integration | |
| 148 | - Conversation tracking | |
| 149 | ||
| 150 | Args: | |
| 151 | room: The room the message was sent in | |
| 152 | event: The message event | |
| 153 | """ | |
| 154 | # Use the base class method to handle basic functionality | |
| 155 | if not hasattr(event, 'body') or not hasattr(event, 'sender'): | |
| 156 | return | |
| 157 | ||
| 158 | # Get message timestamp | |
| 159 | message_timestamp = event.server_timestamp / 1000 | |
| 160 | ||
| 161 | # Skip messages from before the client connected | |
| 162 | if self.connection_time and message_timestamp < self.connection_time: | |
| 163 | logger.debug("Skipping old message") | |
| 164 | return | |
| 165 | ||
| 166 | # Skip messages from the bot itself | |
| 167 | if event.sender == self.user_id: | |
| 168 | logger.debug("Skipping message from self") | |
| 169 | return | |
| 170 | ||
| 171 | message = event.body | |
| 172 | room_id = room.room_id | |
| 173 | sender = event.sender | |
| 174 | ||
| 175 | logger.info(f"Received message in room {room_id} from {sender}: {message}") | |
| 176 | ||
| 177 | # Record start time for performance tracking | |
| 178 | start_time = time.time() | |
| 179 | ||
| 180 | # If a custom callback is set, use it | |
| 181 | if self.message_callback: | |
| 182 | await self.message_callback(room, event, self) | |
| 183 | return | |
| 184 | ||
| 185 | # Check if this is a command | |
| 186 | if message.startswith('/'): | |
| 187 | await self._handle_command(message, room_id, sender) | |
| 188 | ||
| 189 | # Record in conversation history | |
| 190 | if self.enable_conversation_tracking: | |
| 191 | response_time = time.time() - start_time | |
| 192 | from ollamatools.tools.enhanced_matrix_history import record_conversation | |
| 193 | record_conversation( | |
| 194 | self.conversation_history, | |
| 195 | message, | |
| 196 | "Command processed", | |
| 197 | sender, | |
| 198 | room_id, | |
| 199 | response_time, | |
| 200 | self.conversation_history_file, | |
| 201 | self.max_conversation_history | |
| 202 | ) | |
| 203 | ||
| 204 | return | |
| 205 | ||
| 206 | # Extract username from user_id (e.g., "@user:example.com" -> "user") | |
| 207 | username = self.user_id.split(':')[0].lstrip('@') | |
| 208 | ||
| 209 | # Check if the message contains the bot's username (case insensitive) | |
| 210 | # or if it's a direct mention like @username:server.com | |
| 211 | should_respond = False | |
| 212 | ||
| 213 | # Check for username mention (case insensitive) | |
| 214 | if username.lower() in message.lower(): | |
| 215 | logger.info(f"Username mention detected in message: {message}") | |
| 216 | should_respond = True | |
| 217 | ||
| 218 | # Check for direct mention like @username:server.com | |
| 219 | elif self.user_id.lower() in message.lower(): | |
| 220 | logger.info(f"Direct mention detected in message: {message}") | |
| 221 | should_respond = True | |
| 222 | ||
| 223 | # Check if we should respond to all messages | |
| 224 | elif self.respond_to_all_messages: | |
| 225 | logger.info(f"Responding to all messages: {message}") | |
| 226 | should_respond = True | |
| 227 | ||
| 228 | # If we should respond, process with Ollama and send response | |
| 229 | if should_respond: | |
| 230 | # Check if the message might benefit from search | |
| 231 | search_context = "" | |
| 232 | if self.enable_search and MODULAR_AGENT_AVAILABLE and OPENSEARCH_AVAILABLE: | |
| 233 | from ollamatools.tools.enhanced_matrix_search import execute_search_for_message | |
| 234 | search_context = await execute_search_for_message(message, self.opensearch_config) | |
| 235 | ||
| 236 | # Process with Ollama | |
| 237 | try: | |
| 238 | if search_context: | |
| 239 | # Add search context to the message | |
| 240 | augmented_message = f"{message}\n\nSearch context: {search_context}" | |
| 241 | response = await self.process_with_ollama(augmented_message, sender, room_id) | |
| 242 | else: | |
| 243 | response = await self.process_with_ollama(message, sender, room_id) | |
| 244 | ||
| 245 | # Send the response | |
| 246 | await self.send_message(room_id, response) | |
| 247 | ||
| 248 | # Record in conversation history | |
| 249 | if self.enable_conversation_tracking: | |
| 250 | response_time = time.time() - start_time | |
| 251 | from ollamatools.tools.enhanced_matrix_history import record_conversation | |
| 252 | record_conversation( | |
| 253 | self.conversation_history, | |
| 254 | message, | |
| 255 | response, | |
| 256 | sender, | |
| 257 | room_id, | |
| 258 | response_time, | |
| 259 | self.conversation_history_file, | |
| 260 | self.max_conversation_history | |
| 261 | ) | |
| 262 | except Exception as e: | |
| 263 | logger.error(f"Error processing message with Ollama: {e}") | |
| 264 | error_message = f"I encountered an error processing your message: {str(e)}" | |
| 265 | await self.send_message(room_id, error_message) | |
| 266 | ||
| 267 | async def _handle_command(self, message: str, room_id: str, sender: str) -> None: | |
| 268 | """ | |
| 269 | Enhanced command handler with additional commands. | |
| 270 | ||
| 271 | Args: | |
| 272 | message: The command message | |
| 273 | room_id: The ID of the room where the command was sent | |
| 274 | sender: The Matrix user ID of the sender | |
| 275 | """ | |
| 276 | # Check for base commands first | |
| 277 | model_match = self.MODEL_COMMAND_PATTERN.match(message) | |
| 278 | if model_match: | |
| 279 | model_name = model_match.group(1) | |
| 280 | await self._change_model(model_name, room_id, sender) | |
| 281 | return | |
| 282 | ||
| 283 | if self.HELP_COMMAND_PATTERN.match(message): | |
| 284 | await self._send_help(room_id) | |
| 285 | return | |
| 286 | ||
| 287 | if self.LIST_MODELS_COMMAND_PATTERN.match(message): | |
| 288 | await self._list_models(room_id) | |
| 289 | return | |
| 290 | ||
| 291 | # Check for enhanced commands | |
| 292 | search_match = self.SEARCH_COMMAND_PATTERN.match(message) | |
| 293 | if search_match and self.enable_search: | |
| 294 | query = search_match.group(1) | |
| 295 | from ollamatools.tools.enhanced_matrix_search import handle_search_command | |
| 296 | await handle_search_command(self, query, room_id) | |
| 297 | return | |
| 298 | ||
| 299 | history_match = self.HISTORY_COMMAND_PATTERN.match(message) | |
| 300 | if history_match and self.enable_conversation_tracking: | |
| 301 | count = int(history_match.group(1)) if history_match.group(1) else 5 | |
| 302 | from ollamatools.tools.enhanced_matrix_history import handle_history_command | |
| 303 | await handle_history_command(self, count, room_id) | |
| 304 | return | |
| 305 | ||
| 306 | if self.TOPICS_COMMAND_PATTERN.match(message) and self.enable_conversation_tracking: | |
| 307 | from ollamatools.tools.enhanced_matrix_history import handle_topics_command | |
| 308 | await handle_topics_command(self, room_id) | |
| 309 | return | |
| 310 | ||
| 311 | if self.METRICS_COMMAND_PATTERN.match(message) and self.enable_conversation_tracking: | |
| 312 | from ollamatools.tools.enhanced_matrix_history import handle_metrics_command | |
| 313 | await handle_metrics_command(self, room_id) | |
| 314 | return | |
| 315 | ||
| 316 | # Unknown command | |
| 317 | await self.send_message(room_id, f"Unknown command: {message}\nType /help for available commands.") | |
| 318 | ||
| 319 | async def _send_help(self, room_id: str) -> None: | |
| 320 | """ | |
| 321 | Send enhanced help information to a room. | |
| 322 | ||
| 323 | Args: | |
| 324 | room_id: The ID of the room to send the help to | |
| 325 | """ | |
| 326 | help_text = """ | |
| 327 | Available commands: | |
| 328 | - /help - Show this help message | |
| 329 | - /models - List available models | |
| 330 | - /model <model_name> - Change to a different model | |
| 331 | """ | |
| 332 | ||
| 333 | if self.enable_search: | |
| 334 | help_text += "- /search <query> - Search the web for information\n" | |
| 335 | ||
| 336 | if self.enable_conversation_tracking: | |
| 337 | help_text += """- /history [count] - Show recent conversation history | |
| 338 | - /topics - Show conversation topics | |
| 339 | - /metrics - Show performance metrics | |
| 340 | """ | |
| 341 | ||
| 342 | help_text += f""" | |
| 343 | You can also mention me by my username in your message and I'll respond. | |
| 344 | For example: "Hey {self.user_id.split(':')[0].lstrip('@')}, what's the weather like today?" | |
| 345 | """ | |
| 346 | ||
| 347 | await self.send_message(room_id, help_text.strip()) | |
| 348 | ||
| 349 | async def listen(self) -> None: | |
| 350 | """ | |
| 351 | Start listening for messages in joined rooms. | |
| 352 | ||
| 353 | This method will block until the client is stopped. | |
| 354 | """ | |
| 355 | try: | |
| 356 | # Set up the message callback | |
| 357 | self.client.add_event_callback(self._message_callback, RoomMessageText) | |
| 358 | ||
| 359 | # Start syncing with the server | |
| 360 | await self.client.sync_forever(timeout=30000, full_state=True) | |
| 361 | except KeyboardInterrupt: | |
| 362 | logger.info("Interrupted by user") | |
| 363 | except Exception as e: | |
| 364 | logger.error(f"Error in listen: {e}") | |
| 365 | finally: | |
| 366 | # Clean up resources | |
| 367 | await self.disconnect() | |
| 368 | ||
| 369 | async def disconnect(self) -> None: | |
| 370 | """ | |
| 371 | Disconnect from the Matrix homeserver and clean up resources. | |
| 372 | """ | |
| 373 | try: | |
| 374 | logger.info(f"Disconnecting from {self.homeserver}...") | |
| 375 | ||
| 376 | # Save conversation history before disconnecting | |
| 377 | if self.enable_conversation_tracking: | |
| 378 | from ollamatools.tools.enhanced_matrix_history import save_conversation_history | |
| 379 | save_conversation_history(self.conversation_history, self.conversation_history_file) | |
| 380 | ||
| 381 | # Logout and close the client | |
| 382 | try: | |
| 383 | await self.client.logout() | |
| 384 | logger.info("Logged out from Matrix homeserver") | |
| 385 | except Exception as e: | |
| 386 | logger.error(f"Error logging out: {e}") | |
| 387 | ||
| 388 | # Close the client session | |
| 389 | try: | |
| 390 | await self.client.close() | |
| 391 | logger.info("Matrix client session closed") | |
| 392 | except Exception as e: | |
| 393 | logger.error(f"Error closing Matrix client session: {e}") | |
| 394 | ||
| 395 | logger.info(f"Disconnected from {self.homeserver}") | |
| 396 | except Exception as e: | |
| 397 | logger.error(f"Error during disconnect: {e}") | |
| 398 | ||
| 399 | # Async helper function for the module API | |
| 400 | async def _connect_enhanced_matrix( | |
| 401 | homeserver: str, | |
| 402 | user_id: str, | |
| 403 | password: str, | |
| 404 | device_id: str = None, | |
| 405 | ollama_host: str = "localhost:11434", | |
| 406 | ollama_model: str = "llama3:latest", | |
| 407 | ollama_prompt: str = None, | |
| 408 | available_models: List[str] = None, | |
| 409 | admin_users: List[str] = None, | |
| 410 | opensearch_config: Dict[str, Any] = None, | |
| 411 | ssl: bool = True, | |
| 412 | enable_search: bool = True, | |
| 413 | enable_conversation_tracking: bool = True, | |
| 414 | max_context_length: int = 10, | |
| 415 | max_conversation_history: int = 100, | |
| 416 | respond_to_all_messages: bool = False | |
| 417 | ) -> Optional[EnhancedMatrixClient]: | |
| 418 | """ | |
| 419 | Connect to a Matrix homeserver with enhanced features. | |
| 420 | ||
| 421 | Args: | |
| 422 | homeserver: The URL of the Matrix homeserver | |
| 423 | user_id: The Matrix user ID (e.g., @user:example.com) | |
| 424 | password: The password for the Matrix account | |
| 425 | device_id: A unique identifier for this device (for E2E encryption) | |
| 426 | ollama_host: The host address for the Ollama API | |
| 427 | ollama_model: The Ollama model to use for generating responses | |
| 428 | ollama_prompt: An optional system prompt for the Ollama model | |
| 429 | available_models: List of available Ollama models | |
| 430 | admin_users: List of Matrix user IDs that can change models for all users | |
| 431 | opensearch_config: Configuration for OpenSearchNG | |
| 432 | ssl: Whether to use SSL for the Matrix connection | |
| 433 | enable_search: Whether to enable search functionality | |
| 434 | enable_conversation_tracking: Whether to enable conversation tracking | |
| 435 | max_context_length: Maximum number of messages to keep in context | |
| 436 | max_conversation_history: Maximum number of conversations to keep in history | |
| 437 | respond_to_all_messages: Whether to respond to all messages (not just mentions) | |
| 438 | ||
| 439 | Returns: | |
| 440 | Optional[EnhancedMatrixClient]: A connected Enhanced Matrix client, or None if connection failed | |
| 441 | """ | |
| 442 | client = EnhancedMatrixClient( | |
| 443 | homeserver=homeserver, | |
| 444 | user_id=user_id, | |
| 445 | password=password, | |
| 446 | device_id=device_id, | |
| 447 | ollama_host=ollama_host, | |
| 448 | ollama_model=ollama_model, | |
| 449 | ollama_prompt=ollama_prompt, | |
| 450 | available_models=available_models, | |
| 451 | admin_users=admin_users, | |
| 452 | opensearch_config=opensearch_config, | |
| 453 | ssl=ssl, | |
| 454 | enable_search=enable_search, | |
| 455 | enable_conversation_tracking=enable_conversation_tracking, | |
| 456 | max_context_length=max_context_length, | |
| 457 | max_conversation_history=max_conversation_history, | |
| 458 | respond_to_all_messages=respond_to_all_messages | |
| 459 | ) | |
| 460 | ||
| 461 | success = await client.connect() | |
| 462 | ||
| 463 | if success: | |
| 464 | return client | |
| 465 | else: | |
| 466 | return None |
| … | … | @@ -0,0 +1,329 @@ |
| 1 | """ | |
| 2 | Conversation history and analytics for the Enhanced Matrix integration in OllamaTools. | |
| 3 | ||
| 4 | This module provides functionality for tracking conversation history, | |
| 5 | analyzing topics, and generating performance metrics for the Enhanced Matrix client. | |
| 6 | ||
| 7 | Dependencies: | |
| 8 | - matrix-nio: For Matrix protocol support | |
| 9 | - ollama: For Ollama API access | |
| 10 | """ | |
| 11 | ||
| 12 | import os | |
| 13 | import json | |
| 14 | import time | |
| 15 | import logging | |
| 16 | from typing import Dict, List, Optional, Any, Set, Tuple | |
| 17 | from collections import defaultdict | |
| 18 | ||
| 19 | # Configure logging | |
| 20 | logger = logging.getLogger(__name__) | |
| 21 | ||
| 22 | def load_conversation_history(history_file: str) -> List[Dict[str, Any]]: | |
| 23 | """ | |
| 24 | Load conversation history from file. | |
| 25 | ||
| 26 | Args: | |
| 27 | history_file: Path to the history file | |
| 28 | ||
| 29 | Returns: | |
| 30 | List[Dict[str, Any]]: The conversation history | |
| 31 | """ | |
| 32 | if os.path.exists(history_file): | |
| 33 | try: | |
| 34 | with open(history_file, 'r') as f: | |
| 35 | history = json.load(f) | |
| 36 | logger.info(f"Loaded {len(history)} conversations from history") | |
| 37 | return history | |
| 38 | except Exception as e: | |
| 39 | logger.error(f"Failed to load conversation history: {str(e)}") | |
| 40 | return [] | |
| 41 | else: | |
| 42 | logger.info("No conversation history file found") | |
| 43 | return [] | |
| 44 | ||
| 45 | def save_conversation_history(history: List[Dict[str, Any]], history_file: str) -> None: | |
| 46 | """ | |
| 47 | Save conversation history to file. | |
| 48 | ||
| 49 | Args: | |
| 50 | history: The conversation history | |
| 51 | history_file: Path to save the history to | |
| 52 | """ | |
| 53 | try: | |
| 54 | # Create directory if it doesn't exist | |
| 55 | os.makedirs(os.path.dirname(history_file), exist_ok=True) | |
| 56 | ||
| 57 | with open(history_file, 'w') as f: | |
| 58 | json.dump(history, f, indent=2) | |
| 59 | logger.debug(f"Saved {len(history)} conversations to history") | |
| 60 | except Exception as e: | |
| 61 | logger.error(f"Failed to save conversation history: {str(e)}") | |
| 62 | ||
| 63 | def record_conversation( | |
| 64 | history: List[Dict[str, Any]], | |
| 65 | user_message: str, | |
| 66 | assistant_response: str, | |
| 67 | sender: str, | |
| 68 | room_id: str, | |
| 69 | response_time: float, | |
| 70 | history_file: str, | |
| 71 | max_history: int = 100 | |
| 72 | ) -> None: | |
| 73 | """ | |
| 74 | Record a conversation in the history. | |
| 75 | ||
| 76 | Args: | |
| 77 | history: The conversation history list to update | |
| 78 | user_message: The user's message | |
| 79 | assistant_response: The assistant's response | |
| 80 | sender: The Matrix user ID of the sender | |
| 81 | room_id: The ID of the room where the conversation took place | |
| 82 | response_time: The time taken to generate the response | |
| 83 | history_file: Path to save the history to | |
| 84 | max_history: Maximum number of conversations to keep in history | |
| 85 | """ | |
| 86 | # Create a conversation record | |
| 87 | conversation = { | |
| 88 | "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), | |
| 89 | "user_message": user_message, | |
| 90 | "assistant_response": assistant_response, | |
| 91 | "sender": sender, | |
| 92 | "room_id": room_id, | |
| 93 | "response_time": response_time, | |
| 94 | "tokens_estimated": len(user_message.split()) + len(assistant_response.split()) | |
| 95 | } | |
| 96 | ||
| 97 | # Add to conversation history | |
| 98 | history.append(conversation) | |
| 99 | ||
| 100 | # Trim conversation history if it gets too long | |
| 101 | if len(history) > max_history: | |
| 102 | history = history[-max_history:] | |
| 103 | ||
| 104 | # Save conversation history | |
| 105 | save_conversation_history(history, history_file) | |
| 106 | ||
| 107 | def analyze_conversation_topics(conversations: List[Dict[str, Any]]) -> Dict[str, int]: | |
| 108 | """ | |
| 109 | Analyze common topics in conversation history. | |
| 110 | ||
| 111 | Args: | |
| 112 | conversations: List of conversation records | |
| 113 | ||
| 114 | Returns: | |
| 115 | Dict[str, int]: Dictionary of topics and their frequency | |
| 116 | """ | |
| 117 | if not conversations: | |
| 118 | return {} | |
| 119 | ||
| 120 | # Simple keyword-based topic analysis | |
| 121 | topics = { | |
| 122 | "help": 0, | |
| 123 | "search": 0, | |
| 124 | "file": 0, | |
| 125 | "command": 0, | |
| 126 | "code": 0, | |
| 127 | "error": 0, | |
| 128 | "issue": 0, | |
| 129 | "question": 0, | |
| 130 | "other": 0 | |
| 131 | } | |
| 132 | ||
| 133 | for conv in conversations: | |
| 134 | message = conv.get("user_message", "").lower() | |
| 135 | ||
| 136 | # Check for keywords | |
| 137 | if "help" in message or "how" in message: | |
| 138 | topics["help"] += 1 | |
| 139 | if "search" in message or "find" in message or "look" in message: | |
| 140 | topics["search"] += 1 | |
| 141 | if "file" in message or "read" in message or "write" in message: | |
| 142 | topics["file"] += 1 | |
| 143 | if "command" in message or "run" in message or "execute" in message: | |
| 144 | topics["command"] += 1 | |
| 145 | if "code" in message or "program" in message or "script" in message: | |
| 146 | topics["code"] += 1 | |
| 147 | if "error" in message or "bug" in message or "fix" in message: | |
| 148 | topics["error"] += 1 | |
| 149 | if "issue" in message or "problem" in message: | |
| 150 | topics["issue"] += 1 | |
| 151 | if "?" in message: | |
| 152 | topics["question"] += 1 | |
| 153 | ||
| 154 | # If no keywords matched, count as other | |
| 155 | if not any(count > 0 for count in topics.values()): | |
| 156 | topics["other"] += 1 | |
| 157 | ||
| 158 | # Remove topics with zero count | |
| 159 | return {k: v for k, v in topics.items() if v > 0} | |
| 160 | ||
| 161 | def get_performance_metrics(conversations: List[Dict[str, Any]]) -> Dict[str, Any]: | |
| 162 | """ | |
| 163 | Get performance metrics from conversation history. | |
| 164 | ||
| 165 | Args: | |
| 166 | conversations: List of conversation records | |
| 167 | ||
| 168 | Returns: | |
| 169 | Dict[str, Any]: Performance metrics | |
| 170 | """ | |
| 171 | if not conversations: | |
| 172 | return {"error": "No conversation history available"} | |
| 173 | ||
| 174 | # Calculate metrics | |
| 175 | total_conversations = len(conversations) | |
| 176 | total_response_time = sum(conv.get("response_time", 0) for conv in conversations) | |
| 177 | avg_response_time = total_response_time / total_conversations if total_conversations > 0 else 0 | |
| 178 | ||
| 179 | # Calculate token usage | |
| 180 | total_tokens = sum(conv.get("tokens_estimated", 0) for conv in conversations) | |
| 181 | avg_tokens_per_conversation = total_tokens / total_conversations if total_conversations > 0 else 0 | |
| 182 | ||
| 183 | # Find slowest and fastest responses | |
| 184 | slowest = max(conversations, key=lambda x: x.get("response_time", 0)) if conversations else None | |
| 185 | fastest = min(conversations, key=lambda x: x.get("response_time", 0)) if conversations else None | |
| 186 | ||
| 187 | return { | |
| 188 | "total_conversations": total_conversations, | |
| 189 | "total_response_time": total_response_time, | |
| 190 | "avg_response_time": avg_response_time, | |
| 191 | "total_tokens_estimated": total_tokens, | |
| 192 | "avg_tokens_per_conversation": avg_tokens_per_conversation, | |
| 193 | "slowest_response": { | |
| 194 | "time": slowest.get("response_time", 0) if slowest else 0, | |
| 195 | "message": slowest.get("user_message", "")[:50] + "..." if slowest and len(slowest.get("user_message", "")) > 50 else slowest.get("user_message", "") if slowest else "" | |
| 196 | }, | |
| 197 | "fastest_response": { | |
| 198 | "time": fastest.get("response_time", 0) if fastest else 0, | |
| 199 | "message": fastest.get("user_message", "")[:50] + "..." if fastest and len(fastest.get("user_message", "")) > 50 else fastest.get("user_message", "") if fastest else "" | |
| 200 | } | |
| 201 | } | |
| 202 | ||
| 203 | async def handle_history_command(client: Any, count: int, room_id: str) -> None: | |
| 204 | """ | |
| 205 | Handle the /history command. | |
| 206 | ||
| 207 | Args: | |
| 208 | client: The EnhancedMatrixClient instance | |
| 209 | count: Number of recent conversations to show | |
| 210 | room_id: The ID of the room where the command was sent | |
| 211 | """ | |
| 212 | if not client.enable_conversation_tracking: | |
| 213 | await client.send_message(room_id, "Conversation tracking is not enabled.") | |
| 214 | return | |
| 215 | ||
| 216 | if not client.conversation_history: | |
| 217 | await client.send_message(room_id, "No conversation history available.") | |
| 218 | return | |
| 219 | ||
| 220 | # Get the most recent conversations for this room | |
| 221 | room_history = [conv for conv in client.conversation_history if conv.get("room_id") == room_id] | |
| 222 | ||
| 223 | if not room_history: | |
| 224 | await client.send_message(room_id, "No conversation history available for this room.") | |
| 225 | return | |
| 226 | ||
| 227 | # Get the most recent conversations | |
| 228 | recent = room_history[-count:] if len(room_history) >= count else room_history | |
| 229 | ||
| 230 | # Format the history | |
| 231 | formatted_history = ["Recent Conversations:"] | |
| 232 | ||
| 233 | for i, conv in enumerate(recent, 1): | |
| 234 | timestamp = conv.get("timestamp", "Unknown time") | |
| 235 | user_message = conv.get("user_message", "No message") | |
| 236 | sender = conv.get("sender", "Unknown sender") | |
| 237 | ||
| 238 | # Truncate message if it's too long | |
| 239 | if len(user_message) > 100: | |
| 240 | user_message = user_message[:100] + "..." | |
| 241 | ||
| 242 | formatted_history.append(f"{i}. {timestamp}") | |
| 243 | formatted_history.append(f" From: {sender}") | |
| 244 | formatted_history.append(f" Message: {user_message}") | |
| 245 | formatted_history.append("") | |
| 246 | ||
| 247 | # Send the formatted history | |
| 248 | await client.send_message(room_id, "\n".join(formatted_history)) | |
| 249 | ||
| 250 | async def handle_topics_command(client: Any, room_id: str) -> None: | |
| 251 | """ | |
| 252 | Handle the /topics command. | |
| 253 | ||
| 254 | Args: | |
| 255 | client: The EnhancedMatrixClient instance | |
| 256 | room_id: The ID of the room where the command was sent | |
| 257 | """ | |
| 258 | if not client.enable_conversation_tracking: | |
| 259 | await client.send_message(room_id, "Conversation tracking is not enabled.") | |
| 260 | return | |
| 261 | ||
| 262 | if not client.conversation_history: | |
| 263 | await client.send_message(room_id, "No conversation history available.") | |
| 264 | return | |
| 265 | ||
| 266 | # Get the conversations for this room | |
| 267 | room_history = [conv for conv in client.conversation_history if conv.get("room_id") == room_id] | |
| 268 | ||
| 269 | if not room_history: | |
| 270 | await client.send_message(room_id, "No conversation history available for this room.") | |
| 271 | return | |
| 272 | ||
| 273 | # Analyze topics | |
| 274 | topics = analyze_conversation_topics(room_history) | |
| 275 | ||
| 276 | if not topics: | |
| 277 | await client.send_message(room_id, "No topics found in conversation history.") | |
| 278 | return | |
| 279 | ||
| 280 | # Format the topics | |
| 281 | formatted_topics = ["Conversation Topics:"] | |
| 282 | ||
| 283 | for topic, count in sorted(topics.items(), key=lambda x: x[1], reverse=True): | |
| 284 | formatted_topics.append(f"- {topic}: {count} mentions") | |
| 285 | ||
| 286 | # Send the formatted topics | |
| 287 | await client.send_message(room_id, "\n".join(formatted_topics)) | |
| 288 | ||
| 289 | async def handle_metrics_command(client: Any, room_id: str) -> None: | |
| 290 | """ | |
| 291 | Handle the /metrics command. | |
| 292 | ||
| 293 | Args: | |
| 294 | client: The EnhancedMatrixClient instance | |
| 295 | room_id: The ID of the room where the command was sent | |
| 296 | """ | |
| 297 | if not client.enable_conversation_tracking: | |
| 298 | await client.send_message(room_id, "Conversation tracking is not enabled.") | |
| 299 | return | |
| 300 | ||
| 301 | if not client.conversation_history: | |
| 302 | await client.send_message(room_id, "No conversation history available.") | |
| 303 | return | |
| 304 | ||
| 305 | # Get the conversations for this room | |
| 306 | room_history = [conv for conv in client.conversation_history if conv.get("room_id") == room_id] | |
| 307 | ||
| 308 | if not room_history: | |
| 309 | await client.send_message(room_id, "No conversation history available for this room.") | |
| 310 | return | |
| 311 | ||
| 312 | # Calculate metrics | |
| 313 | metrics = get_performance_metrics(room_history) | |
| 314 | ||
| 315 | # Format the metrics | |
| 316 | formatted_metrics = ["Performance Metrics:"] | |
| 317 | formatted_metrics.append(f"- Total conversations: {metrics['total_conversations']}") | |
| 318 | formatted_metrics.append(f"- Average response time: {metrics['avg_response_time']:.2f}s") | |
| 319 | formatted_metrics.append(f"- Estimated total tokens: {metrics['total_tokens_estimated']}") | |
| 320 | formatted_metrics.append(f"- Average tokens per conversation: {metrics['avg_tokens_per_conversation']:.1f}") | |
| 321 | ||
| 322 | if metrics.get("slowest_response"): | |
| 323 | formatted_metrics.append(f"- Slowest response: {metrics['slowest_response']['time']:.2f}s for '{metrics['slowest_response']['message']}'") | |
| 324 | ||
| 325 | if metrics.get("fastest_response"): | |
| 326 | formatted_metrics.append(f"- Fastest response: {metrics['fastest_response']['time']:.2f}s for '{metrics['fastest_response']['message']}'") | |
| 327 | ||
| 328 | # Send the formatted metrics | |
| 329 | await client.send_message(room_id, "\n".join(formatted_metrics)) |
| … | … | @@ -0,0 +1,170 @@ |
| 1 | """ | |
| 2 | Search functionality for the Enhanced Matrix integration in OllamaTools. | |
| 3 | ||
| 4 | This module provides search-related functionality for the Enhanced Matrix client, | |
| 5 | including integration with OpenSearchNG and the modular agent's search capabilities. | |
| 6 | ||
| 7 | Dependencies: | |
| 8 | - matrix-nio: For Matrix protocol support | |
| 9 | - ollama: For Ollama API access | |
| 10 | - ollamatools.agent.modular: For search integration and natural language understanding | |
| 11 | - ollamatools.opensearch_web_tools: For web search functionality | |
| 12 | """ | |
| 13 | ||
| 14 | import logging | |
| 15 | from typing import Dict, List, Optional, Any, Union | |
| 16 | ||
| 17 | # Configure logging | |
| 18 | logger = logging.getLogger(__name__) | |
| 19 | ||
| 20 | # Import from modular agent (optional) | |
| 21 | try: | |
| 22 | from ollamatools.agent.modular.search import ( | |
| 23 | detect_factual_intent, | |
| 24 | extract_search_context, | |
| 25 | execute_search | |
| 26 | ) | |
| 27 | MODULAR_AGENT_AVAILABLE = True | |
| 28 | except ImportError: | |
| 29 | MODULAR_AGENT_AVAILABLE = False | |
| 30 | # Only log this as debug since search might be disabled in config | |
| 31 | logging.debug( | |
| 32 | "The modular agent components are not available. " | |
| 33 | "Search functionality will be disabled." | |
| 34 | ) | |
| 35 | ||
| 36 | # Import from opensearch web tools (optional) | |
| 37 | try: | |
| 38 | from ollamatools.opensearch_web_tools import ( | |
| 39 | opensearch_query, | |
| 40 | opensearch_fetch_url, | |
| 41 | get_opensearch_info, | |
| 42 | DEFAULT_OPENSEARCH_CONFIG | |
| 43 | ) | |
| 44 | OPENSEARCH_AVAILABLE = True | |
| 45 | except ImportError: | |
| 46 | OPENSEARCH_AVAILABLE = False | |
| 47 | # Only log this as debug since search might be disabled in config | |
| 48 | logging.debug( | |
| 49 | "The opensearch_web_tools module is not available. " | |
| 50 | "Search functionality will be disabled." | |
| 51 | ) | |
| 52 | ||
| 53 | def get_default_opensearch_config() -> Dict[str, Any]: | |
| 54 | """ | |
| 55 | Get the default OpenSearchNG configuration. | |
| 56 | ||
| 57 | Returns: | |
| 58 | Dict[str, Any]: The default configuration | |
| 59 | """ | |
| 60 | if OPENSEARCH_AVAILABLE: | |
| 61 | return DEFAULT_OPENSEARCH_CONFIG.copy() | |
| 62 | else: | |
| 63 | return {} | |
| 64 | ||
| 65 | async def execute_search_for_message(message: str, opensearch_config: Dict[str, Any]) -> str: | |
| 66 | """ | |
| 67 | Execute a search for a message and return the search context. | |
| 68 | ||
| 69 | Args: | |
| 70 | message: The message to search for | |
| 71 | opensearch_config: The OpenSearchNG configuration | |
| 72 | ||
| 73 | Returns: | |
| 74 | str: The search context, or an empty string if search failed | |
| 75 | """ | |
| 76 | if not MODULAR_AGENT_AVAILABLE or not OPENSEARCH_AVAILABLE: | |
| 77 | logger.warning("Search functionality is not available.") | |
| 78 | return "" | |
| 79 | ||
| 80 | try: | |
| 81 | factual_intent = detect_factual_intent(message) | |
| 82 | if factual_intent: | |
| 83 | logger.info(f"Factual intent detected in message: {message}") | |
| 84 | try: | |
| 85 | results = await execute_search_query(message, opensearch_config) | |
| 86 | search_context = extract_search_context(results) | |
| 87 | logger.info(f"Search context: {search_context[:100]}...") | |
| 88 | return search_context | |
| 89 | except Exception as e: | |
| 90 | logger.error(f"Error executing search: {e}") | |
| 91 | return "" | |
| 92 | else: | |
| 93 | logger.debug(f"No factual intent detected in message: {message}") | |
| 94 | return "" | |
| 95 | except Exception as e: | |
| 96 | logger.error(f"Error detecting factual intent: {e}") | |
| 97 | return "" | |
| 98 | ||
| 99 | async def execute_search_query(query: str, opensearch_config: Dict[str, Any]) -> Dict[str, Any]: | |
| 100 | """ | |
| 101 | Execute a search query using OpenSearchNG. | |
| 102 | ||
| 103 | Args: | |
| 104 | query: The search query | |
| 105 | opensearch_config: The OpenSearchNG configuration | |
| 106 | ||
| 107 | Returns: | |
| 108 | Dict[str, Any]: Search results | |
| 109 | """ | |
| 110 | if not OPENSEARCH_AVAILABLE: | |
| 111 | logger.warning("OpenSearchNG is not available.") | |
| 112 | return {"error": "Search functionality is not available.", "results": []} | |
| 113 | ||
| 114 | try: | |
| 115 | # Execute the search | |
| 116 | results = opensearch_query( | |
| 117 | query=query, | |
| 118 | config=opensearch_config | |
| 119 | ) | |
| 120 | ||
| 121 | return results | |
| 122 | except Exception as e: | |
| 123 | logger.error(f"Error executing search: {e}") | |
| 124 | return {"error": str(e), "results": []} | |
| 125 | ||
| 126 | async def handle_search_command(client: Any, query: str, room_id: str) -> None: | |
| 127 | """ | |
| 128 | Handle the /search command. | |
| 129 | ||
| 130 | Args: | |
| 131 | client: The EnhancedMatrixClient instance | |
| 132 | query: The search query | |
| 133 | room_id: The ID of the room where the command was sent | |
| 134 | """ | |
| 135 | if not client.enable_search: | |
| 136 | await client.send_message(room_id, "Search functionality is not enabled.") | |
| 137 | return | |
| 138 | ||
| 139 | await client.send_message(room_id, f"Searching for: {query}...") | |
| 140 | ||
| 141 | try: | |
| 142 | results = await execute_search_query(query, client.opensearch_config) | |
| 143 | ||
| 144 | if not results or not results.get("results"): | |
| 145 | await client.send_message(room_id, f"No results found for: {query}") | |
| 146 | return | |
| 147 | ||
| 148 | # Format the results | |
| 149 | formatted_results = ["Search Results:"] | |
| 150 | ||
| 151 | for i, result in enumerate(results["results"][:5], 1): | |
| 152 | title = result.get("title", "No title") | |
| 153 | url = result.get("url", "No URL") | |
| 154 | content = result.get("content", "No content") | |
| 155 | ||
| 156 | # Truncate content if it's too long | |
| 157 | if len(content) > 200: | |
| 158 | content = content[:200] + "..." | |
| 159 | ||
| 160 | formatted_results.append(f"{i}. {title}") | |
| 161 | formatted_results.append(f" URL: {url}") | |
| 162 | formatted_results.append(f" {content}") | |
| 163 | formatted_results.append("") | |
| 164 | ||
| 165 | # Send the formatted results | |
| 166 | await client.send_message(room_id, "\n".join(formatted_results)) | |
| 167 | ||
| 168 | except Exception as e: | |
| 169 | logger.error(f"Error executing search: {e}") | |
| 170 | await client.send_message(room_id, f"Error executing search: {str(e)}") |
| … | … | @@ -0,0 +1,226 @@ |
| 1 | """ | |
| 2 | Enhanced Matrix integration tools for OllamaTools. | |
| 3 | ||
| 4 | This module extends the basic Matrix integration with advanced features from | |
| 5 | the modular and extended agents, including: | |
| 6 | - Improved message response logic | |
| 7 | - Natural language understanding | |
| 8 | - Search integration | |
| 9 | - Conversation history tracking and summarization | |
| 10 | - Topic analysis | |
| 11 | - Performance metrics | |
| 12 | ||
| 13 | Dependencies: | |
| 14 | - matrix-nio: For Matrix protocol support | |
| 15 | - matrix-nio[e2e]: For end-to-end encryption support (optional) | |
| 16 | - pyyaml: For configuration handling | |
| 17 | - ollama: For Ollama API access | |
| 18 | """ | |
| 19 | ||
| 20 | import os | |
| 21 | import sys | |
| 22 | import yaml | |
| 23 | import json | |
| 24 | import asyncio | |
| 25 | import logging | |
| 26 | from typing import Dict, List, Optional, Union, Callable, Any, Set, Tuple | |
| 27 | ||
| 28 | # Import from core module | |
| 29 | from ollamatools.tools.enhanced_matrix_core import ( | |
| 30 | EnhancedMatrixClient, | |
| 31 | _connect_enhanced_matrix | |
| 32 | ) | |
| 33 | ||
| 34 | # Import from search module | |
| 35 | from ollamatools.tools.enhanced_matrix_search import ( | |
| 36 | MODULAR_AGENT_AVAILABLE, | |
| 37 | OPENSEARCH_AVAILABLE, | |
| 38 | get_default_opensearch_config, | |
| 39 | execute_search_for_message, | |
| 40 | execute_search_query, | |
| 41 | handle_search_command | |
| 42 | ) | |
| 43 | ||
| 44 | # Import from history module | |
| 45 | from ollamatools.tools.enhanced_matrix_history import ( | |
| 46 | load_conversation_history, | |
| 47 | save_conversation_history, | |
| 48 | record_conversation, | |
| 49 | analyze_conversation_topics, | |
| 50 | get_performance_metrics, | |
| 51 | handle_history_command, | |
| 52 | handle_topics_command, | |
| 53 | handle_metrics_command | |
| 54 | ) | |
| 55 | ||
| 56 | # Import from base matrix tools | |
| 57 | from ollamatools.tools.matrix_tools import ( | |
| 58 | MatrixConfig, | |
| 59 | load_matrix_config, | |
| 60 | save_matrix_config, | |
| 61 | create_default_matrix_config, | |
| 62 | RoomMessageText | |
| 63 | ) | |
| 64 | ||
| 65 | # Configure logging | |
| 66 | logger = logging.getLogger(__name__) | |
| 67 | ||
| 68 | # Synchronous wrapper functions for the module API | |
| 69 | ||
| 70 | def connect_enhanced_matrix( | |
| 71 | homeserver: str, | |
| 72 | user_id: str, | |
| 73 | password: str, | |
| 74 | device_id: str = None, | |
| 75 | ollama_host: str = "localhost:11434", | |
| 76 | ollama_model: str = "llama3:latest", | |
| 77 | ollama_prompt: str = None, | |
| 78 | available_models: List[str] = None, | |
| 79 | admin_users: List[str] = None, | |
| 80 | opensearch_config: Dict[str, Any] = None, | |
| 81 | ssl: bool = True, | |
| 82 | enable_search: bool = True, | |
| 83 | enable_conversation_tracking: bool = True, | |
| 84 | max_context_length: int = 10, | |
| 85 | max_conversation_history: int = 100, | |
| 86 | respond_to_all_messages: bool = False | |
| 87 | ) -> Optional[EnhancedMatrixClient]: | |
| 88 | """ | |
| 89 | Connect to a Matrix homeserver with enhanced features. | |
| 90 | ||
| 91 | Args: | |
| 92 | homeserver: The URL of the Matrix homeserver | |
| 93 | user_id: The Matrix user ID (e.g., @user:example.com) | |
| 94 | password: The password for the Matrix account | |
| 95 | device_id: A unique identifier for this device (for E2E encryption) | |
| 96 | ollama_host: The host address for the Ollama API | |
| 97 | ollama_model: The Ollama model to use for generating responses | |
| 98 | ollama_prompt: An optional system prompt for the Ollama model | |
| 99 | available_models: List of available Ollama models | |
| 100 | admin_users: List of Matrix user IDs that can change models for all users | |
| 101 | opensearch_config: Configuration for OpenSearchNG | |
| 102 | ssl: Whether to use SSL for the Matrix connection | |
| 103 | enable_search: Whether to enable search functionality | |
| 104 | enable_conversation_tracking: Whether to enable conversation tracking | |
| 105 | max_context_length: Maximum number of messages to keep in context | |
| 106 | max_conversation_history: Maximum number of conversations to keep in history | |
| 107 | respond_to_all_messages: Whether to respond to all messages (not just mentions) | |
| 108 | ||
| 109 | Returns: | |
| 110 | Optional[EnhancedMatrixClient]: A connected Enhanced Matrix client, or None if connection failed | |
| 111 | """ | |
| 112 | return asyncio.run(_connect_enhanced_matrix( | |
| 113 | homeserver=homeserver, | |
| 114 | user_id=user_id, | |
| 115 | password=password, | |
| 116 | device_id=device_id, | |
| 117 | ollama_host=ollama_host, | |
| 118 | ollama_model=ollama_model, | |
| 119 | ollama_prompt=ollama_prompt, | |
| 120 | available_models=available_models, | |
| 121 | admin_users=admin_users, | |
| 122 | opensearch_config=opensearch_config, | |
| 123 | ssl=ssl, | |
| 124 | enable_search=enable_search, | |
| 125 | enable_conversation_tracking=enable_conversation_tracking, | |
| 126 | max_context_length=max_context_length, | |
| 127 | max_conversation_history=max_conversation_history, | |
| 128 | respond_to_all_messages=respond_to_all_messages | |
| 129 | )) | |
| 130 | ||
| 131 | def join_enhanced_matrix_room(client: EnhancedMatrixClient, room_id: str) -> bool: | |
| 132 | """ | |
| 133 | Join a Matrix room with an Enhanced Matrix client. | |
| 134 | ||
| 135 | Args: | |
| 136 | client: A connected Enhanced Matrix client | |
| 137 | room_id: The ID of the room to join (e.g., #room:example.com) | |
| 138 | ||
| 139 | Returns: | |
| 140 | bool: True if joining was successful, False otherwise | |
| 141 | """ | |
| 142 | return asyncio.run(client.join_room(room_id)) | |
| 143 | ||
| 144 | def send_enhanced_matrix_message(client: EnhancedMatrixClient, room_id: str, message: str) -> bool: | |
| 145 | """ | |
| 146 | Send a message to a Matrix room with an Enhanced Matrix client. | |
| 147 | ||
| 148 | Args: | |
| 149 | client: A connected Enhanced Matrix client | |
| 150 | room_id: The ID of the room to send the message to | |
| 151 | message: The message to send | |
| 152 | ||
| 153 | Returns: | |
| 154 | bool: True if sending was successful, False otherwise | |
| 155 | """ | |
| 156 | return asyncio.run(client.send_message(room_id, message)) | |
| 157 | ||
| 158 | def listen_for_enhanced_matrix_messages(client: EnhancedMatrixClient) -> None: | |
| 159 | """ | |
| 160 | Start listening for messages in joined rooms with an Enhanced Matrix client. | |
| 161 | ||
| 162 | This function will block until the client is stopped. | |
| 163 | ||
| 164 | Args: | |
| 165 | client: A connected Enhanced Matrix client | |
| 166 | """ | |
| 167 | asyncio.run(client.listen()) | |
| 168 | ||
| 169 | def create_enhanced_matrix_config() -> Dict: | |
| 170 | """ | |
| 171 | Create a default Enhanced Matrix configuration. | |
| 172 | ||
| 173 | Returns: | |
| 174 | Dict: The default configuration | |
| 175 | """ | |
| 176 | config = create_default_matrix_config() | |
| 177 | ||
| 178 | # Add enhanced configuration | |
| 179 | config["ENHANCED"] = { | |
| 180 | "ENABLE_SEARCH": True, | |
| 181 | "ENABLE_CONVERSATION_TRACKING": True, | |
| 182 | "MAX_CONTEXT_LENGTH": 10, | |
| 183 | "MAX_CONVERSATION_HISTORY": 100, | |
| 184 | "RESPOND_TO_ALL_MESSAGES": False, | |
| 185 | "OPENSEARCH": { | |
| 186 | "BASE_URL": "http://localhost:8081", | |
| 187 | "ENGINES": [ | |
| 188 | "google", | |
| 189 | "bing", | |
| 190 | "duckduckgo", | |
| 191 | "brave" | |
| 192 | ], | |
| 193 | "NUM_RESULTS": 5 | |
| 194 | } | |
| 195 | } | |
| 196 | ||
| 197 | return config | |
| 198 | ||
| 199 | def disconnect_enhanced_matrix(client: EnhancedMatrixClient) -> None: | |
| 200 | """ | |
| 201 | Disconnect from the Matrix homeserver and clean up resources. | |
| 202 | ||
| 203 | Args: | |
| 204 | client: A connected Enhanced Matrix client | |
| 205 | """ | |
| 206 | asyncio.run(client.disconnect()) | |
| 207 | ||
| 208 | def process_with_enhanced_ollama( | |
| 209 | client: EnhancedMatrixClient, | |
| 210 | message: str, | |
| 211 | sender: str = None, | |
| 212 | room_id: str = None | |
| 213 | ) -> str: | |
| 214 | """ | |
| 215 | Process a message with Ollama using an Enhanced Matrix client. | |
| 216 | ||
| 217 | Args: | |
| 218 | client: A connected Enhanced Matrix client | |
| 219 | message: The message to process | |
| 220 | sender: The Matrix user ID of the sender | |
| 221 | room_id: The ID of the room where the message was sent | |
| 222 | ||
| 223 | Returns: | |
| 224 | str: The response from Ollama | |
| 225 | """ | |
| 226 | return asyncio.run(client.process_with_ollama(message, sender, room_id)) |
| … | … | @@ -0,0 +1,800 @@ |
| 1 | """ | |
| 2 | Matrix integration tools for OllamaTools. | |
| 3 | ||
| 4 | This module provides functionality to connect to Matrix homeservers, | |
| 5 | join rooms, send messages, and listen for incoming messages. | |
| 6 | It integrates with Ollama to process messages and generate responses. | |
| 7 | ||
| 8 | Dependencies: | |
| 9 | - matrix-nio: For Matrix protocol support | |
| 10 | - matrix-nio[e2e]: For end-to-end encryption support (optional) | |
| 11 | - pyyaml: For configuration handling | |
| 12 | """ | |
| 13 | ||
| 14 | import os | |
| 15 | import sys | |
| 16 | import yaml | |
| 17 | import logging | |
| 18 | import asyncio | |
| 19 | import re | |
| 20 | from typing import Dict, List, Optional, Union, Callable, Any, Set | |
| 21 | from collections import defaultdict | |
| 22 | ||
| 23 | # Configure logging | |
| 24 | logger = logging.getLogger(__name__) | |
| 25 | ||
| 26 | try: | |
| 27 | from nio import ( | |
| 28 | AsyncClient, | |
| 29 | MatrixRoom, | |
| 30 | RoomMessageText, | |
| 31 | LoginResponse, | |
| 32 | JoinError | |
| 33 | ) | |
| 34 | except ImportError: | |
| 35 | raise ImportError( | |
| 36 | "Matrix support requires the 'matrix-nio' package. " | |
| 37 | "Please install it with 'pip install matrix-nio' or " | |
| 38 | "'pip install matrix-nio[e2e]' for end-to-end encryption support." | |
| 39 | ) | |
| 40 | ||
| 41 | # Make ollama import optional | |
| 42 | try: | |
| 43 | from ollama import AsyncClient as OllamaAsync | |
| 44 | OLLAMA_AVAILABLE = True | |
| 45 | except ImportError: | |
| 46 | OLLAMA_AVAILABLE = False | |
| 47 | logger.warning( | |
| 48 | "The 'ollama' package is not installed. " | |
| 49 | "Message processing with Ollama will not be available. " | |
| 50 | "Install it with 'pip install ollama'." | |
| 51 | ) | |
| 52 | ||
| 53 | class MatrixClient: | |
| 54 | """ | |
| 55 | A client for connecting to Matrix homeservers and interacting with rooms. | |
| 56 | ||
| 57 | This client can connect to a Matrix homeserver, join rooms, send messages, | |
| 58 | and listen for incoming messages. It integrates with Ollama to process | |
| 59 | messages and generate responses. | |
| 60 | """ | |
| 61 | ||
| 62 | # Command patterns | |
| 63 | MODEL_COMMAND_PATTERN = re.compile(r'^/model\s+([a-zA-Z0-9:_\-\.]+)$') | |
| 64 | HELP_COMMAND_PATTERN = re.compile(r'^/help$') | |
| 65 | LIST_MODELS_COMMAND_PATTERN = re.compile(r'^/models$') | |
| 66 | ||
| 67 | def __init__( | |
| 68 | self, | |
| 69 | homeserver: str, | |
| 70 | user_id: str, | |
| 71 | password: str, | |
| 72 | device_id: str = None, | |
| 73 | ollama_host: str = "localhost:11434", | |
| 74 | ollama_model: str = "llama3:latest", | |
| 75 | ollama_prompt: str = None, | |
| 76 | available_models: List[str] = None, | |
| 77 | admin_users: List[str] = None, | |
| 78 | ssl: bool = True | |
| 79 | ): | |
| 80 | """ | |
| 81 | Initialize a new Matrix client. | |
| 82 | ||
| 83 | Args: | |
| 84 | homeserver: The URL of the Matrix homeserver | |
| 85 | user_id: The Matrix user ID (e.g., @user:example.com) | |
| 86 | password: The password for the Matrix account | |
| 87 | device_id: A unique identifier for this device (for E2E encryption) | |
| 88 | ollama_host: The host address for the Ollama API | |
| 89 | ollama_model: The Ollama model to use for generating responses | |
| 90 | ollama_prompt: An optional system prompt for the Ollama model | |
| 91 | available_models: List of available Ollama models | |
| 92 | admin_users: List of Matrix user IDs that can change models for all users | |
| 93 | ssl: Whether to use SSL for the Matrix connection | |
| 94 | """ | |
| 95 | self.homeserver = homeserver | |
| 96 | self.user_id = user_id | |
| 97 | self.password = password | |
| 98 | self.device_id = device_id | |
| 99 | self.ollama_host = ollama_host | |
| 100 | self.ollama_model = ollama_model | |
| 101 | self.ollama_prompt = ollama_prompt | |
| 102 | self.ssl = ssl | |
| 103 | ||
| 104 | # Initialize the Matrix client | |
| 105 | self.client = AsyncClient( | |
| 106 | homeserver=self.homeserver, | |
| 107 | device_id=self.device_id, | |
| 108 | ssl=self.ssl | |
| 109 | ) | |
| 110 | ||
| 111 | # Track when the client connected to avoid processing old messages | |
| 112 | self.connection_time = None | |
| 113 | ||
| 114 | # Track joined rooms | |
| 115 | self.rooms = [] | |
| 116 | ||
| 117 | # Message callback | |
| 118 | self.message_callback = None | |
| 119 | ||
| 120 | # User context tracking | |
| 121 | self.user_contexts = defaultdict(list) | |
| 122 | self.max_context_length = 10 # Maximum number of messages to keep in context | |
| 123 | ||
| 124 | # User model preferences | |
| 125 | self.user_models = {} | |
| 126 | ||
| 127 | # Available models | |
| 128 | self.available_models = available_models or [ollama_model] | |
| 129 | ||
| 130 | # Admin users who can change models | |
| 131 | self.admin_users = set(admin_users or []) | |
| 132 | ||
| 133 | async def connect(self) -> bool: | |
| 134 | """ | |
| 135 | Connect to the Matrix homeserver and log in. | |
| 136 | ||
| 137 | Returns: | |
| 138 | bool: True if the connection was successful, False otherwise | |
| 139 | """ | |
| 140 | try: | |
| 141 | # Extract username from user_id (e.g., "@user:example.com" -> "user") | |
| 142 | username = self.user_id.split(':')[0].lstrip('@') | |
| 143 | ||
| 144 | # Set the user ID for the client | |
| 145 | self.client.user = username | |
| 146 | ||
| 147 | response = await self.client.login( | |
| 148 | password=self.password | |
| 149 | ) | |
| 150 | ||
| 151 | if isinstance(response, LoginResponse): | |
| 152 | logger.info(f"Successfully connected to {self.homeserver} as {self.user_id}") | |
| 153 | self.connection_time = asyncio.get_event_loop().time() | |
| 154 | return True | |
| 155 | else: | |
| 156 | logger.error(f"Failed to connect to {self.homeserver}: {response}") | |
| 157 | return False | |
| 158 | ||
| 159 | except Exception as e: | |
| 160 | logger.error(f"Error connecting to Matrix server: {e}") | |
| 161 | return False | |
| 162 | ||
| 163 | async def join_room(self, room_id: str) -> bool: | |
| 164 | """ | |
| 165 | Join a Matrix room. | |
| 166 | ||
| 167 | Args: | |
| 168 | room_id: The ID of the room to join (e.g., #room:example.com) | |
| 169 | ||
| 170 | Returns: | |
| 171 | bool: True if joining was successful, False otherwise | |
| 172 | """ | |
| 173 | try: | |
| 174 | response = await self.client.join(room_id) | |
| 175 | ||
| 176 | if isinstance(response, JoinError): | |
| 177 | logger.error(f"Failed to join room {room_id}: {response}") | |
| 178 | return False | |
| 179 | ||
| 180 | logger.info(f"Successfully joined room {room_id}") | |
| 181 | self.rooms.append(room_id) | |
| 182 | return True | |
| 183 | ||
| 184 | except Exception as e: | |
| 185 | logger.error(f"Error joining room {room_id}: {e}") | |
| 186 | return False | |
| 187 | ||
| 188 | async def send_message(self, room_id: str, message: str) -> bool: | |
| 189 | """ | |
| 190 | Send a message to a Matrix room. | |
| 191 | ||
| 192 | Args: | |
| 193 | room_id: The ID of the room to send the message to | |
| 194 | message: The message to send | |
| 195 | ||
| 196 | Returns: | |
| 197 | bool: True if sending was successful, False otherwise | |
| 198 | """ | |
| 199 | try: | |
| 200 | response = await self.client.room_send( | |
| 201 | room_id=room_id, | |
| 202 | message_type="m.room.message", | |
| 203 | content={ | |
| 204 | "msgtype": "m.text", | |
| 205 | "body": message | |
| 206 | } | |
| 207 | ) | |
| 208 | ||
| 209 | logger.debug(f"Sent message to room {room_id}") | |
| 210 | return True | |
| 211 | ||
| 212 | except Exception as e: | |
| 213 | logger.error(f"Error sending message to room {room_id}: {e}") | |
| 214 | return False | |
| 215 | ||
| 216 | async def process_with_ollama(self, message: str, sender: str = None, room_id: str = None) -> str: | |
| 217 | """ | |
| 218 | Process a message with Ollama and return the response. | |
| 219 | ||
| 220 | Args: | |
| 221 | message: The message to process | |
| 222 | sender: The Matrix user ID of the sender | |
| 223 | room_id: The ID of the room where the message was sent | |
| 224 | ||
| 225 | Returns: | |
| 226 | str: The response from Ollama | |
| 227 | """ | |
| 228 | if not OLLAMA_AVAILABLE: | |
| 229 | return "Ollama is not available. Please install the 'ollama' package with 'pip install ollama'." | |
| 230 | ||
| 231 | try: | |
| 232 | ollama_client = OllamaAsync(host=self.ollama_host) | |
| 233 | ||
| 234 | # Determine which model to use | |
| 235 | model = self.ollama_model | |
| 236 | if sender and sender in self.user_models: | |
| 237 | model = self.user_models[sender] | |
| 238 | ||
| 239 | messages = [] | |
| 240 | ||
| 241 | # Add system prompt if provided | |
| 242 | if self.ollama_prompt: | |
| 243 | messages.append({ | |
| 244 | "role": "system", | |
| 245 | "content": self.ollama_prompt | |
| 246 | }) | |
| 247 | ||
| 248 | # Add conversation context if available | |
| 249 | if sender and room_id: | |
| 250 | context_key = f"{sender}:{room_id}" | |
| 251 | for ctx_msg in self.user_contexts[context_key]: | |
| 252 | messages.append(ctx_msg) | |
| 253 | ||
| 254 | # Add user message | |
| 255 | user_message = { | |
| 256 | "role": "user", | |
| 257 | "content": message | |
| 258 | } | |
| 259 | messages.append(user_message) | |
| 260 | ||
| 261 | # Store user message in context | |
| 262 | if sender and room_id: | |
| 263 | context_key = f"{sender}:{room_id}" | |
| 264 | self.user_contexts[context_key].append(user_message) | |
| 265 | # Trim context if it gets too long | |
| 266 | if len(self.user_contexts[context_key]) > self.max_context_length: | |
| 267 | self.user_contexts[context_key] = self.user_contexts[context_key][-self.max_context_length:] | |
| 268 | ||
| 269 | response = await ollama_client.chat( | |
| 270 | model=model, | |
| 271 | messages=messages | |
| 272 | ) | |
| 273 | ||
| 274 | response_message = response["message"] | |
| 275 | message_content = response_message["content"] | |
| 276 | ||
| 277 | # Store assistant response in context | |
| 278 | if sender and room_id: | |
| 279 | context_key = f"{sender}:{room_id}" | |
| 280 | self.user_contexts[context_key].append({ | |
| 281 | "role": "assistant", | |
| 282 | "content": message_content | |
| 283 | }) | |
| 284 | # Trim context if it gets too long | |
| 285 | if len(self.user_contexts[context_key]) > self.max_context_length: | |
| 286 | self.user_contexts[context_key] = self.user_contexts[context_key][-self.max_context_length:] | |
| 287 | ||
| 288 | logger.debug(f"Received response from Ollama (model: {model}): {message_content}") | |
| 289 | return message_content | |
| 290 | ||
| 291 | except Exception as e: | |
| 292 | logger.error(f"Error processing message with Ollama: {e}") | |
| 293 | return f"Error processing message: {e}" | |
| 294 | ||
| 295 | async def _message_callback(self, room: MatrixRoom, event: RoomMessageText) -> None: | |
| 296 | """ | |
| 297 | Internal callback for handling incoming messages. | |
| 298 | ||
| 299 | Args: | |
| 300 | room: The room the message was sent in | |
| 301 | event: The message event | |
| 302 | """ | |
| 303 | if not isinstance(event, RoomMessageText): | |
| 304 | return | |
| 305 | ||
| 306 | # Get message timestamp | |
| 307 | message_timestamp = event.server_timestamp / 1000 | |
| 308 | ||
| 309 | # Skip messages from before the client connected | |
| 310 | if self.connection_time and message_timestamp < self.connection_time: | |
| 311 | logger.debug("Skipping old message") | |
| 312 | return | |
| 313 | ||
| 314 | # Skip messages from the bot itself | |
| 315 | if event.sender == self.user_id: | |
| 316 | logger.debug("Skipping message from self") | |
| 317 | return | |
| 318 | ||
| 319 | message = event.body | |
| 320 | room_id = room.room_id | |
| 321 | sender = event.sender | |
| 322 | ||
| 323 | logger.info(f"Received message in room {room_id} from {sender}: {message}") | |
| 324 | ||
| 325 | # If a custom callback is set, use it | |
| 326 | if self.message_callback: | |
| 327 | await self.message_callback(room, event, self) | |
| 328 | return | |
| 329 | ||
| 330 | # Check if this is a command | |
| 331 | if message.startswith('/'): | |
| 332 | await self._handle_command(message, room_id, sender) | |
| 333 | return | |
| 334 | ||
| 335 | # Extract username from user_id (e.g., "@user:example.com" -> "user") | |
| 336 | username = self.user_id.split(':')[0].lstrip('@') | |
| 337 | ||
| 338 | # Check if the message contains the bot's username (case insensitive) | |
| 339 | if username.lower() in message.lower(): | |
| 340 | logger.info(f"Username mention detected in message: {message}") | |
| 341 | # Process with Ollama and send response | |
| 342 | response = await self.process_with_ollama(message, sender, room_id) | |
| 343 | await self.send_message(room_id, response) | |
| 344 | return | |
| 345 | ||
| 346 | # For other messages without username mention, don't automatically respond | |
| 347 | ||
| 348 | async def _handle_command(self, message: str, room_id: str, sender: str) -> None: | |
| 349 | """ | |
| 350 | Handle commands sent to the bot. | |
| 351 | ||
| 352 | Args: | |
| 353 | message: The command message | |
| 354 | room_id: The ID of the room where the command was sent | |
| 355 | sender: The Matrix user ID of the sender | |
| 356 | """ | |
| 357 | # Check if this is a model change command | |
| 358 | model_match = self.MODEL_COMMAND_PATTERN.match(message) | |
| 359 | if model_match: | |
| 360 | model_name = model_match.group(1) | |
| 361 | await self._change_model(model_name, room_id, sender) | |
| 362 | return | |
| 363 | ||
| 364 | # Check if this is a help command | |
| 365 | if self.HELP_COMMAND_PATTERN.match(message): | |
| 366 | await self._send_help(room_id) | |
| 367 | return | |
| 368 | ||
| 369 | # Check if this is a list models command | |
| 370 | if self.LIST_MODELS_COMMAND_PATTERN.match(message): | |
| 371 | await self._list_models(room_id) | |
| 372 | return | |
| 373 | ||
| 374 | # Unknown command | |
| 375 | await self.send_message(room_id, f"Unknown command: {message}\nType /help for available commands.") | |
| 376 | ||
| 377 | async def _change_model(self, model_name: str, room_id: str, sender: str) -> None: | |
| 378 | """ | |
| 379 | Change the Ollama model for a user. | |
| 380 | ||
| 381 | Args: | |
| 382 | model_name: The name of the model to change to | |
| 383 | room_id: The ID of the room where the command was sent | |
| 384 | sender: The Matrix user ID of the sender | |
| 385 | """ | |
| 386 | # Check if the model is available | |
| 387 | if model_name not in self.available_models: | |
| 388 | await self.send_message( | |
| 389 | room_id, | |
| 390 | f"Model '{model_name}' is not available. Available models: {', '.join(self.available_models)}" | |
| 391 | ) | |
| 392 | return | |
| 393 | ||
| 394 | # Change the model for the user | |
| 395 | self.user_models[sender] = model_name | |
| 396 | ||
| 397 | # Clear the user's context | |
| 398 | for key in list(self.user_contexts.keys()): | |
| 399 | if key.startswith(f"{sender}:"): | |
| 400 | self.user_contexts[key] = [] | |
| 401 | ||
| 402 | await self.send_message(room_id, f"Changed model to '{model_name}' for {sender}") | |
| 403 | logger.info(f"Changed model to '{model_name}' for user {sender}") | |
| 404 | ||
| 405 | async def _send_help(self, room_id: str) -> None: | |
| 406 | """ | |
| 407 | Send help information to a room. | |
| 408 | ||
| 409 | Args: | |
| 410 | room_id: The ID of the room to send the help to | |
| 411 | """ | |
| 412 | help_text = """ | |
| 413 | Available commands: | |
| 414 | - /help - Show this help message | |
| 415 | - /models - List available models | |
| 416 | - /model <model_name> - Change to a different model | |
| 417 | ||
| 418 | You can also mention me by my username in your message and I'll respond. | |
| 419 | For example: "Hey {username}, what's the weather like today?" | |
| 420 | """.format(username=self.user_id.split(':')[0].lstrip('@')) | |
| 421 | await self.send_message(room_id, help_text.strip()) | |
| 422 | ||
| 423 | async def _list_models(self, room_id: str) -> None: | |
| 424 | """ | |
| 425 | List available models to a room. | |
| 426 | ||
| 427 | Args: | |
| 428 | room_id: The ID of the room to send the list to | |
| 429 | """ | |
| 430 | models_text = f"Available models: {', '.join(self.available_models)}" | |
| 431 | await self.send_message(room_id, models_text) | |
| 432 | ||
| 433 | async def get_available_models(self) -> List[str]: | |
| 434 | """ | |
| 435 | Get a list of available Ollama models. | |
| 436 | ||
| 437 | Returns: | |
| 438 | List[str]: A list of available model names | |
| 439 | """ | |
| 440 | if not OLLAMA_AVAILABLE: | |
| 441 | return [self.ollama_model] | |
| 442 | ||
| 443 | try: | |
| 444 | ollama_client = OllamaAsync(host=self.ollama_host) | |
| 445 | response = await ollama_client.list() | |
| 446 | models = [model["name"] for model in response["models"]] | |
| 447 | return models | |
| 448 | except Exception as e: | |
| 449 | logger.error(f"Error getting available models: {e}") | |
| 450 | return [self.ollama_model] | |
| 451 | ||
| 452 | async def update_available_models(self) -> None: | |
| 453 | """ | |
| 454 | Update the list of available models from the Ollama server. | |
| 455 | """ | |
| 456 | models = await self.get_available_models() | |
| 457 | self.available_models = models | |
| 458 | logger.info(f"Updated available models: {', '.join(models)}") | |
| 459 | ||
| 460 | def add_admin_user(self, user_id: str) -> None: | |
| 461 | """ | |
| 462 | Add a user to the list of admin users. | |
| 463 | ||
| 464 | Args: | |
| 465 | user_id: The Matrix user ID to add as an admin | |
| 466 | """ | |
| 467 | self.admin_users.add(user_id) | |
| 468 | logger.info(f"Added admin user: {user_id}") | |
| 469 | ||
| 470 | def remove_admin_user(self, user_id: str) -> None: | |
| 471 | """ | |
| 472 | Remove a user from the list of admin users. | |
| 473 | ||
| 474 | Args: | |
| 475 | user_id: The Matrix user ID to remove as an admin | |
| 476 | """ | |
| 477 | if user_id in self.admin_users: | |
| 478 | self.admin_users.remove(user_id) | |
| 479 | logger.info(f"Removed admin user: {user_id}") | |
| 480 | ||
| 481 | def set_message_callback(self, callback: Callable) -> None: | |
| 482 | """ | |
| 483 | Set a custom callback for handling incoming messages. | |
| 484 | ||
| 485 | The callback should be an async function that takes three arguments: | |
| 486 | - room: The MatrixRoom object | |
| 487 | - event: The RoomMessageText event | |
| 488 | - client: This MatrixClient instance | |
| 489 | ||
| 490 | Args: | |
| 491 | callback: The callback function | |
| 492 | """ | |
| 493 | self.message_callback = callback | |
| 494 | ||
| 495 | async def listen(self) -> None: | |
| 496 | """ | |
| 497 | Start listening for messages in joined rooms. | |
| 498 | ||
| 499 | This method will block until the client is stopped. | |
| 500 | """ | |
| 501 | # Set up the message callback | |
| 502 | self.client.add_event_callback(self._message_callback, RoomMessageText) | |
| 503 | ||
| 504 | # Start syncing with the server | |
| 505 | await self.client.sync_forever(timeout=30000, full_state=True) | |
| 506 | ||
| 507 | async def disconnect(self) -> None: | |
| 508 | """ | |
| 509 | Disconnect from the Matrix homeserver. | |
| 510 | """ | |
| 511 | await self.client.logout() | |
| 512 | await self.client.close() | |
| 513 | logger.info(f"Disconnected from {self.homeserver}") | |
| 514 | ||
| 515 | ||
| 516 | class MatrixConfig: | |
| 517 | """ | |
| 518 | Configuration handler for Matrix integration. | |
| 519 | """ | |
| 520 | ||
| 521 | @staticmethod | |
| 522 | def load_config(config_path: str) -> Dict: | |
| 523 | """ | |
| 524 | Load configuration from a YAML file. | |
| 525 | ||
| 526 | Args: | |
| 527 | config_path: Path to the configuration file | |
| 528 | ||
| 529 | Returns: | |
| 530 | Dict: The configuration as a dictionary | |
| 531 | """ | |
| 532 | try: | |
| 533 | with open(config_path, 'r') as f: | |
| 534 | config = yaml.safe_load(f) | |
| 535 | ||
| 536 | logger.info(f"Loaded configuration from {config_path}") | |
| 537 | return config | |
| 538 | ||
| 539 | except Exception as e: | |
| 540 | logger.error(f"Error loading configuration from {config_path}: {e}") | |
| 541 | raise | |
| 542 | ||
| 543 | @staticmethod | |
| 544 | def save_config(config: Dict, config_path: str) -> None: | |
| 545 | """ | |
| 546 | Save configuration to a YAML file. | |
| 547 | ||
| 548 | Args: | |
| 549 | config: The configuration as a dictionary | |
| 550 | config_path: Path to save the configuration to | |
| 551 | """ | |
| 552 | try: | |
| 553 | # Create directory if it doesn't exist | |
| 554 | os.makedirs(os.path.dirname(config_path), exist_ok=True) | |
| 555 | ||
| 556 | with open(config_path, 'w') as f: | |
| 557 | yaml.dump(config, f, default_flow_style=False) | |
| 558 | ||
| 559 | logger.info(f"Saved configuration to {config_path}") | |
| 560 | ||
| 561 | except Exception as e: | |
| 562 | logger.error(f"Error saving configuration to {config_path}: {e}") | |
| 563 | raise | |
| 564 | ||
| 565 | @staticmethod | |
| 566 | def create_default_config() -> Dict: | |
| 567 | """ | |
| 568 | Create a default configuration. | |
| 569 | ||
| 570 | Returns: | |
| 571 | Dict: The default configuration | |
| 572 | """ | |
| 573 | return { | |
| 574 | "MATRIX": { | |
| 575 | "HOMESERVER": "https://matrix.example.org", | |
| 576 | "USER": "@user:example.org", | |
| 577 | "PASSWORD": "your-password", | |
| 578 | "DEVICE_ID": "ollamatools-matrix", | |
| 579 | "ROOMS": [ | |
| 580 | "#room:example.org" | |
| 581 | ], | |
| 582 | "ADMIN_USERS": [ | |
| 583 | "@admin:example.org" | |
| 584 | ] | |
| 585 | }, | |
| 586 | "OLLAMA": { | |
| 587 | "HOST": "localhost:11434", | |
| 588 | "MODEL": "llama3:latest", | |
| 589 | "PROMPT": "You are a helpful assistant.", | |
| 590 | "AVAILABLE_MODELS": [ | |
| 591 | "llama3:latest", | |
| 592 | "mistral:latest", | |
| 593 | "gemma:latest" | |
| 594 | ] | |
| 595 | } | |
| 596 | } | |
| 597 | ||
| 598 | ||
| 599 | # Async helper functions for the module API | |
| 600 | ||
| 601 | async def _connect_to_matrix( | |
| 602 | homeserver: str, | |
| 603 | user_id: str, | |
| 604 | password: str, | |
| 605 | device_id: str = None, | |
| 606 | ollama_host: str = "localhost:11434", | |
| 607 | ollama_model: str = "llama3:latest", | |
| 608 | ollama_prompt: str = None, | |
| 609 | available_models: List[str] = None, | |
| 610 | admin_users: List[str] = None, | |
| 611 | ssl: bool = True | |
| 612 | ) -> Optional[MatrixClient]: | |
| 613 | """ | |
| 614 | Connect to a Matrix homeserver. | |
| 615 | ||
| 616 | Args: | |
| 617 | homeserver: The URL of the Matrix homeserver | |
| 618 | user_id: The Matrix user ID (e.g., @user:example.com) | |
| 619 | password: The password for the Matrix account | |
| 620 | device_id: A unique identifier for this device (for E2E encryption) | |
| 621 | ollama_host: The host address for the Ollama API | |
| 622 | ollama_model: The Ollama model to use for generating responses | |
| 623 | ollama_prompt: An optional system prompt for the Ollama model | |
| 624 | available_models: List of available Ollama models | |
| 625 | admin_users: List of Matrix user IDs that can change models for all users | |
| 626 | ssl: Whether to use SSL for the Matrix connection | |
| 627 | ||
| 628 | Returns: | |
| 629 | Optional[MatrixClient]: A connected Matrix client, or None if connection failed | |
| 630 | """ | |
| 631 | client = MatrixClient( | |
| 632 | homeserver=homeserver, | |
| 633 | user_id=user_id, | |
| 634 | password=password, | |
| 635 | device_id=device_id, | |
| 636 | ollama_host=ollama_host, | |
| 637 | ollama_model=ollama_model, | |
| 638 | ollama_prompt=ollama_prompt, | |
| 639 | available_models=available_models, | |
| 640 | admin_users=admin_users, | |
| 641 | ssl=ssl | |
| 642 | ) | |
| 643 | ||
| 644 | success = await client.connect() | |
| 645 | ||
| 646 | if success: | |
| 647 | return client | |
| 648 | else: | |
| 649 | return None | |
| 650 | ||
| 651 | async def _join_matrix_room(client: MatrixClient, room_id: str) -> bool: | |
| 652 | """ | |
| 653 | Join a Matrix room. | |
| 654 | ||
| 655 | Args: | |
| 656 | client: A connected Matrix client | |
| 657 | room_id: The ID of the room to join (e.g., #room:example.com) | |
| 658 | ||
| 659 | Returns: | |
| 660 | bool: True if joining was successful, False otherwise | |
| 661 | """ | |
| 662 | return await client.join_room(room_id) | |
| 663 | ||
| 664 | async def _send_matrix_message(client: MatrixClient, room_id: str, message: str) -> bool: | |
| 665 | """ | |
| 666 | Send a message to a Matrix room. | |
| 667 | ||
| 668 | Args: | |
| 669 | client: A connected Matrix client | |
| 670 | room_id: The ID of the room to send the message to | |
| 671 | message: The message to send | |
| 672 | ||
| 673 | Returns: | |
| 674 | bool: True if sending was successful, False otherwise | |
| 675 | """ | |
| 676 | return await client.send_message(room_id, message) | |
| 677 | ||
| 678 | async def _listen_for_matrix_messages(client: MatrixClient) -> None: | |
| 679 | """ | |
| 680 | Start listening for messages in joined rooms. | |
| 681 | ||
| 682 | Args: | |
| 683 | client: A connected Matrix client | |
| 684 | """ | |
| 685 | await client.listen() | |
| 686 | ||
| 687 | ||
| 688 | # Synchronous wrapper functions for the module API | |
| 689 | ||
| 690 | def connect_to_matrix( | |
| 691 | homeserver: str, | |
| 692 | user_id: str, | |
| 693 | password: str, | |
| 694 | device_id: str = None, | |
| 695 | ollama_host: str = "localhost:11434", | |
| 696 | ollama_model: str = "llama3:latest", | |
| 697 | ollama_prompt: str = None, | |
| 698 | available_models: List[str] = None, | |
| 699 | admin_users: List[str] = None, | |
| 700 | ssl: bool = True | |
| 701 | ) -> Optional[MatrixClient]: | |
| 702 | """ | |
| 703 | Connect to a Matrix homeserver. | |
| 704 | ||
| 705 | Args: | |
| 706 | homeserver: The URL of the Matrix homeserver | |
| 707 | user_id: The Matrix user ID (e.g., @user:example.com) | |
| 708 | password: The password for the Matrix account | |
| 709 | device_id: A unique identifier for this device (for E2E encryption) | |
| 710 | ollama_host: The host address for the Ollama API | |
| 711 | ollama_model: The Ollama model to use for generating responses | |
| 712 | ollama_prompt: An optional system prompt for the Ollama model | |
| 713 | available_models: List of available Ollama models | |
| 714 | admin_users: List of Matrix user IDs that can change models for all users | |
| 715 | ssl: Whether to use SSL for the Matrix connection | |
| 716 | ||
| 717 | Returns: | |
| 718 | Optional[MatrixClient]: A connected Matrix client, or None if connection failed | |
| 719 | """ | |
| 720 | return asyncio.run(_connect_to_matrix( | |
| 721 | homeserver=homeserver, | |
| 722 | user_id=user_id, | |
| 723 | password=password, | |
| 724 | device_id=device_id, | |
| 725 | ollama_host=ollama_host, | |
| 726 | ollama_model=ollama_model, | |
| 727 | ollama_prompt=ollama_prompt, | |
| 728 | available_models=available_models, | |
| 729 | admin_users=admin_users, | |
| 730 | ssl=ssl | |
| 731 | )) | |
| 732 | ||
| 733 | def join_matrix_room(client: MatrixClient, room_id: str) -> bool: | |
| 734 | """ | |
| 735 | Join a Matrix room. | |
| 736 | ||
| 737 | Args: | |
| 738 | client: A connected Matrix client | |
| 739 | room_id: The ID of the room to join (e.g., #room:example.com) | |
| 740 | ||
| 741 | Returns: | |
| 742 | bool: True if joining was successful, False otherwise | |
| 743 | """ | |
| 744 | return asyncio.run(_join_matrix_room(client, room_id)) | |
| 745 | ||
| 746 | def send_matrix_message(client: MatrixClient, room_id: str, message: str) -> bool: | |
| 747 | """ | |
| 748 | Send a message to a Matrix room. | |
| 749 | ||
| 750 | Args: | |
| 751 | client: A connected Matrix client | |
| 752 | room_id: The ID of the room to send the message to | |
| 753 | message: The message to send | |
| 754 | ||
| 755 | Returns: | |
| 756 | bool: True if sending was successful, False otherwise | |
| 757 | """ | |
| 758 | return asyncio.run(_send_matrix_message(client, room_id, message)) | |
| 759 | ||
| 760 | def listen_for_matrix_messages(client: MatrixClient) -> None: | |
| 761 | """ | |
| 762 | Start listening for messages in joined rooms. | |
| 763 | ||
| 764 | This function will block until the client is stopped. | |
| 765 | ||
| 766 | Args: | |
| 767 | client: A connected Matrix client | |
| 768 | """ | |
| 769 | asyncio.run(_listen_for_matrix_messages(client)) | |
| 770 | ||
| 771 | def load_matrix_config(config_path: str) -> Dict: | |
| 772 | """ | |
| 773 | Load Matrix configuration from a YAML file. | |
| 774 | ||
| 775 | Args: | |
| 776 | config_path: Path to the configuration file | |
| 777 | ||
| 778 | Returns: | |
| 779 | Dict: The configuration as a dictionary | |
| 780 | """ | |
| 781 | return MatrixConfig.load_config(config_path) | |
| 782 | ||
| 783 | def save_matrix_config(config: Dict, config_path: str) -> None: | |
| 784 | """ | |
| 785 | Save Matrix configuration to a YAML file. | |
| 786 | ||
| 787 | Args: | |
| 788 | config: The configuration as a dictionary | |
| 789 | config_path: Path to save the configuration to | |
| 790 | """ | |
| 791 | MatrixConfig.save_config(config, config_path) | |
| 792 | ||
| 793 | def create_default_matrix_config() -> Dict: | |
| 794 | """ | |
| 795 | Create a default Matrix configuration. | |
| 796 | ||
| 797 | Returns: | |
| 798 | Dict: The default configuration | |
| 799 | """ | |
| 800 | return MatrixConfig.create_default_config() |
| … | … | @@ -8,6 +8,9 @@ typing-extensions>=4.4.0 |
| 8 | 8 | # Test dependencies |
| 9 | 9 | pytest>=7.0.0 |
| 10 | 10 | |
| 11 | # Ollama client dependency - uncomment and use the correct one based on your system | |
| 12 | # ollama>=0.4.0 | |
| 13 | # ollama-python>=0.1.2 | |
| 11 | # Ollama client dependency | |
| 12 | ollama>=0.4.0 | |
| 13 | ||
| 14 | # Matrix integration dependencies | |
| 15 | matrix-nio>=0.20.0 | |
| 16 | pyyaml>=6.0.0 |
| … | … | @@ -14,12 +14,16 @@ setup( |
| 14 | 14 | "pydantic>=1.10.0", |
| 15 | 15 | "markdown>=3.4.0", |
| 16 | 16 | "typing-extensions>=4.4.0", |
| 17 | # Ollama client will be added later once we determine which package to use | |
| 17 | "ollama>=0.4.0", | |
| 18 | 18 | ], |
| 19 | 19 | extras_require={ |
| 20 | 20 | "dev": [ |
| 21 | 21 | "pytest>=7.0.0", |
| 22 | 22 | ], |
| 23 | "matrix": [ | |
| 24 | "matrix-nio>=0.20.0", | |
| 25 | "pyyaml>=6.0.0", | |
| 26 | ], | |
| 23 | 27 | }, |
| 24 | 28 | author="OllamaTools Team", |
| 25 | 29 | author_email="example@example.com", |
| … | … | @@ -0,0 +1,272 @@ |
| 1 | """ | |
| 2 | Tests for the Matrix integration tools. | |
| 3 | ||
| 4 | These tests verify that the Matrix integration tools work correctly. | |
| 5 | They use mocking to avoid actual network connections. | |
| 6 | """ | |
| 7 | ||
| 8 | import os | |
| 9 | import sys | |
| 10 | import pytest | |
| 11 | import asyncio | |
| 12 | from unittest.mock import MagicMock, patch, AsyncMock | |
| 13 | ||
| 14 | # Add the parent directory to the path so we can import ollamatools | |
| 15 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) | |
| 16 | ||
| 17 | from ollamatools.tools.matrix_tools import ( | |
| 18 | MatrixClient, | |
| 19 | MatrixConfig, | |
| 20 | connect_to_matrix, | |
| 21 | join_matrix_room, | |
| 22 | send_matrix_message, | |
| 23 | load_matrix_config, | |
| 24 | save_matrix_config, | |
| 25 | create_default_matrix_config | |
| 26 | ) | |
| 27 | ||
| 28 | ||
| 29 | class TestMatrixConfig: | |
| 30 | """Tests for the MatrixConfig class.""" | |
| 31 | ||
| 32 | def test_create_default_config(self): | |
| 33 | """Test creating a default configuration.""" | |
| 34 | config = create_default_matrix_config() | |
| 35 | ||
| 36 | assert 'MATRIX' in config | |
| 37 | assert 'OLLAMA' in config | |
| 38 | assert 'HOMESERVER' in config['MATRIX'] | |
| 39 | assert 'USER' in config['MATRIX'] | |
| 40 | assert 'PASSWORD' in config['MATRIX'] | |
| 41 | assert 'DEVICE_ID' in config['MATRIX'] | |
| 42 | assert 'ROOMS' in config['MATRIX'] | |
| 43 | assert 'HOST' in config['OLLAMA'] | |
| 44 | assert 'MODEL' in config['OLLAMA'] | |
| 45 | assert 'PROMPT' in config['OLLAMA'] | |
| 46 | ||
| 47 | def test_save_and_load_config(self, tmp_path): | |
| 48 | """Test saving and loading a configuration.""" | |
| 49 | config_path = tmp_path / "test_config.yaml" | |
| 50 | config = create_default_matrix_config() | |
| 51 | ||
| 52 | # Save the configuration | |
| 53 | save_matrix_config(config, str(config_path)) | |
| 54 | ||
| 55 | # Check that the file exists | |
| 56 | assert config_path.exists() | |
| 57 | ||
| 58 | # Load the configuration | |
| 59 | loaded_config = load_matrix_config(str(config_path)) | |
| 60 | ||
| 61 | # Check that the loaded configuration matches the original | |
| 62 | assert loaded_config == config | |
| 63 | ||
| 64 | ||
| 65 | @pytest.mark.asyncio | |
| 66 | class TestMatrixClient: | |
| 67 | """Tests for the MatrixClient class.""" | |
| 68 | ||
| 69 | @patch('ollamatools.tools.matrix_tools.AsyncClient') | |
| 70 | async def test_connect(self, mock_async_client): | |
| 71 | """Test connecting to a Matrix homeserver.""" | |
| 72 | # Set up the mock | |
| 73 | mock_client = AsyncMock() | |
| 74 | mock_async_client.return_value = mock_client | |
| 75 | mock_client.login.return_value = MagicMock(spec=['']) | |
| 76 | ||
| 77 | # Create a client | |
| 78 | client = MatrixClient( | |
| 79 | homeserver="https://matrix.example.org", | |
| 80 | user_id="@user:example.org", | |
| 81 | password="password", | |
| 82 | device_id="device_id" | |
| 83 | ) | |
| 84 | ||
| 85 | # Connect | |
| 86 | success = await client.connect() | |
| 87 | ||
| 88 | # Check that the login method was called | |
| 89 | mock_client.login.assert_called_once_with( | |
| 90 | password="password" | |
| 91 | ) | |
| 92 | ||
| 93 | # Check that the connection was successful | |
| 94 | assert success is True | |
| 95 | ||
| 96 | @patch('ollamatools.tools.matrix_tools.AsyncClient') | |
| 97 | async def test_join_room(self, mock_async_client): | |
| 98 | """Test joining a Matrix room.""" | |
| 99 | # Set up the mock | |
| 100 | mock_client = AsyncMock() | |
| 101 | mock_async_client.return_value = mock_client | |
| 102 | mock_client.join.return_value = MagicMock(spec=['']) | |
| 103 | ||
| 104 | # Create a client | |
| 105 | client = MatrixClient( | |
| 106 | homeserver="https://matrix.example.org", | |
| 107 | user_id="@user:example.org", | |
| 108 | password="password", | |
| 109 | device_id="device_id" | |
| 110 | ) | |
| 111 | ||
| 112 | # Join a room | |
| 113 | success = await client.join_room("#room:example.org") | |
| 114 | ||
| 115 | # Check that the join method was called | |
| 116 | mock_client.join.assert_called_once_with("#room:example.org") | |
| 117 | ||
| 118 | # Check that joining was successful | |
| 119 | assert success is True | |
| 120 | ||
| 121 | # Check that the room was added to the list of joined rooms | |
| 122 | assert "#room:example.org" in client.rooms | |
| 123 | ||
| 124 | @patch('ollamatools.tools.matrix_tools.AsyncClient') | |
| 125 | async def test_send_message(self, mock_async_client): | |
| 126 | """Test sending a message to a Matrix room.""" | |
| 127 | # Set up the mock | |
| 128 | mock_client = AsyncMock() | |
| 129 | mock_async_client.return_value = mock_client | |
| 130 | mock_client.room_send.return_value = MagicMock(spec=['']) | |
| 131 | ||
| 132 | # Create a client | |
| 133 | client = MatrixClient( | |
| 134 | homeserver="https://matrix.example.org", | |
| 135 | user_id="@user:example.org", | |
| 136 | password="password", | |
| 137 | device_id="device_id" | |
| 138 | ) | |
| 139 | ||
| 140 | # Send a message | |
| 141 | success = await client.send_message("#room:example.org", "Hello, world!") | |
| 142 | ||
| 143 | # Check that the room_send method was called | |
| 144 | mock_client.room_send.assert_called_once_with( | |
| 145 | room_id="#room:example.org", | |
| 146 | message_type="m.room.message", | |
| 147 | content={ | |
| 148 | "msgtype": "m.text", | |
| 149 | "body": "Hello, world!" | |
| 150 | } | |
| 151 | ) | |
| 152 | ||
| 153 | # Check that sending was successful | |
| 154 | assert success is True | |
| 155 | ||
| 156 | @patch('ollamatools.tools.matrix_tools.OllamaAsync') | |
| 157 | async def test_process_with_ollama(self, mock_ollama_async): | |
| 158 | """Test processing a message with Ollama.""" | |
| 159 | # Set up the mock | |
| 160 | mock_ollama = AsyncMock() | |
| 161 | mock_ollama_async.return_value = mock_ollama | |
| 162 | mock_ollama.chat.return_value = { | |
| 163 | "message": { | |
| 164 | "content": "Hello, I'm Ollama!" | |
| 165 | } | |
| 166 | } | |
| 167 | ||
| 168 | # Create a client | |
| 169 | client = MatrixClient( | |
| 170 | homeserver="https://matrix.example.org", | |
| 171 | user_id="@user:example.org", | |
| 172 | password="password", | |
| 173 | device_id="device_id", | |
| 174 | ollama_host="localhost:11434", | |
| 175 | ollama_model="llama3:latest", | |
| 176 | ollama_prompt="You are a helpful assistant." | |
| 177 | ) | |
| 178 | ||
| 179 | # Process a message | |
| 180 | response = await client.process_with_ollama("Hello, Ollama!") | |
| 181 | ||
| 182 | # Check that the chat method was called | |
| 183 | mock_ollama.chat.assert_called_once_with( | |
| 184 | model="llama3:latest", | |
| 185 | messages=[ | |
| 186 | { | |
| 187 | "role": "system", | |
| 188 | "content": "You are a helpful assistant." | |
| 189 | }, | |
| 190 | { | |
| 191 | "role": "user", | |
| 192 | "content": "Hello, Ollama!" | |
| 193 | } | |
| 194 | ] | |
| 195 | ) | |
| 196 | ||
| 197 | # Check that the response is correct | |
| 198 | assert response == "Hello, I'm Ollama!" | |
| 199 | ||
| 200 | ||
| 201 | @pytest.mark.asyncio | |
| 202 | class TestMatrixToolsFunctions: | |
| 203 | """Tests for the Matrix tools functions.""" | |
| 204 | ||
| 205 | @patch('ollamatools.tools.matrix_tools.MatrixClient.connect') | |
| 206 | async def test_connect_to_matrix(self, mock_connect): | |
| 207 | """Test the connect_to_matrix function.""" | |
| 208 | # Set up the mock | |
| 209 | mock_connect.return_value = True | |
| 210 | ||
| 211 | # Connect to Matrix | |
| 212 | with patch('ollamatools.tools.matrix_tools.asyncio.run', side_effect=lambda x: asyncio.create_task(x)): | |
| 213 | client = await connect_to_matrix( | |
| 214 | homeserver="https://matrix.example.org", | |
| 215 | user_id="@user:example.org", | |
| 216 | password="password", | |
| 217 | device_id="device_id" | |
| 218 | ) | |
| 219 | ||
| 220 | # Check that the connect method was called | |
| 221 | mock_connect.assert_called_once() | |
| 222 | ||
| 223 | # Check that a client was returned | |
| 224 | assert client is not None | |
| 225 | ||
| 226 | @patch('ollamatools.tools.matrix_tools.MatrixClient.join_room') | |
| 227 | async def test_join_matrix_room(self, mock_join_room): | |
| 228 | """Test the join_matrix_room function.""" | |
| 229 | # Set up the mock | |
| 230 | mock_join_room.return_value = True | |
| 231 | ||
| 232 | # Create a client | |
| 233 | client = MatrixClient( | |
| 234 | homeserver="https://matrix.example.org", | |
| 235 | user_id="@user:example.org", | |
| 236 | password="password", | |
| 237 | device_id="device_id" | |
| 238 | ) | |
| 239 | ||
| 240 | # Join a room | |
| 241 | with patch('ollamatools.tools.matrix_tools.asyncio.run', side_effect=lambda x: asyncio.create_task(x)): | |
| 242 | success = await join_matrix_room(client, "#room:example.org") | |
| 243 | ||
| 244 | # Check that the join_room method was called | |
| 245 | mock_join_room.assert_called_once_with("#room:example.org") | |
| 246 | ||
| 247 | # Check that joining was successful | |
| 248 | assert success is True | |
| 249 | ||
| 250 | @patch('ollamatools.tools.matrix_tools.MatrixClient.send_message') | |
| 251 | async def test_send_matrix_message(self, mock_send_message): | |
| 252 | """Test the send_matrix_message function.""" | |
| 253 | # Set up the mock | |
| 254 | mock_send_message.return_value = True | |
| 255 | ||
| 256 | # Create a client | |
| 257 | client = MatrixClient( | |
| 258 | homeserver="https://matrix.example.org", | |
| 259 | user_id="@user:example.org", | |
| 260 | password="password", | |
| 261 | device_id="device_id" | |
| 262 | ) | |
| 263 | ||
| 264 | # Send a message | |
| 265 | with patch('ollamatools.tools.matrix_tools.asyncio.run', side_effect=lambda x: asyncio.create_task(x)): | |
| 266 | success = await send_matrix_message(client, "#room:example.org", "Hello, world!") | |
| 267 | ||
| 268 | # Check that the send_message method was called | |
| 269 | mock_send_message.assert_called_once_with("#room:example.org", "Hello, world!") | |
| 270 | ||
| 271 | # Check that sending was successful | |
| 272 | assert success is True |
| … | … | @@ -0,0 +1,287 @@ |
| 1 | """ | |
| 2 | Tests for the OllamaTools plugin system. | |
| 3 | ||
| 4 | This module contains tests for the core plugin system infrastructure. | |
| 5 | """ | |
| 6 | ||
| 7 | import os | |
| 8 | import sys | |
| 9 | import unittest | |
| 10 | import tempfile | |
| 11 | from unittest.mock import MagicMock, patch | |
| 12 | ||
| 13 | # Add the parent directory to the Python path | |
| 14 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) | |
| 15 | ||
| 16 | from ollamatools.plugins import OllamaToolsPlugin, SharedPlugin, MatrixPlugin, AgentPlugin | |
| 17 | from ollamatools.plugins.registry import PluginRegistry | |
| 18 | from ollamatools.plugins.errors import ( | |
| 19 | PluginError, | |
| 20 | PluginNotFoundError, | |
| 21 | PluginInitializationError, | |
| 22 | PluginDependencyError | |
| 23 | ) | |
| 24 | from ollamatools.config import PluginConfig, load_plugin_config, create_default_config | |
| 25 | ||
| 26 | ||
| 27 | class TestPlugin(SharedPlugin): | |
| 28 | """Test plugin for unit tests.""" | |
| 29 | ||
| 30 | def __init__(self, config=None): | |
| 31 | super().__init__(config) | |
| 32 | self.initialized = False | |
| 33 | self.shutdown_called = False | |
| 34 | ||
| 35 | def initialize(self, context): | |
| 36 | self.initialized = True | |
| 37 | self.context = context | |
| 38 | ||
| 39 | def shutdown(self): | |
| 40 | self.shutdown_called = True | |
| 41 | ||
| 42 | @property | |
| 43 | def version(self): | |
| 44 | return "1.0.0-test" | |
| 45 | ||
| 46 | def get_capabilities(self): | |
| 47 | return ["test_capability"] | |
| 48 | ||
| 49 | ||
| 50 | class DependentPlugin(SharedPlugin): | |
| 51 | """Plugin that depends on TestPlugin.""" | |
| 52 | ||
| 53 | @property | |
| 54 | def dependencies(self): | |
| 55 | return ["TestPlugin"] | |
| 56 | ||
| 57 | ||
| 58 | class FailingPlugin(SharedPlugin): | |
| 59 | """Plugin that fails during initialization.""" | |
| 60 | ||
| 61 | def initialize(self, context): | |
| 62 | raise RuntimeError("Initialization failure") | |
| 63 | ||
| 64 | ||
| 65 | class TestPluginSystem(unittest.TestCase): | |
| 66 | """Test cases for the plugin system.""" | |
| 67 | ||
| 68 | def setUp(self): | |
| 69 | """Set up test fixtures.""" | |
| 70 | self.registry = PluginRegistry() | |
| 71 | self.context = {"test_key": "test_value"} | |
| 72 | ||
| 73 | def test_plugin_registration(self): | |
| 74 | """Test plugin registration.""" | |
| 75 | self.registry.register_plugin(TestPlugin) | |
| 76 | self.assertIn("TestPlugin", self.registry.plugins) | |
| 77 | ||
| 78 | plugin_info = self.registry.plugins["TestPlugin"] | |
| 79 | self.assertEqual(plugin_info["class"], TestPlugin) | |
| 80 | self.assertIsNone(plugin_info["config"]) | |
| 81 | ||
| 82 | def test_plugin_initialization(self): | |
| 83 | """Test plugin initialization.""" | |
| 84 | self.registry.register_plugin(TestPlugin) | |
| 85 | plugin = self.registry.initialize_plugin("TestPlugin", self.context) | |
| 86 | ||
| 87 | self.assertIsInstance(plugin, TestPlugin) | |
| 88 | self.assertTrue(plugin.initialized) | |
| 89 | self.assertEqual(plugin.context, self.context) | |
| 90 | self.assertIn("TestPlugin", self.registry.initialized_plugins) | |
| 91 | ||
| 92 | def test_plugin_dependency(self): | |
| 93 | """Test plugin dependency resolution.""" | |
| 94 | self.registry.register_plugin(TestPlugin) | |
| 95 | self.registry.register_plugin(DependentPlugin) | |
| 96 | ||
| 97 | # Initialize the dependent plugin | |
| 98 | plugin = self.registry.initialize_plugin("DependentPlugin", self.context) | |
| 99 | ||
| 100 | # Both plugins should be initialized | |
| 101 | self.assertIn("TestPlugin", self.registry.initialized_plugins) | |
| 102 | self.assertIn("DependentPlugin", self.registry.initialized_plugins) | |
| 103 | ||
| 104 | def test_plugin_not_found(self): | |
| 105 | """Test handling of non-existent plugins.""" | |
| 106 | with self.assertRaises(PluginNotFoundError): | |
| 107 | self.registry.initialize_plugin("NonExistentPlugin", self.context) | |
| 108 | ||
| 109 | def test_plugin_initialization_error(self): | |
| 110 | """Test handling of plugin initialization errors.""" | |
| 111 | self.registry.register_plugin(FailingPlugin) | |
| 112 | ||
| 113 | with self.assertRaises(PluginInitializationError): | |
| 114 | self.registry.initialize_plugin("FailingPlugin", self.context) | |
| 115 | ||
| 116 | def test_plugin_dependency_error(self): | |
| 117 | """Test handling of missing dependencies.""" | |
| 118 | self.registry.register_plugin(DependentPlugin) | |
| 119 | ||
| 120 | with self.assertRaises(PluginDependencyError): | |
| 121 | self.registry.initialize_plugin("DependentPlugin", self.context) | |
| 122 | ||
| 123 | def test_get_plugin(self): | |
| 124 | """Test retrieving an initialized plugin.""" | |
| 125 | self.registry.register_plugin(TestPlugin) | |
| 126 | self.registry.initialize_plugin("TestPlugin", self.context) | |
| 127 | ||
| 128 | plugin = self.registry.get_plugin("TestPlugin") | |
| 129 | self.assertIsInstance(plugin, TestPlugin) | |
| 130 | ||
| 131 | # Non-existent plugin should return None | |
| 132 | self.assertIsNone(self.registry.get_plugin("NonExistentPlugin")) | |
| 133 | ||
| 134 | def test_get_plugins_by_type(self): | |
| 135 | """Test retrieving plugins by type.""" | |
| 136 | class AnotherTestPlugin(SharedPlugin): | |
| 137 | pass | |
| 138 | ||
| 139 | class TestMatrixPlugin(MatrixPlugin): | |
| 140 | pass | |
| 141 | ||
| 142 | self.registry.register_plugin(TestPlugin) | |
| 143 | self.registry.register_plugin(AnotherTestPlugin) | |
| 144 | self.registry.register_plugin(TestMatrixPlugin) | |
| 145 | ||
| 146 | self.registry.initialize_all(self.context) | |
| 147 | ||
| 148 | shared_plugins = self.registry.get_plugins_by_type(SharedPlugin) | |
| 149 | self.assertEqual(len(shared_plugins), 2) | |
| 150 | self.assertIn("TestPlugin", shared_plugins) | |
| 151 | self.assertIn("AnotherTestPlugin", shared_plugins) | |
| 152 | ||
| 153 | matrix_plugins = self.registry.get_plugins_by_type(MatrixPlugin) | |
| 154 | self.assertEqual(len(matrix_plugins), 1) | |
| 155 | self.assertIn("TestMatrixPlugin", matrix_plugins) | |
| 156 | ||
| 157 | def test_shutdown(self): | |
| 158 | """Test plugin shutdown.""" | |
| 159 | self.registry.register_plugin(TestPlugin) | |
| 160 | plugin = self.registry.initialize_plugin("TestPlugin", self.context) | |
| 161 | ||
| 162 | self.registry.shutdown() | |
| 163 | ||
| 164 | self.assertTrue(plugin.shutdown_called) | |
| 165 | self.assertEqual(len(self.registry.initialized_plugins), 0) | |
| 166 | ||
| 167 | ||
| 168 | class TestPluginConfig(unittest.TestCase): | |
| 169 | """Test cases for the plugin configuration system.""" | |
| 170 | ||
| 171 | def test_config_loading_from_dict(self): | |
| 172 | """Test loading configuration from a dictionary.""" | |
| 173 | config_dict = {"key1": "value1", "key2": 42} | |
| 174 | config = PluginConfig(config_dict) | |
| 175 | ||
| 176 | self.assertEqual(config.get("key1"), "value1") | |
| 177 | self.assertEqual(config.get("key2"), 42) | |
| 178 | self.assertIsNone(config.get("non_existent_key")) | |
| 179 | self.assertEqual(config.get("non_existent_key", "default"), "default") | |
| 180 | ||
| 181 | def test_config_validation(self): | |
| 182 | """Test configuration validation.""" | |
| 183 | schema = { | |
| 184 | "required_key": { | |
| 185 | "type": str, | |
| 186 | "required": True | |
| 187 | }, | |
| 188 | "optional_key": { | |
| 189 | "type": int, | |
| 190 | "required": False | |
| 191 | }, | |
| 192 | "enum_key": { | |
| 193 | "type": str, | |
| 194 | "enum": ["option1", "option2"] | |
| 195 | } | |
| 196 | } | |
| 197 | ||
| 198 | # Valid configuration | |
| 199 | valid_config = { | |
| 200 | "required_key": "value", | |
| 201 | "optional_key": 42, | |
| 202 | "enum_key": "option1" | |
| 203 | } | |
| 204 | ||
| 205 | config = PluginConfig(valid_config, schema) | |
| 206 | self.assertTrue(config.validate()) | |
| 207 | ||
| 208 | # Missing required key | |
| 209 | invalid_config = { | |
| 210 | "optional_key": 42 | |
| 211 | } | |
| 212 | ||
| 213 | with self.assertRaises(PluginError): | |
| 214 | PluginConfig(invalid_config, schema) | |
| 215 | ||
| 216 | # Invalid type | |
| 217 | invalid_config = { | |
| 218 | "required_key": "value", | |
| 219 | "optional_key": "not_an_int" | |
| 220 | } | |
| 221 | ||
| 222 | with self.assertRaises(PluginError): | |
| 223 | PluginConfig(invalid_config, schema) | |
| 224 | ||
| 225 | # Invalid enum value | |
| 226 | invalid_config = { | |
| 227 | "required_key": "value", | |
| 228 | "enum_key": "invalid_option" | |
| 229 | } | |
| 230 | ||
| 231 | with self.assertRaises(PluginError): | |
| 232 | PluginConfig(invalid_config, schema) | |
| 233 | ||
| 234 | def test_config_save_and_load(self): | |
| 235 | """Test saving and loading configuration to/from a file.""" | |
| 236 | config_dict = { | |
| 237 | "string_key": "value", | |
| 238 | "int_key": 42, | |
| 239 | "bool_key": True, | |
| 240 | "list_key": [1, 2, 3], | |
| 241 | "dict_key": {"nested": "value"} | |
| 242 | } | |
| 243 | ||
| 244 | config = PluginConfig(config_dict) | |
| 245 | ||
| 246 | # Save to a temporary file | |
| 247 | with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as temp_file: | |
| 248 | temp_path = temp_file.name | |
| 249 | ||
| 250 | try: | |
| 251 | config.save(temp_path) | |
| 252 | ||
| 253 | # Load the saved configuration | |
| 254 | loaded_config = PluginConfig(temp_path) | |
| 255 | ||
| 256 | # Check that the loaded configuration matches the original | |
| 257 | self.assertEqual(loaded_config.get("string_key"), "value") | |
| 258 | self.assertEqual(loaded_config.get("int_key"), 42) | |
| 259 | self.assertEqual(loaded_config.get("bool_key"), True) | |
| 260 | self.assertEqual(loaded_config.get("list_key"), [1, 2, 3]) | |
| 261 | self.assertEqual(loaded_config.get("dict_key"), {"nested": "value"}) | |
| 262 | finally: | |
| 263 | # Clean up | |
| 264 | if os.path.exists(temp_path): | |
| 265 | os.unlink(temp_path) | |
| 266 | ||
| 267 | def test_default_config(self): | |
| 268 | """Test creating default configuration.""" | |
| 269 | # Test shared plugin config | |
| 270 | shared_config = create_default_config("TestPlugin", "shared") | |
| 271 | self.assertTrue(shared_config["enabled"]) | |
| 272 | self.assertEqual(shared_config["name"], "TestPlugin") | |
| 273 | ||
| 274 | # Test matrix plugin config | |
| 275 | matrix_config = create_default_config("TestPlugin", "matrix") | |
| 276 | self.assertTrue(matrix_config["enabled"]) | |
| 277 | self.assertTrue(matrix_config["commands_enabled"]) | |
| 278 | self.assertEqual(matrix_config["command_prefix"], "/") | |
| 279 | ||
| 280 | # Test agent plugin config | |
| 281 | agent_config = create_default_config("TestPlugin", "agent") | |
| 282 | self.assertTrue(agent_config["enabled"]) | |
| 283 | self.assertTrue(agent_config["functions_enabled"]) | |
| 284 | ||
| 285 | ||
| 286 | if __name__ == "__main__": | |
| 287 | unittest.main() |