OverviewFilesCommitsIssuesDocsReleasesPeers

ollama_semantic_function_calling commits / bfa31ad

Converted ollama-matrix-client to a submodule
bfa31ad7fafd5f4e1028b4aeec1d420372b454b9
by fleeky.ashatron <fleeky@7rnx.net> · 2025-04-14T18:37:58.000Z

Changed files (35) +9203 -125

.gitmodules +3 -0
……@@ -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
ollamatools-project/CHANGELOG.md +75 -0
……@@ -5,6 +5,81 @@ All notable changes to the OllamaTools project will be documented in this file.
55The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77
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
883## [1.0.0] - 2025-04-20
984
1085### Breaking Changes
ollamatools-project/COMPLETION_REPORT_ENHANCED_MATRIX.md +193 -0
……@@ -0,0 +1,193 @@
1# Enhanced Matrix Client with Modular Agent Functionality - Completion Report
2
3## Overview
4
5This 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
9The 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
111. **enhanced_matrix_core.py**: Contains the core `EnhancedMatrixClient` class and basic functionality
122. **enhanced_matrix_search.py**: Contains search-related functionality
133. **enhanced_matrix_history.py**: Contains conversation history tracking and analytics
144. **enhanced_matrix_tools.py**: Main module that imports and re-exports everything
15
16This 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
24Additional 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
31The 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
93The 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
951. **Improved Message Callback**:
96 - Enhanced message detection logic
97 - Integration with search functionality
98 - Performance tracking
99 - Conversation history recording
100
1012. **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
1063. **Search Integration**:
107 - Implemented `_execute_search` method for web search
108 - Added search context augmentation for messages
109 - Created search result formatting
110
1114. **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
118The module now provides a comprehensive API with the following functions:
119
1201. **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
1242. **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
1293. **Message Processing Functions**:
130 - `process_with_enhanced_ollama`: Process a message with Ollama using an enhanced client
131
1324. **Configuration Functions**:
133 - `create_enhanced_matrix_config`: Create a default configuration for the enhanced client
134
1355. **Status Flags**:
136 - `MODULAR_AGENT_AVAILABLE`: Flag indicating if modular agent components are available
137 - `OPENSEARCH_AVAILABLE`: Flag indicating if OpenSearchNG is available
138
139Each 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
155While the current implementation provides a solid foundation for the enhanced Matrix client, there are several areas for future improvement:
156
1571. **Streaming Responses**:
158 - Implement streaming for longer responses from Ollama
159 - Add progress indicators during model processing
160
1612. **Enhanced Admin Controls**:
162 - Add admin-only commands for system management
163 - Implement room-specific model restrictions
164 - Add user management commands
165
1663. **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
1714. **Security Enhancements**:
172 - Add rate limiting to prevent abuse
173 - Implement more granular permission controls
174 - Add encryption for stored contexts
175
1765. **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
183The 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
185The recent refactoring into a modular structure has further enhanced the codebase by:
1861. Improving maintainability through clear separation of concerns
1872. Enhancing extensibility by making it easier to modify specific components
1883. Solving file size and context limitations by splitting functionality into multiple files
1894. Providing better error handling and resource cleanup
1905. Offering a more comprehensive API with additional synchronous wrapper functions
1916. Handling optional dependencies more gracefully with clear fallback mechanisms
192
193The 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.
ollamatools-project/COMPLETION_REPORT_MATRIX.md +133 -0
……@@ -0,0 +1,133 @@
1# Matrix Integration Enhancement Completion Report
2
3## Overview
4
5This 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
49The Matrix client examples were enhanced with:
50
511. **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
58The `MatrixClient` class in `ollamatools/tools/matrix_tools.py` was enhanced with:
59
601. **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
652. **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
713. **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
774. **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
84The configuration system was enhanced to support:
85
861. **Model Configuration**:
87 - Added `AVAILABLE_MODELS` list in the Ollama configuration section
88 - Updated default configuration to include sample models
89
902. **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
96The module API was enhanced to support:
97
981. **New Parameters**:
99 - Added `available_models` parameter to `connect_to_matrix` function
100 - Added `admin_users` parameter to `connect_to_matrix` function
101
1022. **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
108While the current enhancements provide a solid foundation for model selection and user context tracking, there are several areas for future improvement:
109
1101. **Streaming Responses**:
111 - Implement streaming for longer responses from Ollama
112 - Add progress indicators during model processing
113
1142. **Enhanced Admin Controls**:
115 - Add admin-only commands for system management
116 - Implement room-specific model restrictions
117 - Add user management commands
118
1193. **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
1244. **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
131The 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
133The 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.
ollamatools-project/IMPLEMENTATION_ISSUES.md +250 -0
……@@ -0,0 +1,250 @@
1# OllamaTools Implementation Issues and Solutions
2
3After 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```
11Create a proper module structure for the ExtendedModularAgent by:
12
131. Create a new file at ollamatools/agent/extended/agent.py
142. Move the ExtendedModularAgent class from examples/extended_modular_agent.py to this new file
153. Add proper imports and exports in ollamatools/agent/extended/__init__.py to expose the ExtendedModularAgent
164. Update the examples/extended_modular_agent.py file to import from the new module location
175. Ensure all functionality is preserved with the same API
186. 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```
27Create a shared ConversationHistoryPlugin that can be used by both the Matrix client and agent components:
28
291. Create a new file at ollamatools/plugins/conversation_history.py
302. Implement a ConversationHistoryPlugin class that follows the SharedPlugin interface from the unified architecture
313. Extract the common functionality from enhanced_matrix_history.py and ExtendedModularAgent:
32 - Conversation loading/saving
33 - Recording conversations
34 - Topic analysis
35 - Performance metrics
364. Add proper configuration options for file paths, history limits, etc.
375. Implement the initialize(), shutdown(), and get_capabilities() methods
386. Add unit tests for the new plugin
397. Update the ExtendedModularAgent to use this plugin instead of its own implementation
408. 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```
49Refactor the Matrix integration to use the plugin architecture:
50
511. Create a new file at ollamatools/plugins/matrix_core.py
522. Implement a MatrixCorePlugin class that follows the MatrixPlugin interface
533. Extract the core Matrix functionality from enhanced_matrix_core.py into this plugin
544. Create a PluginEnabledMatrixClient class that uses the plugin registry
555. Update the Matrix client initialization to register and initialize plugins
566. Modify the message handling to use plugins for processing messages
577. Add proper error handling and logging
588. Update the Matrix command handling to use plugins for command registration and execution
599. Add unit tests for the new plugin-enabled client
6010. 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```
69Create a MatrixAgentAdapterPlugin to properly connect Matrix clients with agents:
70
711. Create a new file at ollamatools/plugins/matrix_agent_adapter.py
722. Implement a MatrixAgentAdapterPlugin class that follows the MatrixPlugin interface
733. 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()
794. Add proper error handling and logging
805. Add unit tests for the adapter plugin
816. Create an example that demonstrates using the adapter to connect a Matrix client with different agent types
827. 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```
91Create a unified configuration system for the OllamaTools project:
92
931. Create a new file at ollamatools/config/plugin_config.py
942. 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
993. Update the plugin registry to use this configuration system
1004. Add configuration schemas for each plugin type
1015. Update the plugin initialization to use the new configuration system
1026. Add utility functions for creating default configurations
1037. Add unit tests for the configuration system
1048. Update examples to use the new configuration system
1059. 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```
114Create a unified error handling system for plugins:
115
1161. Create a new file at ollamatools/plugins/errors.py
1172. 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
1223. 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
1264. Update the plugin registry to use the new error handling system
1275. Update plugins to use the new exception classes
1286. Add unit tests for the error handling system
1297. 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```
138Create a command registry system for Matrix plugins:
139
1401. Create a new file at ollamatools/plugins/matrix_commands.py
1412. 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
1463. Update the MatrixPlugin interface to include a get_commands() method
1474. Modify the PluginEnabledMatrixClient to use the command registry
1485. Create example command plugins:
149 - HelpCommandPlugin for providing help
150 - SearchCommandPlugin for search commands
151 - HistoryCommandPlugin for history commands
1526. Add unit tests for the command registry
1537. Update examples to demonstrate adding custom commands
1548. 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```
163Implement a plugin discovery system using entry points:
164
1651. Update the PluginRegistry class to discover plugins using entry points
1662. Add a setup.py entry_points configuration for built-in plugins
1673. Create a plugin discovery function that can:
168 - Find plugins in specified directories
169 - Load plugins from entry points
170 - Load plugins from configuration
1714. Add a plugin validation function to ensure plugins implement the required interfaces
1725. Update the plugin initialization to use the discovery system
1736. Add unit tests for the discovery system
1747. Create an example third-party plugin package
1758. 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```
184Create a ConversationContextPlugin for efficient context management:
185
1861. Create a new file at ollamatools/plugins/conversation_context.py
1872. 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
1923. Add configuration options for context size, pruning strategy, etc.
1934. Update the Matrix client and agent to use this plugin for context management
1945. Add unit tests for the context management plugin
1956. 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```
204Create adapter plugins to expose extended agent features to Matrix:
205
2061. Create a new file at ollamatools/plugins/extended_agent_features.py
2072. Implement adapter plugins for each extended feature:
208 - TopicAnalysisPlugin for topic analysis
209 - PerformanceMetricsPlugin for performance tracking
210 - ConversationSummaryPlugin for conversation summarization
2113. Each plugin should:
212 - Follow the MatrixPlugin interface
213 - Add Matrix commands for accessing the feature
214 - Connect to the appropriate agent functionality
2154. Update the MatrixAgentAdapterPlugin to register these plugins when using an extended agent
2165. Add unit tests for the adapter plugins
2176. Create examples demonstrating the extended features in Matrix
2187. Add documentation for the extended features
219```
220
221## Implementation Strategy
222
223To implement these changes effectively, I recommend a phased approach:
224
2251. **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
2302. **Phase 2: Shared Plugins**
231 - Implement the ConversationHistoryPlugin
232 - Implement the ConversationContextPlugin
233 - Create tests for these plugins
234
2353. **Phase 3: Matrix Integration**
236 - Refactor the Matrix client to use plugins
237 - Implement the command registry
238 - Create the MatrixCorePlugin
239
2404. **Phase 4: Agent Integration**
241 - Move the ExtendedModularAgent to a proper module
242 - Update agents to use shared plugins
243 - Create the MatrixAgentAdapterPlugin
244
2455. **Phase 5: Extended Features**
246 - Implement the extended feature adapter plugins
247 - Create the plugin discovery system
248 - Update examples and documentation
249
250This phased approach ensures that each component builds on the previous ones, creating a solid foundation for the unified plugin architecture.
ollamatools-project/ISSUES.md +399 -120
……@@ -1,120 +1,399 @@
1# Issues for the Transparent Ollama Agent
2
3## Original Issues Status
4
51. **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
162. **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
233. **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
294. **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
375. **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
576. **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
687. **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
1041. **Advanced Search Intent Detection**: The current implementation uses simple heuristics for intent detection. Future versions could use more sophisticated language understanding techniques.
105
1062. **Fine-grained Permission System**: Enhance the permission system to support more granular permissions and remember user preferences across sessions.
107
1083. **Command Group Permissions**: Allow users to grant permissions to groups of related commands rather than individual commands.
109
1104. **Issue Categories and Labels**: Add support for categorizing issues and applying labels for better organization.
111
1125. **Export/Import Issues**: Add functionality to export issues to standard formats like CSV or JSON for integration with external issue tracking systems.
113
1146. **Advanced Code Analysis**: Implement code analysis features to help identify potential issues in code being developed with the agent.
115
1167. **Plugin System**: Create a plugin architecture to allow for easy extension of agent capabilities.
117
1188. **Configuration Management**: Add proper configuration file support for customizing agent behavior.
119
1209. **Testing Framework**: Develop a comprehensive testing framework for validating agent functionality.
1# OllamaTools Project Issues
2
3This 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:**
8The 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:**
16Implement 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
24class 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
38class 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:**
55There'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:**
63Extract 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
71class 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:**
92The `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:**
101Reorganize 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
109from ollamatools.agent import ModularOllamaAgent
110
111class ExtendedModularAgent(ModularOllamaAgent):
112 # Implementation here
113 pass
114
115# In ollamatools/agent/extended/__init__.py
116from .agent import ExtendedModularAgent
117
118__all__ = ['ExtendedModularAgent']
119```
120
121## 4. Lack of Unified Configuration System
122
123**Description:**
124Each 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:**
132Implement 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
141class 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:**
162The 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:**
170Create 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
178class 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:**
195Error 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:**
203Implement 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
212class OllamaToolsError(Exception):
213 """Base class for all OllamaTools errors."""
214 pass
215
216class MatrixError(OllamaToolsError):
217 """Base class for Matrix-related errors."""
218 pass
219
220class AgentError(OllamaToolsError):
221 """Base class for Agent-related errors."""
222 pass
223
224class ConfigError(OllamaToolsError):
225 """Configuration error."""
226 pass
227```
228
229## 7. Limited Extensibility for Matrix Command Handling
230
231**Description:**
232The 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:**
240Implement 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
249class 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:**
273There'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:**
281Implement 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
290class 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:**
313The 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:**
321Implement 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
330class 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:**
360The 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:**
368Integrate 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
377from ollamatools.agent.extended import ExtendedModularAgent
378
379class 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)
ollamatools-project/PHASE1_IMPLEMENTATION_PLAN.md +1105 -0
……@@ -0,0 +1,1105 @@
1# Phase 1: Core Infrastructure Implementation Plan
2
3This 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"""
13OllamaTools Plugin System.
14
15This module provides the base interfaces and registry for the OllamaTools plugin system.
16"""
17
18import os
19import sys
20import importlib
21import logging
22from typing import Dict, Any, List, Optional, Set, Type, Callable, Union
23
24# Configure logging
25logger = logging.getLogger(__name__)
26
27class 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
83class 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
138class 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
178class 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"""
197Plugin Registry for OllamaTools.
198
199This module provides the PluginRegistry class for managing OllamaTools plugins.
200"""
201
202import os
203import sys
204import importlib
205import logging
206import pkg_resources
207from typing import Dict, Any, List, Optional, Set, Type, Callable, Union
208
209from ollamatools.plugins import OllamaToolsPlugin
210from ollamatools.plugins.errors import (
211 PluginError,
212 PluginInitializationError,
213 PluginDependencyError,
214 PluginNotFoundError,
215 handle_plugin_exception
216)
217
218# Configure logging
219logger = logging.getLogger(__name__)
220
221class 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"""
404Error handling for the OllamaTools plugin system.
405
406This module provides exception classes and utility functions for handling
407errors in the OllamaTools plugin system.
408"""
409
410import sys
411import logging
412import traceback
413from typing import Dict, Any, Optional
414
415# Configure logging
416logger = logging.getLogger(__name__)
417
418class 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
433class PluginNotFoundError(PluginError):
434 """Error raised when a plugin is not found."""
435 pass
436
437
438class PluginInitializationError(PluginError):
439 """Error raised when a plugin fails to initialize."""
440 pass
441
442
443class PluginDependencyError(PluginError):
444 """Error raised when a plugin dependency cannot be satisfied."""
445 pass
446
447
448class PluginExecutionError(PluginError):
449 """Error raised when a plugin operation fails during execution."""
450 pass
451
452
453class PluginConfigurationError(PluginError):
454 """Error raised when a plugin has invalid configuration."""
455 pass
456
457
458def 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
493def 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
513def 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"""
555Configuration system for the OllamaTools plugin architecture.
556
557This module provides classes and functions for managing plugin configuration.
558"""
559
560import os
561import sys
562import yaml
563import json
564import logging
565from typing import Dict, Any, List, Optional, Set, Union, Callable
566
567from ollamatools.plugins.errors import PluginConfigurationError
568
569# Configure logging
570logger = logging.getLogger(__name__)
571
572class 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
838def 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
871def 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"""
906Configuration schemas for OllamaTools plugins.
907
908This module provides schema definitions for validating plugin configurations.
909"""
910
911from typing import Dict, Any, List, Optional, Set, Union, Callable
912
913# Schema for the ConversationHistoryPlugin
914CONVERSATION_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
941SEARCH_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
969MATRIX_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
984MATRIX_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
1020CONVERSATION_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
1049SCHEMA_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
1057def 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```
1075ollamatools/
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
1094This directory contains the core components of the OllamaTools plugin system.
1095
1096## Overview
1097
1098The 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
1102The plugin system supports several types of plugins:
1103
1104- **SharedPlugin**: Plugins that can be used by multiple components
1105- **MatrixPlugin
ollamatools-project/UNIFIED_PLUGIN_ARCHITECTURE.md +553 -0
……@@ -0,0 +1,553 @@
1# Unified Plugin Architecture for OllamaTools
2
3This 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
7The current OllamaTools project has several components that operate somewhat independently:
8
91. **Matrix Integration**: Provides Matrix chat client functionality
102. **Modular Agent**: Provides a modular agent architecture with plugins
113. **Extended Agent**: Extends the modular agent with additional features
12
13These 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
171. **Separation of Concerns**: Each plugin should have a clear, focused responsibility
182. **Extensibility**: The system should be easy to extend with new plugins
193. **Consistency**: Similar functionality should be implemented in similar ways across components
204. **Reusability**: Common functionality should be extracted into shared modules
215. **Discoverability**: Plugins should be easy to discover and use
22
23## Proposed Architecture
24
25### 1. Plugin Interface
26
27At the core of the architecture is a common plugin interface:
28
29```python
30class 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
63A central registry manages plugin discovery, loading, and dependencies:
64
65```python
66class 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
129From the base plugin interface, we can derive specialized plugin types for different components:
130
131#### 3.1 Agent Plugins
132
133```python
134class 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
153class 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
180class 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
193class 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
233class 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
322class 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
380class 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
453class 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
5131. Create the base plugin interface and registry
5142. Implement specialized plugin types
5153. Create utility functions for plugin discovery and loading
516
517### Phase 2: Shared Plugins
518
5191. Implement the SearchPlugin
5202. Implement the ConversationHistoryPlugin
5213. Create tests for these plugins
522
523### Phase 3: Matrix Integration
524
5251. Refactor the EnhancedMatrixClient to use plugins
5262. Implement the MatrixAgentAdapterPlugin
5273. Update the matrix command handling to use the plugin registry
528
529### Phase 4: Agent Integration
530
5311. Refactor the ModularOllamaAgent to use plugins
5322. Update the ExtendedModularAgent to leverage shared plugins
5333. Create adapter plugins for different agent types
534
535### Phase 5: Documentation and Examples
536
5371. Create comprehensive documentation for the plugin system
5382. Provide examples of creating custom plugins
5393. Update existing examples to use the new plugin architecture
540
541## Benefits
542
543This unified plugin architecture provides several benefits:
544
5451. **Reduced Code Duplication**: Common functionality is extracted into shared plugins
5462. **Improved Extensibility**: New features can be added as plugins without modifying core code
5473. **Better Integration**: Matrix clients and agents can share functionality through plugins
5484. **Consistent Interfaces**: All plugins follow the same patterns and conventions
5495. **Simplified Maintenance**: Each plugin has a clear, focused responsibility
550
551## Conclusion
552
553The 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.
ollamatools-project/docs/matrix_integration.md +546 -0
……@@ -0,0 +1,546 @@
1# Matrix Integration for OllamaTools
2
3This document provides information on how to use the Matrix integration in OllamaTools to connect your Ollama-powered applications to Matrix chat rooms.
4
5There are two Matrix client implementations available:
61. **Basic Matrix Client**: The standard Matrix client with basic functionality
72. **Enhanced Matrix Client**: An extended client with advanced features from modular and extended agents
8
9## Overview
10
11The 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
23To use the Matrix integration, you need to install the following dependencies:
24
25```bash
26pip install matrix-nio[e2e] # For Matrix protocol support with end-to-end encryption
27pip install ollama # For Ollama API access
28pip install pyyaml # For configuration handling
29```
30
31## Getting Started
32
33### 1. Import the Matrix Tools
34
35For the basic Matrix client:
36
37```python
38from 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
47For the enhanced Matrix client:
48
49```python
50from 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
66You can create a Matrix client in two ways:
67
68#### Option 1: Using the MatrixClient class directly
69
70```python
71from ollamatools.tools.matrix_tools import MatrixClient
72
73client = 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
84import asyncio
85async 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
92asyncio.run(connect())
93```
94
95#### Option 2: Using the connect_to_matrix function
96
97```python
98from ollamatools.tools.matrix_tools import connect_to_matrix
99
100client = 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
107if client:
108 print("Connected to Matrix homeserver")
109else:
110 print("Failed to connect to Matrix homeserver")
111```
112
113### 3. Join a Matrix Room
114
115```python
116# Using the MatrixClient class
117async 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
124asyncio.run(join_room_async())
125
126# Or using the join_matrix_room function
127from ollamatools.tools.matrix_tools import join_matrix_room
128
129success = join_matrix_room(client, "#room:example.org")
130if success:
131 print("Joined room")
132else:
133 print("Failed to join room")
134```
135
136### 4. Send a Message to a Room
137
138```python
139# Using the MatrixClient class
140async 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
147asyncio.run(send_message_async())
148
149# Or using the send_matrix_message function
150from ollamatools.tools.matrix_tools import send_matrix_message
151
152success = send_matrix_message(client, "#room:example.org", "Hello from OllamaTools!")
153if success:
154 print("Message sent")
155else:
156 print("Failed to send message")
157```
158
159### 5. Listen for Messages
160
161By 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
165async def listen_async():
166 await client.listen()
167
168asyncio.run(listen_async())
169
170# Or using the listen_for_matrix_messages function
171from ollamatools.tools.matrix_tools import listen_for_matrix_messages
172
173listen_for_matrix_messages(client)
174```
175
176### 6. Custom Message Handling
177
178You can also provide a custom message handler to process messages in your own way:
179
180```python
181async 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
198client.set_message_callback(custom_message_handler)
199
200# Start listening for messages
201asyncio.run(client.listen())
202```
203
204## Configuration
205
206You can use the MatrixConfig class to load and save configuration from YAML files:
207
208```python
209from 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
217config = create_default_matrix_config()
218
219# Save the configuration to a file
220save_matrix_config(config, "config.yaml")
221
222# Load the configuration from a file
223config = load_matrix_config("config.yaml")
224```
225
226A typical configuration file looks like this:
227
228```yaml
229MATRIX:
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
238OLLAMA:
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
248See the `examples/matrix_client_example.py` script for a complete example of how to use the basic Matrix integration.
249
250To run the example:
251
2521. Create a configuration file:
253
254```bash
255python examples/matrix_client_example.py --create-config
256```
257
2582. Edit the configuration file at `examples/config/matrix_config.yaml` with your Matrix credentials and Ollama settings.
259
2603. Run the example:
261
262```bash
263python examples/matrix_client_example.py
264```
265
266### Enhanced Matrix Client
267
268See the `examples/enhanced_matrix_client_example.py` script for a complete example of how to use the enhanced Matrix integration with advanced features.
269
270To run the example:
271
2721. Create a configuration file:
273
274```bash
275python examples/enhanced_matrix_client_example.py --create-config
276```
277
2782. Edit the configuration file at `examples/config/enhanced_matrix_config.yaml` with your Matrix credentials, Ollama settings, and enhanced options.
279
2803. Run the example:
281
282```bash
283python examples/enhanced_matrix_client_example.py
284```
285
286## Advanced Usage
287
288### Basic Matrix Client Features
289
290#### Model Selection
291
292The 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
298To enable model selection:
299
3001. Specify available models in your configuration:
301
302```yaml
303OLLAMA:
304 AVAILABLE_MODELS:
305 - "llama3:latest"
306 - "mistral:latest"
307 - "gemma:latest"
308```
309
3102. Or provide them when creating the client:
311
312```python
313client = MatrixClient(
314 # ... other parameters ...
315 available_models=["llama3:latest", "mistral:latest", "gemma:latest"]
316)
317```
318
3193. Users can list available models with the `/models` command and get help with the `/help` command.
320
3214. You can also update available models from the Ollama server:
322
323```python
324await client.update_available_models()
325```
326
327#### User Context Tracking
328
329The 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
331Context 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
334response = await client.process_with_ollama(message, sender, room_id)
335```
336
337You can control the maximum context length by setting:
338
339```python
340client.max_context_length = 15 # Default is 10
341```
342
343#### Admin Users
344
345You can designate certain Matrix users as admins, giving them special privileges:
346
347```python
348client = MatrixClient(
349 # ... other parameters ...
350 admin_users=["@admin:example.org", "@moderator:example.org"]
351)
352```
353
354Or add them later:
355
356```python
357client.add_admin_user("@admin:example.org")
358client.remove_admin_user("@admin:example.org")
359```
360
361### Enhanced Matrix Client Features
362
363The enhanced Matrix client includes all the features of the basic client, plus the following advanced features:
364
365#### Modular Structure
366
367The enhanced Matrix client is organized into multiple modules for better maintainability and extensibility:
368
3691. **enhanced_matrix_core.py**: Contains the core `EnhancedMatrixClient` class and basic functionality
3702. **enhanced_matrix_search.py**: Contains search-related functionality
3713. **enhanced_matrix_history.py**: Contains conversation history tracking and analytics
3724. **enhanced_matrix_tools.py**: Main module that imports and re-exports everything
373
374This 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
380You can import from the main module to get all functionality:
381
382```python
383from ollamatools.tools.enhanced_matrix_tools import (
384 EnhancedMatrixClient,
385 connect_enhanced_matrix,
386 # ... other imports
387)
388```
389
390Or import directly from specific modules if you only need certain components:
391
392```python
393from ollamatools.tools.enhanced_matrix_core import EnhancedMatrixClient
394from ollamatools.tools.enhanced_matrix_search import execute_search_query
395from ollamatools.tools.enhanced_matrix_history import analyze_conversation_topics
396```
397
398#### Improved Message Response Logic
399
400The 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
407client = EnhancedMatrixClient(
408 # ... other parameters ...
409 respond_to_all_messages=True # Respond to all messages, not just mentions
410)
411```
412
413#### Search Integration
414
415The enhanced client can search the web for information using OpenSearchNG:
416
417```python
418client = 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
429Users can search directly with the `/search` command:
430
431```
432/search what is the capital of France?
433```
434
435The 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
439The enhanced client can track conversation history and provide analytics:
440
441```python
442client = EnhancedMatrixClient(
443 # ... other parameters ...
444 enable_conversation_tracking=True,
445 max_conversation_history=100
446)
447```
448
449Users 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
457You can configure the enhanced client using the configuration file:
458
459```yaml
460ENHANCED:
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
489The Matrix integration supports end-to-end encryption through the matrix-nio[e2e] package. To use E2E encryption, you need to:
490
4911. Install the E2E dependencies:
492
493```bash
494pip install matrix-nio[e2e]
495```
496
4972. Provide a device ID when creating the Matrix client:
498
499```python
500client = 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
510The 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
516If 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
524If 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
531import logging
532logging.basicConfig(level=logging.DEBUG)
533```
534
535## API Reference
536
537For 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
546Each module contains comprehensive docstrings for all classes, methods, and functions.
ollamatools-project/examples/config/enhanced_matrix_config.yaml +50 -0
……@@ -0,0 +1,50 @@
1MATRIX:
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
12OLLAMA:
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
21ENHANCED:
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
ollamatools-project/examples/config/matrix_config.yaml +11 -0
……@@ -0,0 +1,11 @@
1MATRIX:
2 HOMESERVER: "https://prsm.space"
3 USER: "@chat7rn:prsm.space"
4 PASSWORD: "chat7rn"
5 DEVICE_ID: "ollamatools-matrix"
6 ROOMS:
7 - "#general:prsm.space"
8OLLAMA:
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."
ollamatools-project/examples/enhanced_matrix_client_example.py +330 -0
……@@ -0,0 +1,330 @@
1#!/usr/bin/env python3
2"""
3Enhanced Matrix Client Example for OllamaTools
4
5This example demonstrates how to use the enhanced Matrix integration in OllamaTools
6to connect to a Matrix homeserver, join rooms, and process messages with Ollama.
7It showcases advanced features like improved message response logic, search integration,
8conversation tracking, and more.
9
10Requirements:
11- matrix-nio: `pip install matrix-nio[e2e]`
12- ollama: `pip install ollama`
13- pyyaml: `pip install pyyaml`
14
15Optional 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
19Note: If the modular agent components are not available, some advanced features
20like search integration will be automatically disabled, but the client will still
21function with basic features like improved message response logic and conversation tracking.
22
23Usage:
241. Edit the configuration below or create a config.yaml file
252. Run the script: `python enhanced_matrix_client_example.py`
26"""
27
28import os
29import sys
30import yaml
31import logging
32import asyncio
33import argparse
34import random
35from typing import Dict, List, Optional
36
37# Add the parent directory to the path so we can import ollamatools
38sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
39
40from 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
52logging.basicConfig(
53 level=logging.INFO,
54 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
55)
56logger = logging.getLogger(__name__)
57
58# Default configuration file path
59DEFAULT_CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'config', 'enhanced_matrix_config.yaml')
60
61def 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
86def 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
101def 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
114async 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
141def 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
157async 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
305def 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
329if __name__ == '__main__':
330 main()
ollamatools-project/examples/matrix_client_example.py +227 -0
……@@ -0,0 +1,227 @@
1#!/usr/bin/env python3
2"""
3Matrix Client Example for OllamaTools
4
5This example demonstrates how to use the Matrix integration in OllamaTools
6to connect to a Matrix homeserver, join rooms, and process messages with Ollama.
7
8Requirements:
9- matrix-nio: `pip install matrix-nio[e2e]`
10- ollama: `pip install ollama`
11- pyyaml: `pip install pyyaml`
12
13Usage:
141. Edit the configuration below or create a config.yaml file
152. Run the script: `python matrix_client_example.py`
16"""
17
18import os
19import sys
20import yaml
21import logging
22import asyncio
23import argparse
24import random
25from typing import Dict, Optional
26
27# Add the parent directory to the path so we can import ollamatools
28sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
29
30from 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
43logging.basicConfig(
44 level=logging.INFO,
45 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
46)
47logger = logging.getLogger(__name__)
48
49# Default configuration file path
50DEFAULT_CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'config', 'matrix_config.yaml')
51
52def 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
72def 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
87def 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
100async 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
127def 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
143async 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
203def 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
226if __name__ == '__main__':
227 main()
ollamatools-project/examples/plugin_system_example.py +181 -0
……@@ -0,0 +1,181 @@
1"""
2Example demonstrating the OllamaTools plugin system.
3
4This example shows how to create, register, and use plugins with the OllamaTools
5plugin system.
6"""
7
8import os
9import sys
10import logging
11from typing import Dict, Any, List
12
13# Configure logging
14logging.basicConfig(
15 level=logging.INFO,
16 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
17)
18logger = logging.getLogger(__name__)
19
20# Import plugin system components using relative imports
21import sys
22import os
23
24# Add the parent directory to the Python path
25sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
26
27from ollamatools.plugins import OllamaToolsPlugin, SharedPlugin, MatrixPlugin, AgentPlugin
28from ollamatools.plugins.registry import PluginRegistry
29from ollamatools.plugins.errors import PluginError, handle_plugin_exception
30from ollamatools.config import PluginConfig, load_plugin_config
31
32
33# Define a simple plugin
34class 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
76class 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
114def 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
180if __name__ == "__main__":
181 sys.exit(main())
ollamatools-project/matrix_agent_plugin.py +753 -0
……@@ -0,0 +1,753 @@
1#!/usr/bin/env python3
2"""
3Matrix Agent Plugin - Proof of Concept
4
5This file demonstrates how the matrix client could be integrated with the modular agent
6using the proposed unified plugin architecture. It implements a plugin that connects
7a Matrix client to a ModularOllamaAgent, allowing the agent to process messages from
8Matrix rooms.
9
10This is a proof-of-concept implementation and is not intended for production use.
11"""
12
13import os
14import sys
15import json
16import time
17import logging
18import asyncio
19from typing import Dict, Any, List, Optional, Union, Set, Tuple
20
21# Base plugin interface
22class 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
53class 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
77class 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
93class 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
101class 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
163class 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
318class 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
507class 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
688async 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
751if __name__ == "__main__":
752 # Run the example
753 asyncio.run(main())
ollamatools-project/matrix_code_review.md +467 -0
……@@ -0,0 +1,467 @@
1# Code Review: OllamaTools Matrix Integration
2
3## Overview
4
5This 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
91. `examples/enhanced_matrix_client_example.py`
102. `ollamatools/tools/matrix_tools.py`
113. `ollamatools/tools/enhanced_matrix_tools.py`
124. `ollamatools/tools/enhanced_matrix_core.py`
135. `ollamatools/tools/enhanced_matrix_search.py`
146. `ollamatools/tools/enhanced_matrix_history.py`
15
16## 1. Architecture and Design
17
18### 1.1 Overall Architecture
19
20The 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
29This modular approach allows for good maintainability and extensibility.
30
31### 1.2 Design Patterns
32
33The 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
521. **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
632. **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
733. **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
981. **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
1072. **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
1173. **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
1401. **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
1572. **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
1831. **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
2052. **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
2353. **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
2671. **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
2772. **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
2951. **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
3022. **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
3193. **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
343The codebase generally has good exception handling with try/except blocks around critical operations. However, there are some areas for improvement:
344
3451. **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
3472. **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
3493. **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
353Input validation is generally good, but there are some areas that could be improved:
354
3551. **In `matrix_tools.py`, lines 307-315**: The username detection logic could be more robust with regex pattern matching.
356
3572. **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
361The codebase handles missing dependencies well with feature flags and graceful degradation:
362
3631. **In `enhanced_matrix_search.py`, lines 15-36**: Good handling of optional dependencies with clear logging.
364
3652. **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
3711. **In `matrix_tools.py`, lines 196-204**: The Ollama API doesn't appear to have authentication, which could be a security concern.
372
3732. **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
3771. **In `enhanced_matrix_history.py`, lines 48-67**: Conversation history is stored as plain JSON without encryption, which could expose sensitive information.
378
3792. **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
3831. **In `matrix_tools.py`, lines 307-315**: The username detection doesn't sanitize input, which could lead to false positives.
384
3852. **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
3911. **In `matrix_tools.py`, lines 246-250**: The context management could lead to memory issues with long-running bots.
392
3932. **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
3971. **In `enhanced_matrix_core.py`, lines 186-193**: The search is performed synchronously, which could block the message handling loop.
398
3992. **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
405The codebase has excellent documentation with clear docstrings for classes and methods. Examples:
406
4071. **In `matrix_tools.py`, lines 31-59**: Good class documentation with clear explanation of purpose and parameters.
408
4092. **In `enhanced_matrix_core.py`, lines 19-33**: Clear module documentation with dependency information.
410
411### 6.2 Comments
412
413The code includes helpful comments that explain complex logic:
414
4151. **In `enhanced_matrix_core.py`, lines 186-193**: Good comments explaining the search context handling.
416
4172. **In `enhanced_matrix_history.py`, lines 95-133**: Clear comments explaining the topic analysis approach.
418
419### 6.3 Examples
420
421The example code is well-documented and provides clear usage instructions:
422
4231. **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
4291. Consider using a more formal plugin architecture for the enhanced features to make it easier to add new capabilities.
430
4312. Implement a more sophisticated context management system that can handle longer conversations more efficiently.
432
433### 7.2 Code Quality
434
4351. Add more specific exception handling for different types of errors (network, API, authentication, etc.).
436
4372. Improve the username detection with regex pattern matching to avoid false positives.
438
4393. Add validation for configuration values to ensure they are within acceptable ranges.
440
441### 7.3 Security
442
4431. Add encryption for the conversation history to protect sensitive information.
444
4452. Implement more robust authentication and authorization for model changes and other administrative commands.
446
4473. Add input sanitization for search queries and other user inputs.
448
449### 7.4 Performance
450
4511. Implement caching for frequently used data like topic analysis results.
452
4532. Consider using a database for conversation history instead of JSON files for better scalability.
454
4553. Make search operations asynchronous to avoid blocking the message handling loop.
456
457### 7.5 Documentation
458
4591. Add more examples of custom message handlers and how to extend the functionality.
460
4612. Document the security considerations and best practices for deploying the bot in production.
462
463## 8. Conclusion
464
465The 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
467The 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.
ollamatools-project/ollama-matrix-client +1 -0
……@@ -0,0 +1 @@
1Subproject commit 44f8aafb2644ad801e94178ae51d66b3ecf2ac32
ollamatools-project/ollamatools/config/__init__.py +40 -0
……@@ -0,0 +1,40 @@
1"""
2OllamaTools Configuration System.
3
4This package provides configuration management for OllamaTools and its plugins.
5"""
6
7import os
8import logging
9from typing import Dict, Any, Optional
10
11# Configure logging
12logger = logging.getLogger(__name__)
13
14# Default configuration directory
15DEFAULT_CONFIG_DIR = os.path.join(
16 os.path.expanduser('~'),
17 '.ollamatools'
18)
19
20# Create default configuration directory if it doesn't exist
21os.makedirs(DEFAULT_CONFIG_DIR, exist_ok=True)
22os.makedirs(os.path.join(DEFAULT_CONFIG_DIR, 'plugins'), exist_ok=True)
23
24# Import the Config class from core_config.py
25from ollamatools.config.core_config import Config
26
27# Import key components
28from ollamatools.config.plugin_config import PluginConfig, load_plugin_config, create_default_config
29from 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]
ollamatools-project/ollamatools/config/core_config.py +107 -0
……@@ -0,0 +1,107 @@
1"""
2Core configuration management for OllamaTools.
3
4This module contains the Config class, which handles configuration settings for
5the OllamaTools library.
6"""
7
8import json
9import os
10from typing import Optional, Dict, Any
11
12from pydantic import BaseModel, validator, Field
13
14
15class 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")
ollamatools-project/ollamatools/config/plugin_config.py +344 -0
……@@ -0,0 +1,344 @@
1"""
2Configuration system for the OllamaTools plugin architecture.
3
4This module provides classes and functions for managing plugin configuration.
5"""
6
7import os
8import sys
9import yaml
10import json
11import logging
12from typing import Dict, Any, List, Optional, Set, Union, Callable
13
14from ollamatools.plugins.errors import PluginConfigurationError
15
16# Configure logging
17logger = logging.getLogger(__name__)
18
19class 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
285def 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
318def 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))
ollamatools-project/ollamatools/config/schemas.py +163 -0
……@@ -0,0 +1,163 @@
1"""
2Configuration schemas for OllamaTools plugins.
3
4This module provides schema definitions for validating plugin configurations.
5"""
6
7from typing import Dict, Any, List, Optional, Set, Union, Callable
8
9# Schema for the ConversationHistoryPlugin
10CONVERSATION_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
37SEARCH_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
65MATRIX_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
80MATRIX_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
116CONVERSATION_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
145SCHEMA_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
153def 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, {})
ollamatools-project/ollamatools/plugins/README.md +147 -0
……@@ -0,0 +1,147 @@
1# OllamaTools Plugin System
2
3This directory contains the core components of the OllamaTools plugin system.
4
5## Overview
6
7The 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
11The 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
19To create a plugin, you need to:
20
211. Create a class that inherits from one of the plugin base classes
222. Implement the required methods
233. Register the plugin with the plugin registry
24
25### Example Plugin
26
27```python
28from ollamatools.plugins import SharedPlugin
29
30class 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
61Plugins can be registered in several ways:
62
63### 1. Direct Registration
64
65```python
66from ollamatools.plugins.registry import PluginRegistry
67from my_plugin import MyPlugin
68
69# Create a registry
70registry = PluginRegistry()
71
72# Register the plugin
73registry.register_plugin(MyPlugin)
74
75# Initialize the plugin
76context = {"some_context": "value"}
77registry.initialize_plugin("MyPlugin", context)
78```
79
80### 2. Entry Points
81
82You can also register plugins using entry points in your `setup.py`:
83
84```python
85setup(
86 name="my_plugin_package",
87 # ...
88 entry_points={
89 "ollamatools.plugins": [
90 "my_plugin = my_plugin:MyPlugin",
91 ],
92 },
93)
94```
95
96Then discover plugins:
97
98```python
99registry = PluginRegistry()
100registry.discover_plugins()
101```
102
103## Plugin Configuration
104
105Plugins can be configured using the configuration system:
106
107```python
108from ollamatools.config import load_plugin_config
109
110# Load configuration for a plugin
111config = load_plugin_config("MyPlugin")
112
113# Use the configuration
114value = config.get("some_setting", "default_value")
115```
116
117## Error Handling
118
119The plugin system includes comprehensive error handling:
120
121```python
122from ollamatools.plugins.errors import PluginError, handle_plugin_exception
123
124try:
125 # Do something with a plugin
126 plugin.some_method()
127except 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
1351. **Registration**: Plugin classes are registered with the registry
1362. **Initialization**: Plugins are instantiated and initialized with context
1373. **Operation**: Plugins provide functionality through their methods
1384. **Shutdown**: Plugins clean up resources when no longer needed
139
140## Best Practices
141
1421. Keep plugins focused on a single responsibility
1432. Document plugin capabilities and configuration options
1443. Handle errors gracefully
1454. Clean up resources in the shutdown method
1465. Use dependency injection through the context parameter
1476. Follow the plugin interface contract
ollamatools-project/ollamatools/plugins/__init__.py +177 -0
……@@ -0,0 +1,177 @@
1"""
2OllamaTools Plugin System.
3
4This module provides the base interfaces and registry for the OllamaTools plugin system.
5"""
6
7import os
8import sys
9import importlib
10import logging
11from typing import Dict, Any, List, Optional, Set, Type, Callable, Union
12
13# Configure logging
14logger = logging.getLogger(__name__)
15
16class 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
72class 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
127class 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
167class 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 []
ollamatools-project/ollamatools/plugins/errors.py +142 -0
……@@ -0,0 +1,142 @@
1"""
2Error handling for the OllamaTools plugin system.
3
4This module provides exception classes and utility functions for handling
5errors in the OllamaTools plugin system.
6"""
7
8import sys
9import logging
10import traceback
11from typing import Dict, Any, Optional
12
13# Configure logging
14logger = logging.getLogger(__name__)
15
16class 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
31class PluginNotFoundError(PluginError):
32 """Error raised when a plugin is not found."""
33 pass
34
35
36class PluginInitializationError(PluginError):
37 """Error raised when a plugin fails to initialize."""
38 pass
39
40
41class PluginDependencyError(PluginError):
42 """Error raised when a plugin dependency cannot be satisfied."""
43 pass
44
45
46class PluginExecutionError(PluginError):
47 """Error raised when a plugin operation fails during execution."""
48 pass
49
50
51class PluginConfigurationError(PluginError):
52 """Error raised when a plugin has invalid configuration."""
53 pass
54
55
56def 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
91def 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
111def 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
ollamatools-project/ollamatools/plugins/registry.py +198 -0
……@@ -0,0 +1,198 @@
1"""
2Plugin Registry for OllamaTools.
3
4This module provides the PluginRegistry class for managing OllamaTools plugins.
5"""
6
7import os
8import sys
9import importlib
10import logging
11import pkg_resources
12from typing import Dict, Any, List, Optional, Set, Type, Callable, Union
13
14from ollamatools.plugins import OllamaToolsPlugin
15from ollamatools.plugins.errors import (
16 PluginError,
17 PluginInitializationError,
18 PluginDependencyError,
19 PluginNotFoundError,
20 handle_plugin_exception
21)
22
23# Configure logging
24logger = logging.getLogger(__name__)
25
26class 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
ollamatools-project/ollamatools/tools/__init__.py +47 -1
……@@ -5,6 +5,8 @@ This package contains various tool modules for use with OllamaTools:
55- system_tools: Execute Linux commands and file operations
66- web_tools: Make web requests and process responses
77- 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
810"""
911
1012from ollamatools.tools.system_tools import (
……@@ -26,6 +28,31 @@ from ollamatools.tools.doc_tools import (
2628 summarize_text
2729)
2830
31from 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
43from 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
2956__all__ = [
3057 'execute_command',
3158 'read_file',
……@@ -36,5 +63,24 @@ __all__ = [
3663 'search_web',
3764 'generate_markdown',
3865 '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'
4086]
ollamatools-project/ollamatools/tools/enhanced_matrix_core.py +466 -0
……@@ -0,0 +1,466 @@
1"""
2Core functionality for the Enhanced Matrix integration in OllamaTools.
3
4This module provides the core EnhancedMatrixClient class and basic functionality
5for connecting to Matrix homeservers, joining rooms, and handling messages.
6
7Dependencies:
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
14import os
15import sys
16import yaml
17import json
18import time
19import logging
20import asyncio
21import re
22from typing import Dict, List, Optional, Union, Callable, Any, Set, Tuple
23from collections import defaultdict
24
25# Import base matrix tools
26from ollamatools.tools.matrix_tools import (
27 MatrixClient,
28 MatrixConfig,
29 RoomMessageText
30)
31
32# Configure logging
33logger = logging.getLogger(__name__)
34
35# Import flags from other modules
36from ollamatools.tools.enhanced_matrix_search import MODULAR_AGENT_AVAILABLE, OPENSEARCH_AVAILABLE
37
38class 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 = """
327Available 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"""
343You can also mention me by my username in your message and I'll respond.
344For 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
400async 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
ollamatools-project/ollamatools/tools/enhanced_matrix_history.py +329 -0
……@@ -0,0 +1,329 @@
1"""
2Conversation history and analytics for the Enhanced Matrix integration in OllamaTools.
3
4This module provides functionality for tracking conversation history,
5analyzing topics, and generating performance metrics for the Enhanced Matrix client.
6
7Dependencies:
8- matrix-nio: For Matrix protocol support
9- ollama: For Ollama API access
10"""
11
12import os
13import json
14import time
15import logging
16from typing import Dict, List, Optional, Any, Set, Tuple
17from collections import defaultdict
18
19# Configure logging
20logger = logging.getLogger(__name__)
21
22def 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
45def 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
63def 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
107def 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
161def 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
203async 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
250async 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
289async 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))
ollamatools-project/ollamatools/tools/enhanced_matrix_search.py +170 -0
……@@ -0,0 +1,170 @@
1"""
2Search functionality for the Enhanced Matrix integration in OllamaTools.
3
4This module provides search-related functionality for the Enhanced Matrix client,
5including integration with OpenSearchNG and the modular agent's search capabilities.
6
7Dependencies:
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
14import logging
15from typing import Dict, List, Optional, Any, Union
16
17# Configure logging
18logger = logging.getLogger(__name__)
19
20# Import from modular agent (optional)
21try:
22 from ollamatools.agent.modular.search import (
23 detect_factual_intent,
24 extract_search_context,
25 execute_search
26 )
27 MODULAR_AGENT_AVAILABLE = True
28except 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)
37try:
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
45except 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
53def 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
65async 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
99async 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
126async 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)}")
ollamatools-project/ollamatools/tools/enhanced_matrix_tools.py +226 -0
……@@ -0,0 +1,226 @@
1"""
2Enhanced Matrix integration tools for OllamaTools.
3
4This module extends the basic Matrix integration with advanced features from
5the 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
13Dependencies:
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
20import os
21import sys
22import yaml
23import json
24import asyncio
25import logging
26from typing import Dict, List, Optional, Union, Callable, Any, Set, Tuple
27
28# Import from core module
29from ollamatools.tools.enhanced_matrix_core import (
30 EnhancedMatrixClient,
31 _connect_enhanced_matrix
32)
33
34# Import from search module
35from 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
45from 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
57from 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
66logger = logging.getLogger(__name__)
67
68# Synchronous wrapper functions for the module API
69
70def 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
131def 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
144def 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
158def 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
169def 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
199def 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
208def 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))
ollamatools-project/ollamatools/tools/matrix_tools.py +800 -0
……@@ -0,0 +1,800 @@
1"""
2Matrix integration tools for OllamaTools.
3
4This module provides functionality to connect to Matrix homeservers,
5join rooms, send messages, and listen for incoming messages.
6It integrates with Ollama to process messages and generate responses.
7
8Dependencies:
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
14import os
15import sys
16import yaml
17import logging
18import asyncio
19import re
20from typing import Dict, List, Optional, Union, Callable, Any, Set
21from collections import defaultdict
22
23# Configure logging
24logger = logging.getLogger(__name__)
25
26try:
27 from nio import (
28 AsyncClient,
29 MatrixRoom,
30 RoomMessageText,
31 LoginResponse,
32 JoinError
33 )
34except 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
42try:
43 from ollama import AsyncClient as OllamaAsync
44 OLLAMA_AVAILABLE = True
45except 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
53class 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 = """
413Available commands:
414- /help - Show this help message
415- /models - List available models
416- /model <model_name> - Change to a different model
417
418You can also mention me by my username in your message and I'll respond.
419For 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
516class 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
601async 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
651async 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
664async 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
678async 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
690def 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
733def 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
746def 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
760def 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
771def 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
783def 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
793def 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()
ollamatools-project/requirements.txt +6 -3
……@@ -8,6 +8,9 @@ typing-extensions>=4.4.0
88# Test dependencies
99pytest>=7.0.0
1010
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
12ollama>=0.4.0
13
14# Matrix integration dependencies
15matrix-nio>=0.20.0
16pyyaml>=6.0.0
ollamatools-project/setup.py +5 -1
……@@ -14,12 +14,16 @@ setup(
1414 "pydantic>=1.10.0",
1515 "markdown>=3.4.0",
1616 "typing-extensions>=4.4.0",
17 # Ollama client will be added later once we determine which package to use
17 "ollama>=0.4.0",
1818 ],
1919 extras_require={
2020 "dev": [
2121 "pytest>=7.0.0",
2222 ],
23 "matrix": [
24 "matrix-nio>=0.20.0",
25 "pyyaml>=6.0.0",
26 ],
2327 },
2428 author="OllamaTools Team",
2529 author_email="example@example.com",
ollamatools-project/tests/test_matrix_tools.py +272 -0
……@@ -0,0 +1,272 @@
1"""
2Tests for the Matrix integration tools.
3
4These tests verify that the Matrix integration tools work correctly.
5They use mocking to avoid actual network connections.
6"""
7
8import os
9import sys
10import pytest
11import asyncio
12from unittest.mock import MagicMock, patch, AsyncMock
13
14# Add the parent directory to the path so we can import ollamatools
15sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
16
17from 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
29class 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
66class 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
202class 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
ollamatools-project/tests/test_plugin_system.py +287 -0
……@@ -0,0 +1,287 @@
1"""
2Tests for the OllamaTools plugin system.
3
4This module contains tests for the core plugin system infrastructure.
5"""
6
7import os
8import sys
9import unittest
10import tempfile
11from unittest.mock import MagicMock, patch
12
13# Add the parent directory to the Python path
14sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
15
16from ollamatools.plugins import OllamaToolsPlugin, SharedPlugin, MatrixPlugin, AgentPlugin
17from ollamatools.plugins.registry import PluginRegistry
18from ollamatools.plugins.errors import (
19 PluginError,
20 PluginNotFoundError,
21 PluginInitializationError,
22 PluginDependencyError
23)
24from ollamatools.config import PluginConfig, load_plugin_config, create_default_config
25
26
27class 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
50class DependentPlugin(SharedPlugin):
51 """Plugin that depends on TestPlugin."""
52
53 @property
54 def dependencies(self):
55 return ["TestPlugin"]
56
57
58class FailingPlugin(SharedPlugin):
59 """Plugin that fails during initialization."""
60
61 def initialize(self, context):
62 raise RuntimeError("Initialization failure")
63
64
65class 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
168class 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
286if __name__ == "__main__":
287 unittest.main()