| … | … | @@ -0,0 +1,134 @@ |
| 1 | # ModularOllamaAgent Implementation - Completion Report | |
| 2 | ||
| 3 | ## Summary | |
| 4 | ||
| 5 | This report documents the successful implementation of the ModularOllamaAgent, a modular and extensible replacement for the TransparentOllamaAgent. The implementation includes: | |
| 6 | ||
| 7 | 1. **Core Modular Architecture**: Created a modular architecture that separates functionality into logical components | |
| 8 | 2. **Extended Functionality**: Integrated conversation history tracking, topic analysis, and performance metrics | |
| 9 | 3. **Comprehensive Documentation**: Provided detailed documentation for using and extending the agent | |
| 10 | 4. **Backward Compatibility**: Maintained backward compatibility with the TransparentOllamaAgent | |
| 11 | ||
| 12 | ## Implementation Details | |
| 13 | ||
| 14 | ### 1. Core Modular Architecture | |
| 15 | ||
| 16 | We created a modular architecture with the following components: | |
| 17 | ||
| 18 | - **agent.py**: Main agent implementation | |
| 19 | - **search.py**: Search functionality | |
| 20 | - **commands.py**: Command management | |
| 21 | - **issues.py**: Issue tracking | |
| 22 | - **feedback.py**: Semantic function selection feedback | |
| 23 | - **utils.py**: Utility functions | |
| 24 | ||
| 25 | This architecture separates concerns, making the code more maintainable and extensible. | |
| 26 | ||
| 27 | ### 2. Extended Functionality | |
| 28 | ||
| 29 | We extended the ModularOllamaAgent with additional features: | |
| 30 | ||
| 31 | - **Conversation History Tracking**: Records all conversations, including user messages, agent responses, response times, and estimated token usage | |
| 32 | - **Conversation Summarization**: Provides summaries of recent conversations | |
| 33 | - **Topic Analysis**: Analyzes conversation topics based on keywords | |
| 34 | - **Performance Metrics**: Tracks response times and estimated token usage | |
| 35 | ||
| 36 | These features are implemented in the ExtendedModularAgent class, which demonstrates how to extend the ModularOllamaAgent with custom functionality. | |
| 37 | ||
| 38 | ### 3. Comprehensive Documentation | |
| 39 | ||
| 40 | We created detailed documentation for the ModularOllamaAgent: | |
| 41 | ||
| 42 | - **ModularOllamaAgent Guide**: Comprehensive guide to using the ModularOllamaAgent | |
| 43 | - **Migration Guide**: Guide for migrating from TransparentOllamaAgent to ModularOllamaAgent | |
| 44 | - **Module-Level READMEs**: Documentation for each module explaining its purpose and usage | |
| 45 | - **Example Scripts**: Example scripts demonstrating the ModularOllamaAgent and ExtendedModularAgent | |
| 46 | ||
| 47 | ### 4. Backward Compatibility | |
| 48 | ||
| 49 | We maintained backward compatibility with the TransparentOllamaAgent: | |
| 50 | ||
| 51 | - **Alias Class**: Created an alias for ModularOllamaAgent as TransparentOllamaAgent | |
| 52 | - **Deprecation Warning**: Added a deprecation warning to encourage migration to ModularOllamaAgent | |
| 53 | - **Same API**: Ensured the ModularOllamaAgent has the same API as the TransparentOllamaAgent | |
| 54 | ||
| 55 | ## Files Created/Modified | |
| 56 | ||
| 57 | ### New Directories | |
| 58 | - `ollamatools/agent/modular/` | |
| 59 | - `ollamatools/agent/extended/` | |
| 60 | ||
| 61 | ### New Files | |
| 62 | - `ollamatools/agent/modular/agent.py` | |
| 63 | - `ollamatools/agent/modular/search.py` | |
| 64 | - `ollamatools/agent/modular/commands.py` | |
| 65 | - `ollamatools/agent/modular/issues.py` | |
| 66 | - `ollamatools/agent/modular/feedback.py` | |
| 67 | - `ollamatools/agent/modular/utils.py` | |
| 68 | - `ollamatools/agent/modular/__init__.py` | |
| 69 | - `ollamatools/agent/modular/README.md` | |
| 70 | - `ollamatools/agent/extended/__init__.py` | |
| 71 | - `ollamatools/agent/extended/README.md` | |
| 72 | - `examples/modular_agent_example.py` | |
| 73 | - `examples/extended_modular_agent.py` | |
| 74 | - `docs/modular_agent_guide.md` | |
| 75 | - `docs/migration_guide.md` | |
| 76 | ||
| 77 | ### Modified Files | |
| 78 | - `ollamatools/agent/__init__.py` | |
| 79 | - `ollamatools/agent/transparent/__init__.py` | |
| 80 | ||
| 81 | ## Code Quality Improvements | |
| 82 | ||
| 83 | 1. **Better Modularity**: Separated functionality into logical components | |
| 84 | 2. **Enhanced Documentation**: Added comprehensive docstrings and type hints | |
| 85 | 3. **Consistent Naming**: Used consistent naming conventions throughout the codebase | |
| 86 | 4. **Error Handling**: Improved error handling and error messages | |
| 87 | 5. **Code Reuse**: Eliminated code duplication through proper modularization | |
| 88 | ||
| 89 | ## Logical Audit | |
| 90 | ||
| 91 | We conducted a logical audit of each functionality to ensure it achieves the project's goals: | |
| 92 | ||
| 93 | ### Search Functionality | |
| 94 | - **Goal**: Provide seamless search integration with natural language understanding | |
| 95 | - **Implementation**: The search module includes intent detection, search execution, and result processing | |
| 96 | - **Assessment**: The implementation achieves the goal of providing seamless search integration | |
| 97 | ||
| 98 | ### Command Management | |
| 99 | - **Goal**: Provide a secure and flexible way to manage allowed commands | |
| 100 | - **Implementation**: The commands module includes listing, adding, and removing commands | |
| 101 | - **Assessment**: The implementation achieves the goal of providing secure command management | |
| 102 | ||
| 103 | ### Issue Tracking | |
| 104 | - **Goal**: Provide a way to track and manage issues | |
| 105 | - **Implementation**: The issues module includes creating, listing, and updating issues | |
| 106 | - **Assessment**: The implementation achieves the goal of providing issue tracking | |
| 107 | ||
| 108 | ### Feedback System | |
| 109 | - **Goal**: Improve the agent's ability to understand natural language queries | |
| 110 | - **Implementation**: The feedback module includes feedback collection, pattern analysis, and command intent detection | |
| 111 | - **Assessment**: The implementation achieves the goal of improving natural language understanding | |
| 112 | ||
| 113 | ### Extended Functionality | |
| 114 | - **Goal**: Demonstrate how to extend the agent with custom functionality | |
| 115 | - **Implementation**: The ExtendedModularAgent includes conversation history tracking, topic analysis, and performance metrics | |
| 116 | - **Assessment**: The implementation achieves the goal of demonstrating extensibility | |
| 117 | ||
| 118 | ## Future Improvements | |
| 119 | ||
| 120 | While the current implementation is robust and feature-rich, there are several areas for future improvement: | |
| 121 | ||
| 122 | 1. **Advanced Natural Language Understanding**: Enhance the feedback system with more sophisticated language understanding techniques | |
| 123 | 2. **Plugin Architecture**: Develop a formal plugin architecture for easier extension | |
| 124 | 3. **Configuration Management**: Add proper configuration file support for customizing agent behavior | |
| 125 | 4. **Testing Framework**: Develop a comprehensive testing framework for validating agent functionality | |
| 126 | 5. **Performance Optimization**: Optimize performance for large-scale usage | |
| 127 | ||
| 128 | ## Conclusion | |
| 129 | ||
| 130 | The ModularOllamaAgent implementation successfully consolidates all functionality from the TransparentOllamaAgent into a modular, extensible architecture. The implementation is well-documented, maintains backward compatibility, and demonstrates how to extend the agent with custom functionality. | |
| 131 | ||
| 132 | The modular architecture makes the code more maintainable and extensible, while the comprehensive documentation makes it easier for users to understand and use the agent. The logical audit confirms that each functionality achieves its intended goals. | |
| 133 | ||
| 134 | Overall, the ModularOllamaAgent is a significant improvement over the TransparentOllamaAgent and provides a solid foundation for future development. |
| … | … | @@ -0,0 +1,260 @@ |
| 1 | # Migrating from TransparentOllamaAgent to ModularOllamaAgent | |
| 2 | ||
| 3 | This guide provides instructions for migrating from the TransparentOllamaAgent to the new ModularOllamaAgent. | |
| 4 | ||
| 5 | ## Overview | |
| 6 | ||
| 7 | The ModularOllamaAgent is a drop-in replacement for the TransparentOllamaAgent, with the same API but a more modular and maintainable implementation. This guide will help you migrate your code to use the new ModularOllamaAgent. | |
| 8 | ||
| 9 | ## Basic Migration | |
| 10 | ||
| 11 | ### Import Changes | |
| 12 | ||
| 13 | Change your imports from: | |
| 14 | ||
| 15 | ```python | |
| 16 | from ollamatools.agent.transparent import TransparentOllamaAgent | |
| 17 | ``` | |
| 18 | ||
| 19 | To: | |
| 20 | ||
| 21 | ```python | |
| 22 | from ollamatools.agent import ModularOllamaAgent | |
| 23 | ``` | |
| 24 | ||
| 25 | ### Class Name Changes | |
| 26 | ||
| 27 | Change instances of `TransparentOllamaAgent` to `ModularOllamaAgent`: | |
| 28 | ||
| 29 | ```python | |
| 30 | # Old code | |
| 31 | agent = TransparentOllamaAgent(model="llama3.1:latest") | |
| 32 | ||
| 33 | # New code | |
| 34 | agent = ModularOllamaAgent(model="llama3.1:latest") | |
| 35 | ``` | |
| 36 | ||
| 37 | ## Utility Function Migration | |
| 38 | ||
| 39 | If you're using utility functions from the transparent agent, update your imports: | |
| 40 | ||
| 41 | ### Old imports: | |
| 42 | ||
| 43 | ```python | |
| 44 | from ollamatools.agent.transparent import ( | |
| 45 | print_status, print_success, print_error, | |
| 46 | print_warning, print_info | |
| 47 | ) | |
| 48 | ``` | |
| 49 | ||
| 50 | ### New imports: | |
| 51 | ||
| 52 | ```python | |
| 53 | from ollamatools.agent.modular import ( | |
| 54 | print_status, print_success, print_error, | |
| 55 | print_warning, print_info, suggest_fix | |
| 56 | ) | |
| 57 | ``` | |
| 58 | ||
| 59 | ## Feedback System Migration | |
| 60 | ||
| 61 | If you're using the feedback system, update your imports: | |
| 62 | ||
| 63 | ### Old imports: | |
| 64 | ||
| 65 | ```python | |
| 66 | from ollamatools.agent.transparent import FunctionSelectionFeedback | |
| 67 | from ollamatools.agent.transparent.feedback import ( | |
| 68 | detect_command_intent, | |
| 69 | extract_command_from_similar_queries, | |
| 70 | format_command_result | |
| 71 | ) | |
| 72 | ``` | |
| 73 | ||
| 74 | ### New imports: | |
| 75 | ||
| 76 | ```python | |
| 77 | from ollamatools.agent.modular import FunctionSelectionFeedback | |
| 78 | from ollamatools.agent.modular.feedback import ( | |
| 79 | detect_command_intent, | |
| 80 | extract_command_from_similar_queries, | |
| 81 | format_command_result | |
| 82 | ) | |
| 83 | ``` | |
| 84 | ||
| 85 | ## Complete Example Migration | |
| 86 | ||
| 87 | ### Old code: | |
| 88 | ||
| 89 | ```python | |
| 90 | from ollamatools.agent.transparent import ( | |
| 91 | TransparentOllamaAgent, | |
| 92 | print_status, print_success, print_info | |
| 93 | ) | |
| 94 | from ollamatools.opensearch_web_tools import DEFAULT_OPENSEARCH_CONFIG | |
| 95 | from ollamatools.command_validator import DEFAULT_ALLOWLIST | |
| 96 | ||
| 97 | # Extend the default command allowlist | |
| 98 | EXTENDED_ALLOWLIST = DEFAULT_ALLOWLIST.union({ | |
| 99 | "python", "python3", "calc", "find", "grep", "cat", "echo", "pwd" | |
| 100 | }) | |
| 101 | ||
| 102 | # Initialize the agent | |
| 103 | agent = TransparentOllamaAgent( | |
| 104 | model="llama3.1:latest", | |
| 105 | permission_prompts=True, | |
| 106 | opensearch_config=DEFAULT_OPENSEARCH_CONFIG, | |
| 107 | command_allowlist=EXTENDED_ALLOWLIST | |
| 108 | ) | |
| 109 | ||
| 110 | # Use the agent | |
| 111 | response = agent.chat("Hello, how can you help me?") | |
| 112 | print(response) | |
| 113 | ``` | |
| 114 | ||
| 115 | ### New code: | |
| 116 | ||
| 117 | ```python | |
| 118 | from ollamatools.agent import ModularOllamaAgent | |
| 119 | from ollamatools.agent.modular import ( | |
| 120 | print_status, print_success, print_info | |
| 121 | ) | |
| 122 | from ollamatools.opensearch_web_tools import DEFAULT_OPENSEARCH_CONFIG | |
| 123 | from ollamatools.command_validator import DEFAULT_ALLOWLIST | |
| 124 | ||
| 125 | # Extend the default command allowlist | |
| 126 | EXTENDED_ALLOWLIST = DEFAULT_ALLOWLIST.union({ | |
| 127 | "python", "python3", "calc", "find", "grep", "cat", "echo", "pwd" | |
| 128 | }) | |
| 129 | ||
| 130 | # Initialize the agent | |
| 131 | agent = ModularOllamaAgent( | |
| 132 | model="llama3.1:latest", | |
| 133 | permission_prompts=True, | |
| 134 | opensearch_config=DEFAULT_OPENSEARCH_CONFIG, | |
| 135 | command_allowlist=EXTENDED_ALLOWLIST | |
| 136 | ) | |
| 137 | ||
| 138 | # Use the agent | |
| 139 | response = agent.chat("Hello, how can you help me?") | |
| 140 | print(response) | |
| 141 | ``` | |
| 142 | ||
| 143 | ## Extending the Agent | |
| 144 | ||
| 145 | If you have code that extends the TransparentOllamaAgent, update your class inheritance: | |
| 146 | ||
| 147 | ### Old code: | |
| 148 | ||
| 149 | ```python | |
| 150 | from ollamatools.agent.transparent import TransparentOllamaAgent | |
| 151 | ||
| 152 | class CustomAgent(TransparentOllamaAgent): | |
| 153 | def __init__(self, *args, **kwargs): | |
| 154 | super().__init__(*args, **kwargs) | |
| 155 | # Custom initialization | |
| 156 | ||
| 157 | def custom_method(self): | |
| 158 | # Custom implementation | |
| 159 | pass | |
| 160 | ``` | |
| 161 | ||
| 162 | ### New code: | |
| 163 | ||
| 164 | ```python | |
| 165 | from ollamatools.agent import ModularOllamaAgent | |
| 166 | ||
| 167 | class CustomAgent(ModularOllamaAgent): | |
| 168 | def __init__(self, *args, **kwargs): | |
| 169 | super().__init__(*args, **kwargs) | |
| 170 | # Custom initialization | |
| 171 | ||
| 172 | def custom_method(self): | |
| 173 | # Custom implementation | |
| 174 | pass | |
| 175 | ``` | |
| 176 | ||
| 177 | ## Backward Compatibility | |
| 178 | ||
| 179 | For backward compatibility, the TransparentOllamaAgent class is still available and will continue to work. However, it is now an alias for ModularOllamaAgent and will show a deprecation warning: | |
| 180 | ||
| 181 | ``` | |
| 182 | DeprecationWarning: TransparentOllamaAgent is deprecated and will be removed in a future version. Use ModularOllamaAgent from ollamatools.agent.modular instead. | |
| 183 | ``` | |
| 184 | ||
| 185 | ## New Features in ModularOllamaAgent | |
| 186 | ||
| 187 | The ModularOllamaAgent includes several new utility functions that weren't available in the TransparentOllamaAgent: | |
| 188 | ||
| 189 | - `suggest_fix`: Suggests a fix for an error | |
| 190 | - `format_code_block`: Formats text as a code block | |
| 191 | - `format_table`: Formats data as a table | |
| 192 | - `truncate_text`: Truncates text to a maximum length | |
| 193 | - `format_execution_time`: Formats execution time in a human-readable format | |
| 194 | ||
| 195 | Example usage: | |
| 196 | ||
| 197 | ```python | |
| 198 | from ollamatools.agent.modular import ( | |
| 199 | suggest_fix, format_code_block, format_table, | |
| 200 | truncate_text, format_execution_time | |
| 201 | ) | |
| 202 | ||
| 203 | # Suggest a fix | |
| 204 | suggest_fix("Make sure your OpenSearchNG instance is running") | |
| 205 | ||
| 206 | # Format code | |
| 207 | code = format_code_block("print('Hello, world!')", language="python") | |
| 208 | ||
| 209 | # Format a table | |
| 210 | table = format_table( | |
| 211 | headers=["Name", "Age", "City"], | |
| 212 | rows=[ | |
| 213 | ["Alice", 30, "New York"], | |
| 214 | ["Bob", 25, "San Francisco"] | |
| 215 | ] | |
| 216 | ) | |
| 217 | ||
| 218 | # Truncate text | |
| 219 | short_text = truncate_text("This is a very long text...", max_length=10) | |
| 220 | ||
| 221 | # Format execution time | |
| 222 | formatted_time = format_execution_time(0.123) # "123.00ms" | |
| 223 | ``` | |
| 224 | ||
| 225 | ## Troubleshooting | |
| 226 | ||
| 227 | ### Import Errors | |
| 228 | ||
| 229 | If you encounter import errors after migration, make sure you're using the correct import paths: | |
| 230 | ||
| 231 | ```python | |
| 232 | # For the main agent class | |
| 233 | from ollamatools.agent import ModularOllamaAgent | |
| 234 | ||
| 235 | # For utility functions | |
| 236 | from ollamatools.agent.modular import print_status, print_success, print_error | |
| 237 | ``` | |
| 238 | ||
| 239 | ### Method Not Found Errors | |
| 240 | ||
| 241 | If you encounter "method not found" errors, check if you're using methods that have been renamed or moved. Refer to the [ModularOllamaAgent Guide](modular_agent_guide.md) for a complete list of available methods. | |
| 242 | ||
| 243 | ### Deprecation Warnings | |
| 244 | ||
| 245 | If you continue to use the TransparentOllamaAgent class, you'll see deprecation warnings. To suppress these warnings, you can use: | |
| 246 | ||
| 247 | ```python | |
| 248 | import warnings | |
| 249 | warnings.filterwarnings("ignore", category=DeprecationWarning) | |
| 250 | ``` | |
| 251 | ||
| 252 | However, it's recommended to migrate to the ModularOllamaAgent class instead. | |
| 253 | ||
| 254 | ## Getting Help | |
| 255 | ||
| 256 | If you encounter any issues during migration, please: | |
| 257 | ||
| 258 | 1. Check the [ModularOllamaAgent Guide](modular_agent_guide.md) for detailed documentation | |
| 259 | 2. Look at the example scripts in the `examples/` directory | |
| 260 | 3. File an issue on the GitHub repository if you encounter a bug |
| … | … | @@ -0,0 +1,274 @@ |
| 1 | # ModularOllamaAgent Guide | |
| 2 | ||
| 3 | This guide provides comprehensive documentation for the ModularOllamaAgent, a modular implementation of the OllamaAgent with enhanced capabilities. | |
| 4 | ||
| 5 | ## Overview | |
| 6 | ||
| 7 | The ModularOllamaAgent is a feature-rich agent built on top of the base OllamaAgent, designed with a modular architecture that separates functionality into logical components for better maintainability and extensibility. It includes features like: | |
| 8 | ||
| 9 | - Search integration with OpenSearchNG | |
| 10 | - Command management | |
| 11 | - Issue tracking | |
| 12 | - Semantic function selection feedback | |
| 13 | - Detailed execution history | |
| 14 | ||
| 15 | ## Installation | |
| 16 | ||
| 17 | The ModularOllamaAgent is included in the OllamaTools package. To install: | |
| 18 | ||
| 19 | ```bash | |
| 20 | pip install ollamatools | |
| 21 | ``` | |
| 22 | ||
| 23 | ## Basic Usage | |
| 24 | ||
| 25 | Here's a simple example of how to use the ModularOllamaAgent: | |
| 26 | ||
| 27 | ```python | |
| 28 | from ollamatools.agent import ModularOllamaAgent | |
| 29 | ||
| 30 | # Initialize the agent | |
| 31 | agent = ModularOllamaAgent( | |
| 32 | model="llama3.1:latest", | |
| 33 | permission_prompts=True | |
| 34 | ) | |
| 35 | ||
| 36 | # Chat with the agent | |
| 37 | response = agent.chat("Hello, how can you help me?") | |
| 38 | print(response) | |
| 39 | ``` | |
| 40 | ||
| 41 | ## Configuration Options | |
| 42 | ||
| 43 | The ModularOllamaAgent accepts several configuration options: | |
| 44 | ||
| 45 | | Parameter | Type | Description | Default | | |
| 46 | |-----------|------|-------------|---------| | |
| 47 | | `model` | str | Ollama model to use | "llama3.1:latest" | | |
| 48 | | `permission_prompts` | bool | Whether to display permission prompts | True | | |
| 49 | | `opensearch_config` | Dict | OpenSearchNG configuration | Default config | | |
| 50 | | `command_allowlist` | Set | Custom command allowlist | Extended allowlist | | |
| 51 | | `feedback_file_path` | str | Path to the feedback file | ~/.ollamatools/function_selection_feedback.jsonl | | |
| 52 | ||
| 53 | ## Features | |
| 54 | ||
| 55 | ### Search Integration | |
| 56 | ||
| 57 | The ModularOllamaAgent can automatically detect when a query might benefit from search and perform web searches using OpenSearchNG: | |
| 58 | ||
| 59 | ```python | |
| 60 | # Explicit search | |
| 61 | results = agent.execute_search("What is the capital of France?") | |
| 62 | ||
| 63 | # Or through natural language | |
| 64 | response = agent.chat("Can you tell me about the history of Python programming language?") | |
| 65 | # The agent will automatically search if needed | |
| 66 | ``` | |
| 67 | ||
| 68 | ### Command Management | |
| 69 | ||
| 70 | The agent provides methods to manage the list of allowed commands: | |
| 71 | ||
| 72 | ```python | |
| 73 | # List allowed commands | |
| 74 | agent.list_allowed_commands() | |
| 75 | ||
| 76 | # Add a command to the allowlist | |
| 77 | agent.add_allowed_command("npm") | |
| 78 | ||
| 79 | # Remove a command from the allowlist | |
| 80 | agent.remove_allowed_command("rm") | |
| 81 | ``` | |
| 82 | ||
| 83 | ### Issue Tracking | |
| 84 | ||
| 85 | The agent includes a built-in issue tracking system: | |
| 86 | ||
| 87 | ```python | |
| 88 | # Add an issue | |
| 89 | issue_id = agent.add_issue( | |
| 90 | title="Search not working", | |
| 91 | description="The search functionality fails when using Brave engine", | |
| 92 | severity="high" | |
| 93 | ) | |
| 94 | ||
| 95 | # List all issues | |
| 96 | agent.list_issues() | |
| 97 | ||
| 98 | # Get issue details | |
| 99 | agent.get_issue(issue_id) | |
| 100 | ||
| 101 | # Update an issue | |
| 102 | agent.update_issue(issue_id, status="resolved") | |
| 103 | ``` | |
| 104 | ||
| 105 | ### Semantic Function Selection Feedback | |
| 106 | ||
| 107 | The agent includes a feedback system that improves its ability to understand natural language queries and map them to appropriate functions: | |
| 108 | ||
| 109 | ```python | |
| 110 | # Provide explicit feedback | |
| 111 | agent.provide_function_feedback( | |
| 112 | query="what directory am I in", | |
| 113 | correct_function="execute_command", | |
| 114 | actual_function="read_file" | |
| 115 | ) | |
| 116 | ||
| 117 | # Analyze feedback patterns | |
| 118 | analysis = agent.function_feedback.analyze_function_selection_patterns() | |
| 119 | ``` | |
| 120 | ||
| 121 | ### Execution History | |
| 122 | ||
| 123 | The agent keeps track of executed functions and commands: | |
| 124 | ||
| 125 | ```python | |
| 126 | # Show execution history | |
| 127 | agent.show_execution_history(max_entries=10) | |
| 128 | ``` | |
| 129 | ||
| 130 | ## Advanced Usage | |
| 131 | ||
| 132 | ### Custom System Prompt | |
| 133 | ||
| 134 | You can customize the system prompt used by the agent: | |
| 135 | ||
| 136 | ```python | |
| 137 | agent = ModularOllamaAgent(model="llama3.1:latest") | |
| 138 | agent.system_prompt = """ | |
| 139 | You are an AI assistant specialized in Python programming. | |
| 140 | Always provide code examples when explaining programming concepts. | |
| 141 | """ | |
| 142 | response = agent.chat("How do I use list comprehensions in Python?") | |
| 143 | ``` | |
| 144 | ||
| 145 | ### Directory Structure Analysis | |
| 146 | ||
| 147 | The agent can analyze directory structures: | |
| 148 | ||
| 149 | ```python | |
| 150 | structure = agent.get_directory_structure(path=".", max_depth=2) | |
| 151 | ``` | |
| 152 | ||
| 153 | ## Extending the ModularOllamaAgent | |
| 154 | ||
| 155 | The modular architecture makes it easy to extend the agent with custom functionality: | |
| 156 | ||
| 157 | ```python | |
| 158 | from ollamatools.agent import ModularOllamaAgent | |
| 159 | ||
| 160 | class CustomAgent(ModularOllamaAgent): | |
| 161 | """A custom agent with additional capabilities.""" | |
| 162 | ||
| 163 | def __init__(self, *args, **kwargs): | |
| 164 | super().__init__(*args, **kwargs) | |
| 165 | # Add custom initialization | |
| 166 | ||
| 167 | def custom_method(self, param): | |
| 168 | """Implement a custom method.""" | |
| 169 | # Your implementation here | |
| 170 | return f"Processed {param}" | |
| 171 | ``` | |
| 172 | ||
| 173 | ## Module Structure | |
| 174 | ||
| 175 | The ModularOllamaAgent is organized into several modules: | |
| 176 | ||
| 177 | - `agent.py`: Main agent implementation | |
| 178 | - `search.py`: Search functionality | |
| 179 | - `commands.py`: Command management | |
| 180 | - `issues.py`: Issue tracking | |
| 181 | - `feedback.py`: Semantic function selection feedback | |
| 182 | - `utils.py`: Utility functions | |
| 183 | ||
| 184 | ## Complete Example | |
| 185 | ||
| 186 | Here's a complete example that demonstrates various features of the ModularOllamaAgent: | |
| 187 | ||
| 188 | ```python | |
| 189 | import os | |
| 190 | import sys | |
| 191 | from ollamatools.agent import ModularOllamaAgent | |
| 192 | from ollamatools.agent.modular import print_info | |
| 193 | from ollamatools.opensearch_web_tools import DEFAULT_OPENSEARCH_CONFIG | |
| 194 | from ollamatools.command_validator import DEFAULT_ALLOWLIST | |
| 195 | ||
| 196 | # Extend the default command allowlist | |
| 197 | EXTENDED_ALLOWLIST = DEFAULT_ALLOWLIST.union({ | |
| 198 | "python", "python3", "calc", "find", "grep", "cat", "echo", "pwd" | |
| 199 | }) | |
| 200 | ||
| 201 | # Initialize the agent | |
| 202 | agent = ModularOllamaAgent( | |
| 203 | model="llama3.1:latest", | |
| 204 | permission_prompts=True, | |
| 205 | opensearch_config=DEFAULT_OPENSEARCH_CONFIG, | |
| 206 | command_allowlist=EXTENDED_ALLOWLIST | |
| 207 | ) | |
| 208 | ||
| 209 | # Simple chat loop | |
| 210 | while True: | |
| 211 | user_input = input("\nYou: ").strip() | |
| 212 | ||
| 213 | if user_input.lower() in ['exit', 'quit', 'bye']: | |
| 214 | print("\nExiting. Goodbye!") | |
| 215 | break | |
| 216 | ||
| 217 | # Process as normal chat message | |
| 218 | response = agent.chat(user_input) | |
| 219 | print(f"\nAssistant: {response}") | |
| 220 | ``` | |
| 221 | ||
| 222 | ## Migrating from TransparentOllamaAgent | |
| 223 | ||
| 224 | If you're currently using the TransparentOllamaAgent, migrating to the ModularOllamaAgent is straightforward: | |
| 225 | ||
| 226 | ```python | |
| 227 | # Old code | |
| 228 | from ollamatools.agent.transparent import TransparentOllamaAgent | |
| 229 | agent = TransparentOllamaAgent(model="llama3.1:latest") | |
| 230 | ||
| 231 | # New code | |
| 232 | from ollamatools.agent import ModularOllamaAgent | |
| 233 | agent = ModularOllamaAgent(model="llama3.1:latest") | |
| 234 | ``` | |
| 235 | ||
| 236 | The ModularOllamaAgent is a drop-in replacement for the TransparentOllamaAgent, with the same API but a more modular and maintainable implementation. | |
| 237 | ||
| 238 | ## Troubleshooting | |
| 239 | ||
| 240 | ### OpenSearchNG Not Available | |
| 241 | ||
| 242 | If OpenSearchNG is not available, the agent will fall back to standard search methods: | |
| 243 | ||
| 244 | ``` | |
| 245 | ❌ OpenSearchNG not available: Could not connect to OpenSearchNG server at http://localhost:8081 | |
| 246 | ⚠️ Falling back to regular search methods | |
| 247 | 💡 Suggested fix: Make sure your OpenSearchNG instance is running at http://localhost:8081 | |
| 248 | ``` | |
| 249 | ||
| 250 | ### Command Not in Allowlist | |
| 251 | ||
| 252 | If a command is not in the allowlist, you'll see an error: | |
| 253 | ||
| 254 | ``` | |
| 255 | ❌ Command 'npm' is not in the allowed command list. | |
| 256 | ``` | |
| 257 | ||
| 258 | You can add it to the allowlist: | |
| 259 | ||
| 260 | ```python | |
| 261 | agent.add_allowed_command("npm") | |
| 262 | ``` | |
| 263 | ||
| 264 | ## Best Practices | |
| 265 | ||
| 266 | 1. **Use the ModularOllamaAgent for new projects**: The ModularOllamaAgent is the recommended agent for new projects. | |
| 267 | ||
| 268 | 2. **Leverage the feedback system**: The feedback system can significantly improve the agent's ability to understand natural language queries. | |
| 269 | ||
| 270 | 3. **Customize the command allowlist**: Tailor the command allowlist to your specific needs to balance functionality and security. | |
| 271 | ||
| 272 | 4. **Extend rather than modify**: When adding custom functionality, extend the ModularOllamaAgent rather than modifying its core implementation. | |
| 273 | ||
| 274 | 5. **Use the issue tracking system**: The built-in issue tracking system can help you manage issues and track their resolution. |
| … | … | @@ -32,6 +32,7 @@ The improved implementation: |
| 32 | 32 | - `commands.py`: Command management |
| 33 | 33 | - `issues.py`: Issue tracking |
| 34 | 34 | - `utils.py`: Utility functions |
| 35 | - `feedback.py`: Semantic function selection feedback | |
| 35 | 36 | |
| 36 | 37 | 2. **Easier Maintenance**: Changes to one aspect (e.g., search) don't require modifying the entire agent |
| 37 | 38 | |
| … | … | @@ -107,6 +108,123 @@ python extended_transparent_agent.py |
| 107 | 108 | |
| 108 | 109 | The original and improved implementations provide the same functionality, but the improved version uses the modularized code structure for better maintainability and extensibility. The extended implementation demonstrates how to add custom features by inheriting from the modularized agent. |
| 109 | 110 | |
| 111 | ## Semantic Function Selection Feedback | |
| 112 | ||
| 113 | The improved agent now includes a feedback system that enhances its ability to understand natural language queries and map them to appropriate functions. This system: | |
| 114 | ||
| 115 | 1. **Learns from User Interactions**: Records when functions are used correctly or incorrectly | |
| 116 | 2. **Improves Over Time**: Gets better at selecting the right function for similar queries | |
| 117 | 3. **Adapts to User Language**: Learns the specific ways users phrase their requests | |
| 118 | 4. **Provides Analytics**: Offers insights into common patterns and mismatches | |
| 119 | ||
| 120 | ### How the Feedback System Works | |
| 121 | ||
| 122 | 1. **Data Collection**: The system records: | |
| 123 | - Which queries led to which function calls | |
| 124 | - Whether the function calls were successful | |
| 125 | - User corrections when the wrong function was used | |
| 126 | - Similarity scores between current and past queries | |
| 127 | ||
| 128 | 2. **Pattern Recognition**: The system analyzes: | |
| 129 | - Common words and phrases in similar queries | |
| 130 | - Which functions work best for different types of questions | |
| 131 | - Patterns of mismatches between expected and actual functions | |
| 132 | - Implicit feedback from execution results | |
| 133 | ||
| 134 | 3. **Improved Selection**: When a new query comes in: | |
| 135 | - The system compares it to previous similar queries | |
| 136 | - It suggests the function that worked best for similar queries | |
| 137 | - It calculates confidence scores for function suggestions | |
| 138 | - It applies this knowledge before falling back to default behavior | |
| 139 | ||
| 140 | ### Technical Implementation | |
| 141 | ||
| 142 | The feedback system is implemented in the `ollamatools/agent/transparent/feedback.py` module through the `FunctionSelectionFeedback` class: | |
| 143 | ||
| 144 | 1. **Feedback Storage**: | |
| 145 | - Feedback is stored in a JSONL file at `~/.ollamatools/function_selection_feedback.jsonl` | |
| 146 | - Recent feedback is cached in memory for quick access | |
| 147 | - Each feedback entry includes query, function information, timestamp, and similarity data | |
| 148 | ||
| 149 | 2. **Similarity Calculation**: | |
| 150 | - Uses Jaccard similarity between query word sets | |
| 151 | - Compares new queries against previously recorded queries | |
| 152 | - Filters out common stop words to focus on meaningful terms | |
| 153 | - Returns top similar queries with their similarity scores | |
| 154 | ||
| 155 | 3. **Function Suggestion**: | |
| 156 | - Weights function suggestions by similarity scores | |
| 157 | - Calculates confidence based on consistency of function usage | |
| 158 | - Returns the best function match with confidence score | |
| 159 | ||
| 160 | 4. **Pattern Generation**: | |
| 161 | - Automatically generates regex patterns from feedback data | |
| 162 | - Creates patterns based on common words in similar queries | |
| 163 | - Identifies consistent query patterns that map to specific functions | |
| 164 | - Suggests improvements for function selection logic | |
| 165 | ||
| 166 | 5. **Command Intent Detection**: | |
| 167 | - Specialized support for detecting system command intents | |
| 168 | - Maps natural language questions to appropriate shell commands | |
| 169 | - Uses both predefined patterns and learned patterns from feedback | |
| 170 | - Formats command results in a user-friendly way | |
| 171 | ||
| 172 | ### Using the Feedback System | |
| 173 | ||
| 174 | The improved agent includes commands for interacting with the feedback system: | |
| 175 | ||
| 176 | - `feedback <query> | <correct_function>` - Provide explicit feedback on function selection | |
| 177 | - `analyze_feedback` - Show analysis of function selection patterns and improvement suggestions | |
| 178 | ||
| 179 | Example: | |
| 180 | ``` | |
| 181 | You: what directory am I in | |
| 182 | Assistant: [uses read_file incorrectly] | |
| 183 | ||
| 184 | You: feedback 'what directory am I in' | execute_command | |
| 185 | Assistant: Thank you for your feedback! I'll remember to use execute_command for similar queries in the future. | |
| 186 | ||
| 187 | [Later] | |
| 188 | You: where am I located? | |
| 189 | Assistant: [now correctly uses pwd command] | |
| 190 | ``` | |
| 191 | ||
| 192 | ### Advanced Usage | |
| 193 | ||
| 194 | Developers can leverage the feedback system in their own agents: | |
| 195 | ||
| 196 | ```python | |
| 197 | from ollamatools.agent.transparent.feedback import FunctionSelectionFeedback | |
| 198 | ||
| 199 | # Initialize the feedback system | |
| 200 | feedback_system = FunctionSelectionFeedback() | |
| 201 | ||
| 202 | # Get function suggestions for a query | |
| 203 | function, confidence, similar_queries = feedback_system.get_function_suggestion("what files are in this folder?") | |
| 204 | ||
| 205 | # Record explicit feedback | |
| 206 | feedback_system.record_explicit_feedback( | |
| 207 | query="what files are in this folder?", | |
| 208 | intended_function="execute_command", | |
| 209 | actual_function="read_file", | |
| 210 | user_correction=True | |
| 211 | ) | |
| 212 | ||
| 213 | # Record implicit feedback from execution results | |
| 214 | feedback_system.record_implicit_feedback( | |
| 215 | query="show me the files", | |
| 216 | function="execute_command", | |
| 217 | confidence_score=0.8, | |
| 218 | execution_result={"returncode": 0, "stdout": "file1.txt file2.txt", "stderr": ""} | |
| 219 | ) | |
| 220 | ||
| 221 | # Generate improved patterns for function selection | |
| 222 | improved_patterns = feedback_system.generate_improved_patterns() | |
| 223 | ||
| 224 | # Analyze function selection patterns | |
| 225 | analysis = feedback_system.analyze_function_selection_patterns() | |
| 226 | ``` | |
| 227 | ||
| 110 | 228 | ## Extended Agent Features |
| 111 | 229 | |
| 112 | 230 | The `extended_transparent_agent.py` example demonstrates: |
| … | … | @@ -0,0 +1,309 @@ |
| 1 | #!/usr/bin/env python3 | |
| 2 | """ | |
| 3 | Extended ModularOllamaAgent Example. | |
| 4 | ||
| 5 | This script demonstrates how to extend the ModularOllamaAgent | |
| 6 | with custom functionality like conversation history tracking, | |
| 7 | conversation summarization, and topic analysis. | |
| 8 | """ | |
| 9 | ||
| 10 | import os | |
| 11 | import sys | |
| 12 | import json | |
| 13 | import time | |
| 14 | import logging | |
| 15 | from typing import Dict, Any, List, Optional | |
| 16 | ||
| 17 | # Import the ModularOllamaAgent | |
| 18 | from ollamatools.agent import ModularOllamaAgent | |
| 19 | from ollamatools.agent.modular import ( | |
| 20 | print_status, print_success, print_info, print_error | |
| 21 | ) | |
| 22 | from ollamatools.opensearch_web_tools import DEFAULT_OPENSEARCH_CONFIG | |
| 23 | from ollamatools.command_validator import DEFAULT_ALLOWLIST | |
| 24 | from ollamatools.utils.logger import setup_logger | |
| 25 | ||
| 26 | # Set up logging | |
| 27 | logger = setup_logger("extended_modular_agent", level="INFO") | |
| 28 | ||
| 29 | class ExtendedModularAgent(ModularOllamaAgent): | |
| 30 | """ | |
| 31 | Extended version of the ModularOllamaAgent with additional features. | |
| 32 | ||
| 33 | This class demonstrates how to extend the ModularOllamaAgent with custom | |
| 34 | functionality while leveraging the existing modular architecture. | |
| 35 | """ | |
| 36 | ||
| 37 | def __init__(self, *args, **kwargs): | |
| 38 | """Initialize the extended agent.""" | |
| 39 | # Initialize the base class | |
| 40 | super().__init__(*args, **kwargs) | |
| 41 | ||
| 42 | # Add custom attributes | |
| 43 | self.conversation_history_file = os.path.join( | |
| 44 | os.path.expanduser('~'), | |
| 45 | '.ollamatools', | |
| 46 | 'conversation_history.json' | |
| 47 | ) | |
| 48 | self.conversation_history = [] | |
| 49 | ||
| 50 | # Load conversation history if it exists | |
| 51 | self._load_conversation_history() | |
| 52 | ||
| 53 | print_info("Extended agent initialized with conversation history tracking") | |
| 54 | ||
| 55 | def _load_conversation_history(self) -> None: | |
| 56 | """Load conversation history from file.""" | |
| 57 | if os.path.exists(self.conversation_history_file): | |
| 58 | try: | |
| 59 | with open(self.conversation_history_file, 'r') as f: | |
| 60 | self.conversation_history = json.load(f) | |
| 61 | print_info(f"Loaded {len(self.conversation_history)} conversations from history") | |
| 62 | except Exception as e: | |
| 63 | print_error(f"Failed to load conversation history: {str(e)}") | |
| 64 | self.conversation_history = [] | |
| 65 | ||
| 66 | def _save_conversation_history(self) -> None: | |
| 67 | """Save conversation history to file.""" | |
| 68 | try: | |
| 69 | # Create directory if it doesn't exist | |
| 70 | os.makedirs(os.path.dirname(self.conversation_history_file), exist_ok=True) | |
| 71 | ||
| 72 | with open(self.conversation_history_file, 'w') as f: | |
| 73 | json.dump(self.conversation_history, f, indent=2) | |
| 74 | print_info(f"Saved {len(self.conversation_history)} conversations to history") | |
| 75 | except Exception as e: | |
| 76 | print_error(f"Failed to save conversation history: {str(e)}") | |
| 77 | ||
| 78 | def chat(self, message: str) -> str: | |
| 79 | """ | |
| 80 | Enhanced chat method that tracks conversation history. | |
| 81 | ||
| 82 | Args: | |
| 83 | message: User message | |
| 84 | ||
| 85 | Returns: | |
| 86 | str: Response message | |
| 87 | """ | |
| 88 | # Record start time for performance tracking | |
| 89 | start_time = time.time() | |
| 90 | ||
| 91 | # Process the message using the parent class method | |
| 92 | response = super().chat(message) | |
| 93 | ||
| 94 | # Calculate response time | |
| 95 | response_time = time.time() - start_time | |
| 96 | ||
| 97 | # Add to conversation history | |
| 98 | conversation = { | |
| 99 | "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), | |
| 100 | "user_message": message, | |
| 101 | "assistant_response": response, | |
| 102 | "response_time": response_time, | |
| 103 | "tokens_estimated": len(message.split()) + len(response.split()) | |
| 104 | } | |
| 105 | ||
| 106 | self.conversation_history.append(conversation) | |
| 107 | self._save_conversation_history() | |
| 108 | ||
| 109 | return response | |
| 110 | ||
| 111 | def summarize_conversations(self, n: int = 5) -> str: | |
| 112 | """ | |
| 113 | Summarize recent conversations. | |
| 114 | ||
| 115 | Args: | |
| 116 | n: Number of recent conversations to summarize | |
| 117 | ||
| 118 | Returns: | |
| 119 | str: Summary of recent conversations | |
| 120 | """ | |
| 121 | if not self.conversation_history: | |
| 122 | return "No conversation history available." | |
| 123 | ||
| 124 | # Get the most recent conversations | |
| 125 | recent = self.conversation_history[-n:] if len(self.conversation_history) >= n else self.conversation_history | |
| 126 | ||
| 127 | # Create a summary | |
| 128 | summary = ["Recent Conversations:"] | |
| 129 | for i, conv in enumerate(recent, 1): | |
| 130 | summary.append(f"\n{i}. {conv['timestamp']}") | |
| 131 | summary.append(f"User: {conv['user_message'][:100]}..." if len(conv['user_message']) > 100 else f"User: {conv['user_message']}") | |
| 132 | summary.append(f"Response time: {conv['response_time']:.2f}s") | |
| 133 | ||
| 134 | return "\n".join(summary) | |
| 135 | ||
| 136 | def analyze_conversation_topics(self) -> Dict[str, int]: | |
| 137 | """ | |
| 138 | Analyze common topics in conversation history. | |
| 139 | ||
| 140 | Returns: | |
| 141 | Dict[str, int]: Dictionary of topics and their frequency | |
| 142 | """ | |
| 143 | if not self.conversation_history: | |
| 144 | return {} | |
| 145 | ||
| 146 | # Simple keyword-based topic analysis | |
| 147 | topics = { | |
| 148 | "help": 0, | |
| 149 | "search": 0, | |
| 150 | "file": 0, | |
| 151 | "command": 0, | |
| 152 | "code": 0, | |
| 153 | "error": 0, | |
| 154 | "issue": 0, | |
| 155 | "question": 0, | |
| 156 | "other": 0 | |
| 157 | } | |
| 158 | ||
| 159 | for conv in self.conversation_history: | |
| 160 | message = conv["user_message"].lower() | |
| 161 | ||
| 162 | # Check for keywords | |
| 163 | if "help" in message or "how" in message: | |
| 164 | topics["help"] += 1 | |
| 165 | if "search" in message or "find" in message or "look" in message: | |
| 166 | topics["search"] += 1 | |
| 167 | if "file" in message or "read" in message or "write" in message: | |
| 168 | topics["file"] += 1 | |
| 169 | if "command" in message or "run" in message or "execute" in message: | |
| 170 | topics["command"] += 1 | |
| 171 | if "code" in message or "program" in message or "script" in message: | |
| 172 | topics["code"] += 1 | |
| 173 | if "error" in message or "bug" in message or "fix" in message: | |
| 174 | topics["error"] += 1 | |
| 175 | if "issue" in message or "problem" in message: | |
| 176 | topics["issue"] += 1 | |
| 177 | if "?" in message: | |
| 178 | topics["question"] += 1 | |
| 179 | ||
| 180 | # If no keywords matched, count as other | |
| 181 | if not any(count > 0 for count in topics.values()): | |
| 182 | topics["other"] += 1 | |
| 183 | ||
| 184 | # Remove topics with zero count | |
| 185 | return {k: v for k, v in topics.items() if v > 0} | |
| 186 | ||
| 187 | def get_performance_metrics(self) -> Dict[str, Any]: | |
| 188 | """ | |
| 189 | Get performance metrics from conversation history. | |
| 190 | ||
| 191 | Returns: | |
| 192 | Dict[str, Any]: Performance metrics | |
| 193 | """ | |
| 194 | if not self.conversation_history: | |
| 195 | return {"error": "No conversation history available"} | |
| 196 | ||
| 197 | # Calculate metrics | |
| 198 | total_conversations = len(self.conversation_history) | |
| 199 | total_response_time = sum(conv["response_time"] for conv in self.conversation_history) | |
| 200 | avg_response_time = total_response_time / total_conversations if total_conversations > 0 else 0 | |
| 201 | ||
| 202 | # Calculate token usage | |
| 203 | total_tokens = sum(conv.get("tokens_estimated", 0) for conv in self.conversation_history) | |
| 204 | avg_tokens_per_conversation = total_tokens / total_conversations if total_conversations > 0 else 0 | |
| 205 | ||
| 206 | # Find slowest and fastest responses | |
| 207 | slowest = max(self.conversation_history, key=lambda x: x["response_time"]) if self.conversation_history else None | |
| 208 | fastest = min(self.conversation_history, key=lambda x: x["response_time"]) if self.conversation_history else None | |
| 209 | ||
| 210 | return { | |
| 211 | "total_conversations": total_conversations, | |
| 212 | "total_response_time": total_response_time, | |
| 213 | "avg_response_time": avg_response_time, | |
| 214 | "total_tokens_estimated": total_tokens, | |
| 215 | "avg_tokens_per_conversation": avg_tokens_per_conversation, | |
| 216 | "slowest_response": { | |
| 217 | "time": slowest["response_time"] if slowest else 0, | |
| 218 | "message": slowest["user_message"][:50] + "..." if slowest and len(slowest["user_message"]) > 50 else slowest["user_message"] if slowest else "" | |
| 219 | }, | |
| 220 | "fastest_response": { | |
| 221 | "time": fastest["response_time"] if fastest else 0, | |
| 222 | "message": fastest["user_message"][:50] + "..." if fastest and len(fastest["user_message"]) > 50 else fastest["user_message"] if fastest else "" | |
| 223 | } | |
| 224 | } | |
| 225 | ||
| 226 | ||
| 227 | def main(): | |
| 228 | """Run the extended ModularOllamaAgent.""" | |
| 229 | try: | |
| 230 | # Initialize the extended agent | |
| 231 | logger.info("Initializing ExtendedModularAgent...") | |
| 232 | agent = ExtendedModularAgent( | |
| 233 | model="llama3.1:latest", | |
| 234 | permission_prompts=True | |
| 235 | ) | |
| 236 | ||
| 237 | logger.info("Agent initialized. Enter 'exit' to quit.") | |
| 238 | print("\nExtended ModularOllamaAgent") | |
| 239 | print("===========================") | |
| 240 | print("Type 'exit' to quit") | |
| 241 | print("Type 'commands' to see available commands") | |
| 242 | print("Type 'history' to see execution history") | |
| 243 | print("Type 'conversations' to see recent conversations") | |
| 244 | print("Type 'analyze' to analyze conversation topics") | |
| 245 | print("Type 'metrics' to see performance metrics") | |
| 246 | ||
| 247 | # Simple chat loop | |
| 248 | while True: | |
| 249 | # Get user input | |
| 250 | user_input = input("\nYou: ").strip() | |
| 251 | ||
| 252 | # Check for exit command | |
| 253 | if user_input.lower() in ['exit', 'quit', 'bye']: | |
| 254 | print("\nExiting. Goodbye!") | |
| 255 | break | |
| 256 | ||
| 257 | # Handle special commands | |
| 258 | if user_input.lower() == 'commands': | |
| 259 | agent.list_allowed_commands() | |
| 260 | continue | |
| 261 | ||
| 262 | if user_input.lower() == 'history': | |
| 263 | agent.show_execution_history() | |
| 264 | continue | |
| 265 | ||
| 266 | if user_input.lower() == 'conversations': | |
| 267 | summary = agent.summarize_conversations() | |
| 268 | print(summary) | |
| 269 | continue | |
| 270 | ||
| 271 | if user_input.lower() == 'analyze': | |
| 272 | topics = agent.analyze_conversation_topics() | |
| 273 | if topics: | |
| 274 | print("\nConversation Topic Analysis:") | |
| 275 | for topic, count in sorted(topics.items(), key=lambda x: x[1], reverse=True): | |
| 276 | print(f" {topic}: {count}") | |
| 277 | else: | |
| 278 | print("\nNo conversation data to analyze.") | |
| 279 | continue | |
| 280 | ||
| 281 | if user_input.lower() == 'metrics': | |
| 282 | metrics = agent.get_performance_metrics() | |
| 283 | if "error" in metrics: | |
| 284 | print(f"\n{metrics['error']}") | |
| 285 | else: | |
| 286 | print("\nPerformance Metrics:") | |
| 287 | print(f" Total conversations: {metrics['total_conversations']}") | |
| 288 | print(f" Average response time: {metrics['avg_response_time']:.2f}s") | |
| 289 | print(f" Estimated total tokens: {metrics['total_tokens_estimated']}") | |
| 290 | print(f" Average tokens per conversation: {metrics['avg_tokens_per_conversation']:.1f}") | |
| 291 | print(f" Slowest response: {metrics['slowest_response']['time']:.2f}s for '{metrics['slowest_response']['message']}'") | |
| 292 | print(f" Fastest response: {metrics['fastest_response']['time']:.2f}s for '{metrics['fastest_response']['message']}'") | |
| 293 | continue | |
| 294 | ||
| 295 | # Process as normal chat message | |
| 296 | response = agent.chat(user_input) | |
| 297 | print(f"\nAssistant: {response}") | |
| 298 | ||
| 299 | except KeyboardInterrupt: | |
| 300 | print("\n\nExiting due to keyboard interrupt.") | |
| 301 | except Exception as e: | |
| 302 | logger.error(f"Unexpected error in main function: {str(e)}") | |
| 303 | print(f"\nUnexpected error: {str(e)}") | |
| 304 | print("Check the logs for details.") | |
| 305 | ||
| 306 | return 0 | |
| 307 | ||
| 308 | if __name__ == "__main__": | |
| 309 | sys.exit(main()) |
| … | … | @@ -68,6 +68,10 @@ def main(): |
| 68 | 68 | print(" add_issue <title> | <description> | [severity] - Add a new issue") |
| 69 | 69 | print(" resolve_issue <id> - Mark an issue as resolved") |
| 70 | 70 | |
| 71 | print("\nFunction Selection Feedback:") | |
| 72 | print(" feedback <query> | <correct_function> - Provide feedback on function selection") | |
| 73 | print(" analyze_feedback - Show analysis of function selection patterns") | |
| 74 | ||
| 71 | 75 | # Simple chat loop |
| 72 | 76 | while True: |
| 73 | 77 | # Get user input |
| … | … | @@ -160,6 +164,54 @@ def main(): |
| 160 | 164 | print("Invalid issue ID. Use 'resolve_issue <number>'.") |
| 161 | 165 | continue |
| 162 | 166 | |
| 167 | # Handle function selection feedback commands | |
| 168 | if user_input.lower().startswith('feedback '): | |
| 169 | # Parse command: feedback query | correct_function | |
| 170 | parts = user_input[len('feedback '):].split('|') | |
| 171 | if len(parts) >= 2: | |
| 172 | query = parts[0].strip() | |
| 173 | correct_function = parts[1].strip() | |
| 174 | agent.provide_function_feedback(query, correct_function) | |
| 175 | else: | |
| 176 | print("Usage: feedback query | correct_function") | |
| 177 | print("Example: feedback 'what directory am I in' | execute_command") | |
| 178 | continue | |
| 179 | ||
| 180 | if user_input.lower() == 'analyze_feedback': | |
| 181 | try: | |
| 182 | analysis = agent.function_feedback.analyze_function_selection_patterns() | |
| 183 | if "error" in analysis: | |
| 184 | print(f"Error analyzing feedback: {analysis['error']}") | |
| 185 | else: | |
| 186 | print("\nFunction Selection Feedback Analysis:") | |
| 187 | print(f"Total feedback entries: {analysis['total_feedback_entries']}") | |
| 188 | ||
| 189 | if analysis.get("top_mismatches"): | |
| 190 | print("\nTop Function Mismatches:") | |
| 191 | for i, (mismatch, data) in enumerate(analysis["top_mismatches"][:5], 1): | |
| 192 | print(f"{i}. {mismatch} (count: {data['count']})") | |
| 193 | print(f" Examples: {', '.join(data['examples'][:3])}") | |
| 194 | ||
| 195 | if analysis.get("top_query_patterns"): | |
| 196 | print("\nTop Query Patterns:") | |
| 197 | for i, (pattern, data) in enumerate(analysis["top_query_patterns"][:5], 1): | |
| 198 | print(f"{i}. '{pattern}' (count: {data['count']})") | |
| 199 | if data.get("functions"): | |
| 200 | top_function = max(data["functions"].items(), key=lambda x: x[1]) | |
| 201 | print(f" Most common function: {top_function[0]} ({top_function[1]} occurrences)") | |
| 202 | ||
| 203 | if analysis.get("improvement_suggestions"): | |
| 204 | print("\nImprovement Suggestions:") | |
| 205 | for i, suggestion in enumerate(analysis["improvement_suggestions"][:3], 1): | |
| 206 | print(f"{i}. {suggestion['description']}") | |
| 207 | if suggestion.get("examples"): | |
| 208 | print(f" Examples: {', '.join(suggestion['examples'])}") | |
| 209 | if suggestion.get("suggested_keywords"): | |
| 210 | print(f" Keywords: {', '.join(suggestion['suggested_keywords'])}") | |
| 211 | except Exception as e: | |
| 212 | print(f"Error analyzing feedback: {str(e)}") | |
| 213 | continue | |
| 214 | ||
| 163 | 215 | # Process as normal chat message |
| 164 | 216 | response = agent.chat(user_input) |
| 165 | 217 | print(f"\nAssistant: {response}") |
| … | … | @@ -0,0 +1,228 @@ |
| 1 | #!/usr/bin/env python3 | |
| 2 | """ | |
| 3 | ModularOllamaAgent Example. | |
| 4 | ||
| 5 | This script demonstrates how to use the ModularOllamaAgent | |
| 6 | to create a feature-rich agent with search capabilities, | |
| 7 | command management, issue tracking, and semantic function selection feedback. | |
| 8 | """ | |
| 9 | ||
| 10 | import os | |
| 11 | import sys | |
| 12 | import logging | |
| 13 | from typing import Dict, Any | |
| 14 | ||
| 15 | # Import the ModularOllamaAgent | |
| 16 | from ollamatools.agent import ModularOllamaAgent | |
| 17 | from ollamatools.agent.modular import ( | |
| 18 | print_status, print_success, print_info | |
| 19 | ) | |
| 20 | from ollamatools.opensearch_web_tools import DEFAULT_OPENSEARCH_CONFIG | |
| 21 | from ollamatools.command_validator import DEFAULT_ALLOWLIST | |
| 22 | from ollamatools.utils.logger import setup_logger | |
| 23 | ||
| 24 | # Set up logging | |
| 25 | logger = setup_logger("modular_agent_example", level="INFO") | |
| 26 | ||
| 27 | # Extend the default command allowlist | |
| 28 | EXTENDED_ALLOWLIST = DEFAULT_ALLOWLIST.union({ | |
| 29 | "python", "python3", "calc", "bc", "find", "grep", "awk", "sed", | |
| 30 | "head", "tail", "more", "less", "cat", "echo", "cd", "pwd" | |
| 31 | }) | |
| 32 | ||
| 33 | # Configuration for OpenSearchNG | |
| 34 | OPENSEARCH_CONFIG = DEFAULT_OPENSEARCH_CONFIG.copy() | |
| 35 | OPENSEARCH_CONFIG.update({ | |
| 36 | "base_url": "http://localhost:8081", | |
| 37 | "engines": ["google", "bing", "duckduckgo", "brave"], | |
| 38 | "num_results": 5 | |
| 39 | }) | |
| 40 | ||
| 41 | def main(): | |
| 42 | """Run the ModularOllamaAgent example.""" | |
| 43 | try: | |
| 44 | # Initialize the agent | |
| 45 | logger.info("Initializing ModularOllamaAgent...") | |
| 46 | agent = ModularOllamaAgent( | |
| 47 | model="llama3.1:latest", | |
| 48 | permission_prompts=True, | |
| 49 | opensearch_config=OPENSEARCH_CONFIG, | |
| 50 | command_allowlist=EXTENDED_ALLOWLIST | |
| 51 | ) | |
| 52 | ||
| 53 | logger.info("Agent initialized. Enter 'exit' to quit.") | |
| 54 | print("\nModularOllamaAgent Example") | |
| 55 | print("=========================") | |
| 56 | print("Type 'exit' to quit") | |
| 57 | print("Type 'commands' to see available commands") | |
| 58 | print("Type 'add_command <name>' to add a command to the allowed list") | |
| 59 | print("Type 'remove_command <name>' to remove a command from the allowed list") | |
| 60 | print("Type 'search <query>' to search directly") | |
| 61 | print("Type 'dir' to show current directory structure") | |
| 62 | print("Type 'history' to see execution history") | |
| 63 | ||
| 64 | print("\nIssue Tracking:") | |
| 65 | print(" issues - List all issues") | |
| 66 | print(" issue <id> - Show issue details") | |
| 67 | print(" add_issue <title> | <description> | [severity] - Add a new issue") | |
| 68 | print(" resolve_issue <id> - Mark an issue as resolved") | |
| 69 | ||
| 70 | print("\nFunction Selection Feedback:") | |
| 71 | print(" feedback <query> | <correct_function> - Provide feedback on function selection") | |
| 72 | print(" analyze_feedback - Show analysis of function selection patterns") | |
| 73 | ||
| 74 | # Simple chat loop | |
| 75 | while True: | |
| 76 | # Get user input | |
| 77 | user_input = input("\nYou: ").strip() | |
| 78 | ||
| 79 | # Check for exit command | |
| 80 | if user_input.lower() in ['exit', 'quit', 'bye']: | |
| 81 | print("\nExiting. Goodbye!") | |
| 82 | break | |
| 83 | ||
| 84 | # Handle special commands | |
| 85 | if user_input.lower() == 'commands': | |
| 86 | agent.list_allowed_commands() | |
| 87 | continue | |
| 88 | ||
| 89 | if user_input.lower().startswith('add_command '): | |
| 90 | command = user_input[len('add_command '):].strip() | |
| 91 | agent.add_allowed_command(command) | |
| 92 | continue | |
| 93 | ||
| 94 | if user_input.lower().startswith('remove_command '): | |
| 95 | command = user_input[len('remove_command '):].strip() | |
| 96 | agent.remove_allowed_command(command) | |
| 97 | continue | |
| 98 | ||
| 99 | if user_input.lower() == 'history': | |
| 100 | agent.show_execution_history() | |
| 101 | continue | |
| 102 | ||
| 103 | if user_input.lower().startswith('search '): | |
| 104 | query = user_input[7:].strip() | |
| 105 | if query: | |
| 106 | try: | |
| 107 | results = agent.execute_search(query) | |
| 108 | # Show the top 3 results | |
| 109 | if results.get("results"): | |
| 110 | print("\nTop Results:") | |
| 111 | for i, result in enumerate(results["results"][:3], 1): | |
| 112 | print(f"{i}. {result['title']}") | |
| 113 | print(f" URL: {result['url']}") | |
| 114 | print(f" {result['content'][:100]}...") | |
| 115 | print() | |
| 116 | except Exception as e: | |
| 117 | print(f"Search failed: {str(e)}") | |
| 118 | else: | |
| 119 | print("Please provide a search query.") | |
| 120 | continue | |
| 121 | ||
| 122 | if user_input.lower().startswith('dir'): | |
| 123 | parts = user_input.split() | |
| 124 | path = parts[1] if len(parts) > 1 else '.' | |
| 125 | depth = int(parts[2]) if len(parts) > 2 and parts[2].isdigit() else 2 | |
| 126 | ||
| 127 | try: | |
| 128 | agent.get_directory_structure(path, depth) | |
| 129 | except Exception as e: | |
| 130 | print(f"Failed to get directory structure: {str(e)}") | |
| 131 | continue | |
| 132 | ||
| 133 | # Handle issue tracking commands | |
| 134 | if user_input.lower() == 'issues': | |
| 135 | agent.list_issues() | |
| 136 | continue | |
| 137 | ||
| 138 | if user_input.lower().startswith('issue '): | |
| 139 | try: | |
| 140 | issue_id = int(user_input[len('issue '):].strip()) | |
| 141 | agent.get_issue(issue_id) | |
| 142 | except ValueError: | |
| 143 | print("Invalid issue ID. Use 'issue <number>'.") | |
| 144 | continue | |
| 145 | ||
| 146 | if user_input.lower().startswith('add_issue '): | |
| 147 | # Parse command: add_issue Title | Description | [severity] | |
| 148 | parts = user_input[len('add_issue '):].split('|') | |
| 149 | if len(parts) >= 2: | |
| 150 | title = parts[0].strip() | |
| 151 | description = parts[1].strip() | |
| 152 | severity = parts[2].strip().lower() if len(parts) > 2 else 'medium' | |
| 153 | agent.add_issue(title, description, severity) | |
| 154 | else: | |
| 155 | print("Usage: add_issue Title | Description | [severity]") | |
| 156 | continue | |
| 157 | ||
| 158 | if user_input.lower().startswith('resolve_issue '): | |
| 159 | try: | |
| 160 | issue_id = int(user_input[len('resolve_issue '):].strip()) | |
| 161 | agent.update_issue(issue_id, status='resolved') | |
| 162 | except ValueError: | |
| 163 | print("Invalid issue ID. Use 'resolve_issue <number>'.") | |
| 164 | continue | |
| 165 | ||
| 166 | # Handle function selection feedback commands | |
| 167 | if user_input.lower().startswith('feedback '): | |
| 168 | # Parse command: feedback query | correct_function | |
| 169 | parts = user_input[len('feedback '):].split('|') | |
| 170 | if len(parts) >= 2: | |
| 171 | query = parts[0].strip() | |
| 172 | correct_function = parts[1].strip() | |
| 173 | agent.provide_function_feedback(query, correct_function) | |
| 174 | else: | |
| 175 | print("Usage: feedback query | correct_function") | |
| 176 | print("Example: feedback 'what directory am I in' | execute_command") | |
| 177 | continue | |
| 178 | ||
| 179 | if user_input.lower() == 'analyze_feedback': | |
| 180 | try: | |
| 181 | analysis = agent.function_feedback.analyze_function_selection_patterns() | |
| 182 | if "error" in analysis: | |
| 183 | print(f"Error analyzing feedback: {analysis['error']}") | |
| 184 | else: | |
| 185 | print("\nFunction Selection Feedback Analysis:") | |
| 186 | print(f"Total feedback entries: {analysis['total_feedback_entries']}") | |
| 187 | ||
| 188 | if analysis.get("top_mismatches"): | |
| 189 | print("\nTop Function Mismatches:") | |
| 190 | for i, (mismatch, data) in enumerate(analysis["top_mismatches"][:5], 1): | |
| 191 | print(f"{i}. {mismatch} (count: {data['count']})") | |
| 192 | print(f" Examples: {', '.join(data['examples'][:3])}") | |
| 193 | ||
| 194 | if analysis.get("top_query_patterns"): | |
| 195 | print("\nTop Query Patterns:") | |
| 196 | for i, (pattern, data) in enumerate(analysis["top_query_patterns"][:5], 1): | |
| 197 | print(f"{i}. '{pattern}' (count: {data['count']})") | |
| 198 | if data.get("functions"): | |
| 199 | top_function = max(data["functions"].items(), key=lambda x: x[1]) | |
| 200 | print(f" Most common function: {top_function[0]} ({top_function[1]} occurrences)") | |
| 201 | ||
| 202 | if analysis.get("improvement_suggestions"): | |
| 203 | print("\nImprovement Suggestions:") | |
| 204 | for i, suggestion in enumerate(analysis["improvement_suggestions"][:3], 1): | |
| 205 | print(f"{i}. {suggestion['description']}") | |
| 206 | if suggestion.get("examples"): | |
| 207 | print(f" Examples: {', '.join(suggestion['examples'])}") | |
| 208 | if suggestion.get("suggested_keywords"): | |
| 209 | print(f" Keywords: {', '.join(suggestion['suggested_keywords'])}") | |
| 210 | except Exception as e: | |
| 211 | print(f"Error analyzing feedback: {str(e)}") | |
| 212 | continue | |
| 213 | ||
| 214 | # Process as normal chat message | |
| 215 | response = agent.chat(user_input) | |
| 216 | print(f"\nAssistant: {response}") | |
| 217 | ||
| 218 | except KeyboardInterrupt: | |
| 219 | print("\n\nExiting due to keyboard interrupt.") | |
| 220 | except Exception as e: | |
| 221 | logger.error(f"Unexpected error in main function: {str(e)}") | |
| 222 | print(f"\nUnexpected error: {str(e)}") | |
| 223 | print("Check the logs for details.") | |
| 224 | ||
| 225 | return 0 | |
| 226 | ||
| 227 | if __name__ == "__main__": | |
| 228 | sys.exit(main()) |
| … | … | @@ -4,7 +4,17 @@ OllamaTools Agent module initialization. |
| 4 | 4 | This package provides agent implementations for OllamaTools. |
| 5 | 5 | """ |
| 6 | 6 | |
| 7 | # This is a package initialization file | |
| 8 | # The transparent agent module is imported here | |
| 7 | # Import the ModularOllamaAgent | |
| 8 | from .modular import ModularOllamaAgent | |
| 9 | 9 | |
| 10 | __all__ = [] | |
| 10 | # Import the ExtendedModularAgent | |
| 11 | from .extended import ExtendedModularAgent | |
| 12 | ||
| 13 | # For backward compatibility | |
| 14 | from .transparent import TransparentOllamaAgent | |
| 15 | ||
| 16 | __all__ = [ | |
| 17 | 'ModularOllamaAgent', | |
| 18 | 'ExtendedModularAgent', | |
| 19 | 'TransparentOllamaAgent' | |
| 20 | ] |
| … | … | @@ -0,0 +1,109 @@ |
| 1 | # ExtendedModularAgent | |
| 2 | ||
| 3 | This package provides extensions to the ModularOllamaAgent with additional features like conversation history tracking, conversation summarization, topic analysis, and performance metrics. | |
| 4 | ||
| 5 | ## Overview | |
| 6 | ||
| 7 | The ExtendedModularAgent builds on the modular architecture of the ModularOllamaAgent, adding features that enhance its capabilities for tracking and analyzing conversations. | |
| 8 | ||
| 9 | ## Features | |
| 10 | ||
| 11 | ### Conversation History Tracking | |
| 12 | ||
| 13 | The ExtendedModularAgent automatically tracks all conversations, including: | |
| 14 | - User messages | |
| 15 | - Agent responses | |
| 16 | - Response times | |
| 17 | - Estimated token usage | |
| 18 | ||
| 19 | This history is saved to a JSON file for persistence across sessions. | |
| 20 | ||
| 21 | ### Conversation Summarization | |
| 22 | ||
| 23 | The agent can summarize recent conversations, providing a quick overview of: | |
| 24 | - Timestamps | |
| 25 | - User messages (truncated for readability) | |
| 26 | - Response times | |
| 27 | ||
| 28 | ### Topic Analysis | |
| 29 | ||
| 30 | The agent can analyze conversation topics based on keywords, categorizing conversations into topics like: | |
| 31 | - Help requests | |
| 32 | - Search queries | |
| 33 | - File operations | |
| 34 | - Command executions | |
| 35 | - Code-related questions | |
| 36 | - Error reports | |
| 37 | - Issue discussions | |
| 38 | - General questions | |
| 39 | ||
| 40 | ### Performance Metrics | |
| 41 | ||
| 42 | The agent tracks performance metrics, including: | |
| 43 | - Total number of conversations | |
| 44 | - Average response time | |
| 45 | - Total estimated token usage | |
| 46 | - Average tokens per conversation | |
| 47 | - Slowest and fastest responses | |
| 48 | ||
| 49 | ## Usage | |
| 50 | ||
| 51 | To use the ExtendedModularAgent: | |
| 52 | ||
| 53 | ```python | |
| 54 | from ollamatools.agent.extended import ExtendedModularAgent | |
| 55 | ||
| 56 | # Initialize the agent | |
| 57 | agent = ExtendedModularAgent( | |
| 58 | model="llama3.1:latest", | |
| 59 | permission_prompts=True | |
| 60 | ) | |
| 61 | ||
| 62 | # Chat with the agent | |
| 63 | response = agent.chat("Hello, how can you help me?") | |
| 64 | print(response) | |
| 65 | ||
| 66 | # Get conversation summary | |
| 67 | summary = agent.summarize_conversations(n=5) | |
| 68 | print(summary) | |
| 69 | ||
| 70 | # Analyze conversation topics | |
| 71 | topics = agent.analyze_conversation_topics() | |
| 72 | print(topics) | |
| 73 | ||
| 74 | # Get performance metrics | |
| 75 | metrics = agent.get_performance_metrics() | |
| 76 | print(metrics) | |
| 77 | ``` | |
| 78 | ||
| 79 | ## Example Script | |
| 80 | ||
| 81 | See the [extended_modular_agent.py](../../../../examples/extended_modular_agent.py) example script for a complete demonstration of the ExtendedModularAgent's capabilities. | |
| 82 | ||
| 83 | ## Extending Further | |
| 84 | ||
| 85 | The ExtendedModularAgent can be further extended with additional functionality: | |
| 86 | ||
| 87 | ```python | |
| 88 | from ollamatools.agent.extended import ExtendedModularAgent | |
| 89 | ||
| 90 | class SuperExtendedAgent(ExtendedModularAgent): | |
| 91 | """An even more extended agent with additional capabilities.""" | |
| 92 | ||
| 93 | def __init__(self, *args, **kwargs): | |
| 94 | super().__init__(*args, **kwargs) | |
| 95 | # Add custom initialization | |
| 96 | ||
| 97 | def custom_method(self): | |
| 98 | """Implement a custom method.""" | |
| 99 | # Your implementation here | |
| 100 | pass | |
| 101 | ``` | |
| 102 | ||
| 103 | ## Documentation | |
| 104 | ||
| 105 | For more detailed documentation, see: | |
| 106 | ||
| 107 | - [ModularOllamaAgent Guide](../../../../docs/modular_agent_guide.md): Comprehensive guide to using the ModularOllamaAgent | |
| 108 | - [Migration Guide](../../../../docs/migration_guide.md): Guide for migrating from TransparentOllamaAgent to ModularOllamaAgent | |
| 109 | - [Example Scripts](../../../../examples/): Example scripts demonstrating the ModularOllamaAgent and ExtendedModularAgent |
| … | … | @@ -0,0 +1,13 @@ |
| 1 | """ | |
| 2 | Extended ModularOllamaAgent package. | |
| 3 | ||
| 4 | This package provides extensions to the ModularOllamaAgent with additional | |
| 5 | features like conversation history tracking, conversation summarization, | |
| 6 | topic analysis, and performance metrics. | |
| 7 | """ | |
| 8 | ||
| 9 | # Import the ExtendedModularAgent from the example script | |
| 10 | # In a real implementation, this would be defined in a proper module file | |
| 11 | from examples.extended_modular_agent import ExtendedModularAgent | |
| 12 | ||
| 13 | __all__ = ['ExtendedModularAgent'] |
| … | … | @@ -0,0 +1,97 @@ |
| 1 | # ModularOllamaAgent | |
| 2 | ||
| 3 | This package provides a modular implementation of the OllamaAgent with enhanced capabilities like search integration, command management, issue tracking, and semantic function selection feedback. | |
| 4 | ||
| 5 | ## Module Structure | |
| 6 | ||
| 7 | The ModularOllamaAgent is organized into several modules: | |
| 8 | ||
| 9 | - `agent.py`: Main agent implementation | |
| 10 | - `search.py`: Search functionality | |
| 11 | - `commands.py`: Command management | |
| 12 | - `issues.py`: Issue tracking | |
| 13 | - `feedback.py`: Semantic function selection feedback | |
| 14 | - `utils.py`: Utility functions | |
| 15 | ||
| 16 | ## Architecture | |
| 17 | ||
| 18 | The ModularOllamaAgent follows a modular architecture where functionality is separated into logical components: | |
| 19 | ||
| 20 | ``` | |
| 21 | ModularOllamaAgent | |
| 22 | ├── Core Agent (agent.py) | |
| 23 | │ ├── Initialization | |
| 24 | │ ├── Chat Processing | |
| 25 | │ └── Error Handling | |
| 26 | │ | |
| 27 | ├── Search Module (search.py) | |
| 28 | │ ├── Intent Detection | |
| 29 | │ ├── Search Execution | |
| 30 | │ └── Result Processing | |
| 31 | │ | |
| 32 | ├── Commands Module (commands.py) | |
| 33 | │ ├── Command Listing | |
| 34 | │ ├── Command Addition | |
| 35 | │ └── Command Removal | |
| 36 | │ | |
| 37 | ├── Issues Module (issues.py) | |
| 38 | │ ├── Issue Loading/Saving | |
| 39 | │ ├── Issue Creation | |
| 40 | │ └── Issue Management | |
| 41 | │ | |
| 42 | ├── Feedback Module (feedback.py) | |
| 43 | │ ├── Feedback Collection | |
| 44 | │ ├── Pattern Analysis | |
| 45 | │ └── Command Intent Detection | |
| 46 | │ | |
| 47 | └── Utilities (utils.py) | |
| 48 | ├── Status Messages | |
| 49 | ├── Formatting Functions | |
| 50 | └── Error Suggestions | |
| 51 | ``` | |
| 52 | ||
| 53 | ## Usage | |
| 54 | ||
| 55 | To use the ModularOllamaAgent: | |
| 56 | ||
| 57 | ```python | |
| 58 | from ollamatools.agent import ModularOllamaAgent | |
| 59 | ||
| 60 | # Initialize the agent | |
| 61 | agent = ModularOllamaAgent( | |
| 62 | model="llama3.1:latest", | |
| 63 | permission_prompts=True | |
| 64 | ) | |
| 65 | ||
| 66 | # Chat with the agent | |
| 67 | response = agent.chat("Hello, how can you help me?") | |
| 68 | print(response) | |
| 69 | ``` | |
| 70 | ||
| 71 | ## Extending the Agent | |
| 72 | ||
| 73 | The modular architecture makes it easy to extend the agent with custom functionality: | |
| 74 | ||
| 75 | ```python | |
| 76 | from ollamatools.agent import ModularOllamaAgent | |
| 77 | ||
| 78 | class CustomAgent(ModularOllamaAgent): | |
| 79 | """A custom agent with additional capabilities.""" | |
| 80 | ||
| 81 | def __init__(self, *args, **kwargs): | |
| 82 | super().__init__(*args, **kwargs) | |
| 83 | # Add custom initialization | |
| 84 | ||
| 85 | def custom_method(self, param): | |
| 86 | """Implement a custom method.""" | |
| 87 | # Your implementation here | |
| 88 | return f"Processed {param}" | |
| 89 | ``` | |
| 90 | ||
| 91 | ## Documentation | |
| 92 | ||
| 93 | For more detailed documentation, see: | |
| 94 | ||
| 95 | - [ModularOllamaAgent Guide](../../../../docs/modular_agent_guide.md): Comprehensive guide to using the ModularOllamaAgent | |
| 96 | - [Migration Guide](../../../../docs/migration_guide.md): Guide for migrating from TransparentOllamaAgent to ModularOllamaAgent | |
| 97 | - [Example Scripts](../../../../examples/): Example scripts demonstrating the ModularOllamaAgent |
| … | … | @@ -0,0 +1,42 @@ |
| 1 | """ | |
| 2 | ModularOllamaAgent package. | |
| 3 | ||
| 4 | This package provides a modular implementation of the OllamaAgent with | |
| 5 | enhanced capabilities like search integration, command management, | |
| 6 | issue tracking, and semantic function selection feedback. | |
| 7 | """ | |
| 8 | ||
| 9 | from .agent import ModularOllamaAgent | |
| 10 | from .utils import ( | |
| 11 | print_status, print_success, print_error, | |
| 12 | print_warning, print_info, suggest_fix, | |
| 13 | format_code_block, format_table, truncate_text, | |
| 14 | format_execution_time | |
| 15 | ) | |
| 16 | from .feedback import FunctionSelectionFeedback | |
| 17 | from .search import detect_factual_intent, extract_search_context, execute_search | |
| 18 | from .commands import list_allowed_commands, add_allowed_command, remove_allowed_command | |
| 19 | from .issues import load_issues, save_issues, add_issue, list_issues, get_issue, update_issue | |
| 20 | ||
| 21 | __all__ = [ | |
| 22 | # Main agent class | |
| 23 | 'ModularOllamaAgent', | |
| 24 | ||
| 25 | # Utility functions | |
| 26 | 'print_status', 'print_success', 'print_error', | |
| 27 | 'print_warning', 'print_info', 'suggest_fix', | |
| 28 | 'format_code_block', 'format_table', 'truncate_text', | |
| 29 | 'format_execution_time', | |
| 30 | ||
| 31 | # Feedback system | |
| 32 | 'FunctionSelectionFeedback', | |
| 33 | ||
| 34 | # Search functions | |
| 35 | 'detect_factual_intent', 'extract_search_context', 'execute_search', | |
| 36 | ||
| 37 | # Command management functions | |
| 38 | 'list_allowed_commands', 'add_allowed_command', 'remove_allowed_command', | |
| 39 | ||
| 40 | # Issue tracking functions | |
| 41 | 'load_issues', 'save_issues', 'add_issue', 'list_issues', 'get_issue', 'update_issue' | |
| 42 | ] |
| … | … | @@ -0,0 +1,702 @@ |
| 1 | """ | |
| 2 | ModularOllamaAgent implementation. | |
| 3 | ||
| 4 | This module provides the ModularOllamaAgent class, which extends | |
| 5 | the base OllamaAgent with modular components, detailed feedback, and | |
| 6 | additional features like search integration, command management, | |
| 7 | issue tracking, and semantic function selection feedback. | |
| 8 | """ | |
| 9 | ||
| 10 | import os | |
| 11 | import sys | |
| 12 | import json | |
| 13 | import time | |
| 14 | import logging | |
| 15 | import re | |
| 16 | from typing import Dict, Any, List, Optional, Set, Union, Tuple | |
| 17 | ||
| 18 | from ollamatools import OllamaAgent, Config | |
| 19 | from ollamatools.command_validator import DEFAULT_ALLOWLIST | |
| 20 | from ollamatools.utils.logger import setup_logger | |
| 21 | from ollamatools.opensearch_web_tools import ( | |
| 22 | opensearch_query, | |
| 23 | opensearch_fetch_url, | |
| 24 | get_opensearch_info, | |
| 25 | DEFAULT_OPENSEARCH_CONFIG | |
| 26 | ) | |
| 27 | from ollamatools.errors import ( | |
| 28 | OllamaToolsError, | |
| 29 | CommandError, | |
| 30 | FileError, | |
| 31 | WebError, | |
| 32 | handle_exception | |
| 33 | ) | |
| 34 | from ollamatools.tools.system_tools import execute_command | |
| 35 | ||
| 36 | # Import agent components | |
| 37 | from .search import detect_factual_intent, extract_search_context, execute_search | |
| 38 | from .commands import list_allowed_commands, add_allowed_command, remove_allowed_command | |
| 39 | from .issues import load_issues, save_issues, add_issue, list_issues, get_issue, update_issue | |
| 40 | from .utils import print_status, print_success, print_error, print_warning, print_info, suggest_fix | |
| 41 | from .feedback import ( | |
| 42 | FunctionSelectionFeedback, | |
| 43 | detect_command_intent, | |
| 44 | extract_command_from_similar_queries, | |
| 45 | format_command_result | |
| 46 | ) | |
| 47 | ||
| 48 | # Set up logging | |
| 49 | logger = setup_logger("modular_ollama_agent", level="INFO") | |
| 50 | ||
| 51 | # Extend the default command allowlist with useful commands | |
| 52 | EXTENDED_ALLOWLIST = DEFAULT_ALLOWLIST.union({ | |
| 53 | "python", "python3", "calc", "bc", "find", "grep", "awk", "sed", | |
| 54 | "head", "tail", "more", "less", "cat", "echo", "cd", "pwd" | |
| 55 | }) | |
| 56 | ||
| 57 | # Configuration for OpenSearchNG | |
| 58 | OPENSEARCH_CONFIG = DEFAULT_OPENSEARCH_CONFIG.copy() | |
| 59 | OPENSEARCH_CONFIG.update({ | |
| 60 | "base_url": "http://localhost:8081", # Updated to your container's port | |
| 61 | "engines": ["google", "bing", "duckduckgo", "brave"], | |
| 62 | "num_results": 5 | |
| 63 | }) | |
| 64 | ||
| 65 | class ModularOllamaAgent: | |
| 66 | """ | |
| 67 | A modular OllamaAgent that provides transparent execution and better feedback. | |
| 68 | ||
| 69 | This agent is designed with a modular architecture that separates functionality | |
| 70 | into logical components for better maintainability and extensibility. It includes | |
| 71 | features like search integration, command management, issue tracking, and | |
| 72 | semantic function selection feedback. | |
| 73 | """ | |
| 74 | ||
| 75 | def __init__( | |
| 76 | self, | |
| 77 | model: str = "llama3.1:latest", | |
| 78 | permission_prompts: bool = True, | |
| 79 | opensearch_config: Optional[Dict[str, Any]] = None, | |
| 80 | command_allowlist: Optional[Set[str]] = None, | |
| 81 | feedback_file_path: Optional[str] = None | |
| 82 | ): | |
| 83 | """ | |
| 84 | Initialize the agent. | |
| 85 | ||
| 86 | Args: | |
| 87 | model: Ollama model to use | |
| 88 | permission_prompts: Whether to display permission prompts | |
| 89 | opensearch_config: OpenSearchNG configuration | |
| 90 | command_allowlist: Custom command allowlist | |
| 91 | feedback_file_path: Path to the feedback file (default: ~/.ollamatools/function_selection_feedback.jsonl) | |
| 92 | """ | |
| 93 | # Initialize OllamaAgent | |
| 94 | self.agent = OllamaAgent( | |
| 95 | model=model, | |
| 96 | permission_prompts=permission_prompts | |
| 97 | ) | |
| 98 | ||
| 99 | # Store configuration | |
| 100 | self.opensearch_config = opensearch_config or OPENSEARCH_CONFIG | |
| 101 | self.command_allowlist = command_allowlist or EXTENDED_ALLOWLIST | |
| 102 | ||
| 103 | # Initialize state | |
| 104 | self.context = [] | |
| 105 | self.execution_history = [] | |
| 106 | ||
| 107 | # Initialize issue tracking | |
| 108 | self.issues = [] | |
| 109 | self.issue_id_counter = 1 | |
| 110 | ||
| 111 | # Path for saving issues | |
| 112 | self.issues_file = os.path.join( | |
| 113 | os.path.expanduser('~'), | |
| 114 | '.ollamatools', | |
| 115 | 'issues.json' | |
| 116 | ) | |
| 117 | ||
| 118 | # Create directory if it doesn't exist | |
| 119 | os.makedirs(os.path.dirname(self.issues_file), exist_ok=True) | |
| 120 | ||
| 121 | # Load existing issues if any | |
| 122 | self.load_issues() | |
| 123 | ||
| 124 | # Initialize function selection feedback system | |
| 125 | self.function_feedback = FunctionSelectionFeedback(feedback_file_path) | |
| 126 | logger.info("Initialized function selection feedback system") | |
| 127 | ||
| 128 | # Define system prompt | |
| 129 | self.system_prompt = f""" | |
| 130 | You are an AI assistant with modular access to various tools. | |
| 131 | You always explain what you're doing step by step and show your work. | |
| 132 | ||
| 133 | When a user asks you a question, you should: | |
| 134 | 1. Identify the most appropriate tool for the task | |
| 135 | 2. Describe what you're going to do before you do it | |
| 136 | 3. Show the results of any commands or functions you execute | |
| 137 | 4. Explain how you interpret these results | |
| 138 | ||
| 139 | MOST IMPORTANTLY: When calculating math problems, ALWAYS use the 'calc' command rather than | |
| 140 | doing the calculation yourself or using python -c. For example: calc 2+2 | |
| 141 | ||
| 142 | For searching the web: | |
| 143 | - Use the 'opensearch_query' function to perform web searches | |
| 144 | - Use the 'opensearch_fetch_url' function to fetch web page content safely | |
| 145 | - Specify search engines if needed (available: {', '.join(self.opensearch_config['engines'])}) | |
| 146 | ||
| 147 | For file and system operations: | |
| 148 | - Use 'find_files' to locate files | |
| 149 | - Use 'read_file' to view file content | |
| 150 | - Use 'execute_command' for running shell commands like 'ls', 'pwd', etc. | |
| 151 | ||
| 152 | For system information queries: | |
| 153 | - When a user asks about the current directory, use 'pwd' command | |
| 154 | - When a user asks about file listings, use 'ls' command | |
| 155 | - When a user asks about the current user, use 'whoami' command | |
| 156 | - When a user asks about system information, prefer shell commands over other methods | |
| 157 | ||
| 158 | Always check command permissions carefully and explain what each command does. | |
| 159 | For directory structure, use 'find .' or 'ls -la' commands. | |
| 160 | ||
| 161 | When a user asks for information that might require searching the web or retrieving external data, | |
| 162 | automatically determine if you should use the 'opensearch_query' function instead of requiring explicit search commands. | |
| 163 | ||
| 164 | For example, if a user asks "can you give me a recipe for chilli sauce?", you should: | |
| 165 | 1. Recognize this as a request that requires external information | |
| 166 | 2. Use the opensearch_query function with an appropriate search query | |
| 167 | 3. Present the results in a helpful format | |
| 168 | ||
| 169 | Only use tools when necessary - for factual questions you can answer directly, don't use search. | |
| 170 | """ | |
| 171 | ||
| 172 | # Check if OpenSearchNG is available | |
| 173 | try: | |
| 174 | self.print_status("Checking OpenSearchNG availability...") | |
| 175 | info = get_opensearch_info(config=self.opensearch_config) | |
| 176 | self.print_success(f"OpenSearchNG running at {self.opensearch_config['base_url']}") | |
| 177 | ||
| 178 | # Log available engines | |
| 179 | if 'engines' in info and info['engines']: | |
| 180 | self.print_info(f"Available search engines: {', '.join(info['engines'])}") | |
| 181 | except Exception as e: | |
| 182 | self.print_error(f"OpenSearchNG not available: {str(e)}") | |
| 183 | self.print_warning("Falling back to regular search methods") | |
| 184 | self.suggest_fix("Make sure your OpenSearchNG instance is running at " + | |
| 185 | f"{self.opensearch_config['base_url']}") | |
| 186 | ||
| 187 | # Utility methods redirected to utils module | |
| 188 | def print_status(self, message: str) -> None: | |
| 189 | print_status(message) | |
| 190 | ||
| 191 | def print_success(self, message: str) -> None: | |
| 192 | print_success(message) | |
| 193 | ||
| 194 | def print_error(self, message: str) -> None: | |
| 195 | print_error(message) | |
| 196 | ||
| 197 | def print_warning(self, message: str) -> None: | |
| 198 | print_warning(message) | |
| 199 | ||
| 200 | def print_info(self, message: str) -> None: | |
| 201 | print_info(message) | |
| 202 | ||
| 203 | def suggest_fix(self, suggestion: str) -> None: | |
| 204 | suggest_fix(suggestion) | |
| 205 | ||
| 206 | # Search methods redirected to search module | |
| 207 | def detect_factual_intent(self, message: str) -> bool: | |
| 208 | return detect_factual_intent(message) | |
| 209 | ||
| 210 | def extract_search_context(self, search_results: Dict[str, Any]) -> str: | |
| 211 | return extract_search_context(search_results) | |
| 212 | ||
| 213 | def execute_search(self, query: str) -> Dict[str, Any]: | |
| 214 | return execute_search(query, self.opensearch_config, self.agent.permission_manager, self.execution_history) | |
| 215 | ||
| 216 | # Command management methods redirected to commands module | |
| 217 | def list_allowed_commands(self) -> List[str]: | |
| 218 | return list_allowed_commands(self.command_allowlist) | |
| 219 | ||
| 220 | def add_allowed_command(self, command: str) -> bool: | |
| 221 | return add_allowed_command(command, self.command_allowlist) | |
| 222 | ||
| 223 | def remove_allowed_command(self, command: str) -> bool: | |
| 224 | return remove_allowed_command(command, self.command_allowlist) | |
| 225 | ||
| 226 | # Issue tracking methods redirected to issues module | |
| 227 | def load_issues(self) -> None: | |
| 228 | load_issues(self) | |
| 229 | ||
| 230 | def save_issues(self) -> None: | |
| 231 | save_issues(self) | |
| 232 | ||
| 233 | def add_issue(self, title: str, description: str, severity: str = 'medium') -> int: | |
| 234 | return add_issue(self, title, description, severity) | |
| 235 | ||
| 236 | def list_issues(self, status: Optional[str] = None) -> List[Dict[str, Any]]: | |
| 237 | return list_issues(self, status) | |
| 238 | ||
| 239 | def get_issue(self, issue_id: int) -> Optional[Dict[str, Any]]: | |
| 240 | return get_issue(self, issue_id) | |
| 241 | ||
| 242 | def update_issue(self, issue_id: int, status: Optional[str] = None, | |
| 243 | severity: Optional[str] = None, description: Optional[str] = None) -> bool: | |
| 244 | return update_issue(self, issue_id, status, severity, description) | |
| 245 | ||
| 246 | def show_execution_history(self, max_entries: int = 10) -> None: | |
| 247 | """ | |
| 248 | Display the execution history. | |
| 249 | ||
| 250 | Args: | |
| 251 | max_entries: Maximum number of history entries to show | |
| 252 | """ | |
| 253 | if not self.execution_history: | |
| 254 | print("\n📜 Execution history is empty.") | |
| 255 | return | |
| 256 | ||
| 257 | # Get the most recent entries | |
| 258 | recent_entries = self.execution_history[-max_entries:] | |
| 259 | ||
| 260 | print("\n📜 Execution History:") | |
| 261 | print("=" * 60) | |
| 262 | ||
| 263 | for i, entry in enumerate(recent_entries, 1): | |
| 264 | # Format timestamp | |
| 265 | timestamp = time.strftime("%H:%M:%S", time.localtime(entry["timestamp"])) | |
| 266 | ||
| 267 | # Format function and args | |
| 268 | function_name = entry["function"] | |
| 269 | args_str = ", ".join(f"{k}={v!r}" for k, v in entry["args"].items()) | |
| 270 | ||
| 271 | # Format status | |
| 272 | status = "✅ Success" if entry.get("success", False) else "❌ Failed" | |
| 273 | ||
| 274 | # Print the entry | |
| 275 | print(f"{i}. [{timestamp}] {function_name}({args_str}) -> {status}") | |
| 276 | ||
| 277 | # Show additional details for errors | |
| 278 | if not entry.get("success", False) and "error" in entry: | |
| 279 | print(f" Error: {entry['error']}") | |
| 280 | ||
| 281 | # Show additional details for commands | |
| 282 | if function_name == "execute_command" and "returncode" in entry: | |
| 283 | print(f" Exit code: {entry['returncode']}") | |
| 284 | ||
| 285 | # Show additional details for search | |
| 286 | if function_name == "opensearch_query" and "num_results" in entry: | |
| 287 | print(f" Results: {entry['num_results']}") | |
| 288 | ||
| 289 | print("=" * 60) | |
| 290 | ||
| 291 | def get_directory_structure(self, path: str = '.', max_depth: int = 2) -> Dict[str, Any]: | |
| 292 | """ | |
| 293 | Get a formatted directory structure. | |
| 294 | ||
| 295 | Args: | |
| 296 | path: Starting directory path | |
| 297 | max_depth: Maximum directory depth to show | |
| 298 | ||
| 299 | Returns: | |
| 300 | dict: Directory structure information | |
| 301 | """ | |
| 302 | try: | |
| 303 | self.print_status(f"Getting directory structure for: {path} (depth: {max_depth})") | |
| 304 | ||
| 305 | # Execute find command to get the structure | |
| 306 | from ollamatools.tools.system_tools import execute_command | |
| 307 | result = execute_command( | |
| 308 | command="find", | |
| 309 | args=[path, "-maxdepth", str(max_depth), "-type", "f", "-o", "-type", "d"], | |
| 310 | permission_manager=self.agent.permission_manager | |
| 311 | ) | |
| 312 | ||
| 313 | if "error" in result: | |
| 314 | return result | |
| 315 | ||
| 316 | # Process the output into a more structured format | |
| 317 | lines = result.get("stdout", "").strip().split('\n') | |
| 318 | ||
| 319 | structure = { | |
| 320 | "path": path, | |
| 321 | "depth": max_depth, | |
| 322 | "entries": lines, | |
| 323 | "files": [line for line in lines if os.path.isfile(line)], | |
| 324 | "directories": [line for line in lines if os.path.isdir(line)], | |
| 325 | "count": { | |
| 326 | "total": len(lines), | |
| 327 | "files": len([line for line in lines if os.path.isfile(line)]), | |
| 328 | "directories": len([line for line in lines if os.path.isdir(line)]) | |
| 329 | } | |
| 330 | } | |
| 331 | ||
| 332 | self.print_success(f"Found {structure['count']['total']} entries " | |
| 333 | f"({structure['count']['files']} files, " | |
| 334 | f"{structure['count']['directories']} directories)") | |
| 335 | ||
| 336 | return structure | |
| 337 | ||
| 338 | except Exception as e: | |
| 339 | self.print_error(f"Failed to get directory structure: {str(e)}") | |
| 340 | self.suggest_fix(self._generate_error_fix_suggestion(e)) | |
| 341 | ||
| 342 | return { | |
| 343 | "error": str(e), | |
| 344 | "path": path, | |
| 345 | "depth": max_depth | |
| 346 | } | |
| 347 | ||
| 348 | def _generate_error_fix_suggestion(self, error: Exception) -> str: | |
| 349 | """ | |
| 350 | Generate a fix suggestion based on the error type. | |
| 351 | ||
| 352 | Args: | |
| 353 | error: The exception | |
| 354 | ||
| 355 | Returns: | |
| 356 | str: Suggested fix | |
| 357 | """ | |
| 358 | if isinstance(error, CommandError): | |
| 359 | return self._generate_command_fix_suggestion(error) | |
| 360 | elif isinstance(error, FileError): | |
| 361 | return self._generate_file_fix_suggestion(error) | |
| 362 | elif isinstance(error, WebError): | |
| 363 | return self._generate_web_fix_suggestion(error) | |
| 364 | else: | |
| 365 | # Generic suggestion | |
| 366 | return f"Try a different approach or check if the required resources are available." | |
| 367 | ||
| 368 | def _generate_command_fix_suggestion(self, error: Union[CommandError, Dict[str, Any]]) -> str: | |
| 369 | """ | |
| 370 | Generate a fix suggestion for command errors. | |
| 371 | ||
| 372 | Args: | |
| 373 | error: Error object or dictionary | |
| 374 | ||
| 375 | Returns: | |
| 376 | str: Suggested fix | |
| 377 | """ | |
| 378 | # Extract error details | |
| 379 | if isinstance(error, CommandError): | |
| 380 | error_msg = str(error) | |
| 381 | details = error.details or {} | |
| 382 | command = details.get('command', '') | |
| 383 | stderr = details.get('stderr', '') | |
| 384 | else: | |
| 385 | error_msg = error.get('error', '') | |
| 386 | command = error.get('command', '') | |
| 387 | stderr = error.get('stderr', '') | |
| 388 | ||
| 389 | # Check for common error patterns | |
| 390 | if "not in allowed command list" in error_msg: | |
| 391 | return (f"The command '{command}' is not in the allowed command list. " | |
| 392 | f"Try using one of these commands instead: {', '.join(sorted(list(self.command_allowlist)[:10]))}... " | |
| 393 | f"or add this command to the allowlist when initializing the agent.") | |
| 394 | ||
| 395 | elif "dangerous pattern" in error_msg: | |
| 396 | pattern = error_msg.split("pattern matching '")[1].split("'")[0] if "pattern matching '" in error_msg else "" | |
| 397 | return (f"The command contains potentially dangerous pattern: {pattern}. " | |
| 398 | f"Try reformulating the command to avoid shell metacharacters.") | |
| 399 | ||
| 400 | elif "not found" in error_msg or "command not found" in stderr: | |
| 401 | return f"The command '{command}' does not exist on this system. Check the spelling or install the command." | |
| 402 | ||
| 403 | elif "permission denied" in stderr.lower(): | |
| 404 | return f"Permission denied when executing '{command}'. Try running with appropriate permissions." | |
| 405 | ||
| 406 | else: | |
| 407 | return f"Try a different command syntax or check command documentation." | |
| 408 | ||
| 409 | def _generate_file_fix_suggestion(self, error: FileError) -> str: | |
| 410 | """ | |
| 411 | Generate a fix suggestion for file errors. | |
| 412 | ||
| 413 | Args: | |
| 414 | error: File error | |
| 415 | ||
| 416 | Returns: | |
| 417 | str: Suggested fix | |
| 418 | """ | |
| 419 | error_msg = str(error) | |
| 420 | details = error.details or {} | |
| 421 | path = details.get('path', '') | |
| 422 | ||
| 423 | if "not found" in error_msg: | |
| 424 | return f"The file '{path}' does not exist. Check the path or create the file." | |
| 425 | ||
| 426 | elif "is a directory" in error_msg: | |
| 427 | return f"'{path}' is a directory, not a file. Try specifying a file path instead." | |
| 428 | ||
| 429 | elif "permission denied" in error_msg.lower(): | |
| 430 | return f"Permission denied when accessing '{path}'. Check file permissions." | |
| 431 | ||
| 432 | elif "exceeds maximum size" in error_msg: | |
| 433 | return f"The file '{path}' is too large. Try reading a smaller file or increasing max_size." | |
| 434 | ||
| 435 | elif "outside the allowed directories" in error_msg: | |
| 436 | return f"The path '{path}' is outside the allowed directories. Try a path within the sandbox." | |
| 437 | ||
| 438 | else: | |
| 439 | return f"Check if the file exists and you have appropriate permissions." | |
| 440 | ||
| 441 | def _generate_web_fix_suggestion(self, error: WebError) -> str: | |
| 442 | """ | |
| 443 | Generate a fix suggestion for web errors. | |
| 444 | ||
| 445 | Args: | |
| 446 | error: Web error | |
| 447 | ||
| 448 | Returns: | |
| 449 | str: Suggested fix | |
| 450 | """ | |
| 451 | error_msg = str(error) | |
| 452 | details = error.details or {} | |
| 453 | url = details.get('url', '') | |
| 454 | ||
| 455 | if "domain" in error_msg and "blocked" in error_msg: | |
| 456 | return f"The domain in '{url}' is blocked for security reasons. Try a different domain." | |
| 457 | ||
| 458 | elif "scheme" in error_msg and "not allowed" in error_msg: | |
| 459 | return f"The URL scheme is not allowed. Use http:// or https:// instead." | |
| 460 | ||
| 461 | elif "Connection refused" in error_msg: | |
| 462 | return f"Could not connect to '{url}'. Check if the service is running and accessible." | |
| 463 | ||
| 464 | elif "timeout" in error_msg.lower(): | |
| 465 | return f"The request to '{url}' timed out. Try again later or check your internet connection." | |
| 466 | ||
| 467 | elif "Status code:" in error_msg and "404" in error_msg: | |
| 468 | return f"The resource at '{url}' was not found (404). Check the URL for typos." | |
| 469 | ||
| 470 | elif "Status code:" in error_msg and "403" in error_msg: | |
| 471 | return f"Access to '{url}' is forbidden (403). The site may require authentication." | |
| 472 | ||
| 473 | else: | |
| 474 | return f"Try a different URL or check your internet connection." | |
| 475 | ||
| 476 | def detect_command_intent(self, message: str) -> Tuple[bool, Optional[str]]: | |
| 477 | """ | |
| 478 | Detect if a message is asking for system information that requires a command. | |
| 479 | ||
| 480 | Args: | |
| 481 | message: User message | |
| 482 | ||
| 483 | Returns: | |
| 484 | Tuple[bool, Optional[str]]: (has_command_intent, suggested_command) | |
| 485 | """ | |
| 486 | return detect_command_intent(message, self.function_feedback) | |
| 487 | ||
| 488 | def provide_function_feedback(self, query: str, correct_function: str, actual_function: str = None) -> bool: | |
| 489 | """ | |
| 490 | Allow users to provide feedback on function selection. | |
| 491 | ||
| 492 | Args: | |
| 493 | query: The original query | |
| 494 | correct_function: The function that should have been used | |
| 495 | actual_function: The function that was actually used (if known) | |
| 496 | ||
| 497 | Returns: | |
| 498 | bool: True if feedback was recorded successfully | |
| 499 | """ | |
| 500 | if actual_function: | |
| 501 | # Record explicit feedback | |
| 502 | self.function_feedback.record_explicit_feedback( | |
| 503 | query, correct_function, actual_function, user_correction=True | |
| 504 | ) | |
| 505 | self.print_success(f"Thank you for your feedback! I'll remember to use {correct_function} for similar queries in the future.") | |
| 506 | return True | |
| 507 | else: | |
| 508 | # Try to find the most recent function used for this query | |
| 509 | for entry in reversed(self.execution_history): | |
| 510 | if entry.get("query", "") == query: | |
| 511 | actual_function = entry.get("function") | |
| 512 | if actual_function and actual_function != correct_function: | |
| 513 | self.function_feedback.record_explicit_feedback( | |
| 514 | query, correct_function, actual_function, user_correction=True | |
| 515 | ) | |
| 516 | self.print_success(f"Thank you for your feedback! I'll remember to use {correct_function} for similar queries in the future.") | |
| 517 | return True | |
| 518 | ||
| 519 | # If we couldn't find a matching entry, just record the correct function | |
| 520 | self.function_feedback.record_explicit_feedback( | |
| 521 | query, correct_function, "unknown", user_correction=True | |
| 522 | ) | |
| 523 | self.print_success(f"Thank you for your feedback! I'll remember to use {correct_function} for similar queries in the future.") | |
| 524 | return True | |
| 525 | ||
| 526 | def chat(self, message: str) -> str: | |
| 527 | """ | |
| 528 | Process a chat message, showing the execution steps transparently. | |
| 529 | ||
| 530 | Args: | |
| 531 | message: User message | |
| 532 | ||
| 533 | Returns: | |
| 534 | str: Response message | |
| 535 | """ | |
| 536 | try: | |
| 537 | # First check if the message is asking for system information | |
| 538 | has_command_intent, suggested_command = self.detect_command_intent(message) | |
| 539 | if has_command_intent and suggested_command: | |
| 540 | self.print_info(f"This appears to be a question about system information. Using '{suggested_command}' command...") | |
| 541 | try: | |
| 542 | # Execute the command | |
| 543 | result = execute_command( | |
| 544 | command=suggested_command, | |
| 545 | permission_manager=self.agent.permission_manager | |
| 546 | ) | |
| 547 | ||
| 548 | # Record this in execution history | |
| 549 | self.execution_history.append({ | |
| 550 | "function": "execute_command", | |
| 551 | "args": {"command": suggested_command}, | |
| 552 | "timestamp": time.time(), | |
| 553 | "success": "error" not in result, | |
| 554 | "returncode": result.get("returncode"), | |
| 555 | "query": message | |
| 556 | }) | |
| 557 | ||
| 558 | # Record implicit feedback | |
| 559 | self.function_feedback.record_implicit_feedback( | |
| 560 | message, "execute_command", 1.0, result | |
| 561 | ) | |
| 562 | ||
| 563 | if "error" in result: | |
| 564 | self.print_error(f"Command execution failed: {result['error']}") | |
| 565 | return f"I tried to run the '{suggested_command}' command to answer your question, but encountered an error: {result['error']}" | |
| 566 | else: | |
| 567 | # Format the response based on the command | |
| 568 | formatted_result = format_command_result(suggested_command, result) | |
| 569 | return formatted_result | |
| 570 | except Exception as e: | |
| 571 | self.print_error(f"Command execution failed: {str(e)}") | |
| 572 | return f"I tried to run the '{suggested_command}' command to answer your question, but encountered an error: {str(e)}" | |
| 573 | ||
| 574 | # If not a command, check if the message likely requires a search but doesn't explicitly ask for one | |
| 575 | if not message.lower().startswith('search '): | |
| 576 | search_indicators = [ | |
| 577 | "what is", "how to", "who is", "where can", "when did", "why does", | |
| 578 | "recipe for", "information about", "tell me about", "find", "look up", | |
| 579 | "latest", "news on", "current", "recent" | |
| 580 | ] | |
| 581 | ||
| 582 | # Check if the message contains search indicators | |
| 583 | needs_search = any(indicator in message.lower() for indicator in search_indicators) | |
| 584 | ||
| 585 | # Check for question structure | |
| 586 | question_structure = message.lower().startswith(("what", "how", "who", "where", "when", "why", "can", "could")) | |
| 587 | ||
| 588 | if needs_search or question_structure: | |
| 589 | # Check if message is asking for external information we likely don't have | |
| 590 | factual_intent = self.detect_factual_intent(message) | |
| 591 | if factual_intent: | |
| 592 | self.print_info("This appears to be a question that might benefit from search. Searching automatically...") | |
| 593 | try: | |
| 594 | results = self.execute_search(message) | |
| 595 | # Process search results before continuing with normal chat flow | |
| 596 | context = self.extract_search_context(results) | |
| 597 | # Add context to the user message | |
| 598 | message = f"{message}\n\nSearch context: {context}" | |
| 599 | ||
| 600 | # Record implicit feedback | |
| 601 | self.function_feedback.record_implicit_feedback( | |
| 602 | message, "opensearch_query", 0.9, results | |
| 603 | ) | |
| 604 | except Exception as e: | |
| 605 | self.print_error(f"Automatic search failed: {str(e)}") | |
| 606 | self.print_info("Continuing without search results") | |
| 607 | ||
| 608 | # Print what we're doing | |
| 609 | self.print_status(f"Processing message: {message}") | |
| 610 | ||
| 611 | # Start timer for perf measurement | |
| 612 | start_time = time.time() | |
| 613 | ||
| 614 | # Process LLM response | |
| 615 | try: | |
| 616 | response = self.agent.chat( | |
| 617 | message=message, | |
| 618 | system_prompt=self.system_prompt, | |
| 619 | context=self.context | |
| 620 | ) | |
| 621 | ||
| 622 | # Check if the response looks like a JSON function call | |
| 623 | if response.strip().startswith('{') and ('function' in response or 'name' in response): | |
| 624 | self.print_info("Detected potential function call in response") | |
| 625 | try: | |
| 626 | # Parse the JSON response | |
| 627 | func_data = json.loads(response) | |
| 628 | ||
| 629 | # Extract function name and args | |
| 630 | function_name = None | |
| 631 | args = {} | |
| 632 | ||
| 633 | # Handle different JSON formats | |
| 634 | if 'function' in func_data: | |
| 635 | function_name = func_data['function'].get('name') | |
| 636 | args_str = func_data['function'].get('arguments', '{}') | |
| 637 | if isinstance(args_str, str): | |
| 638 | args = json.loads(args_str) | |
| 639 | else: | |
| 640 | args = args_str | |
| 641 | elif 'name' in func_data: | |
| 642 | function_name = func_data.get('name') | |
| 643 | args = func_data.get('arguments', {}) | |
| 644 | ||
| 645 | if function_name: | |
| 646 | self.print_status(f"Executing function: {function_name} with args: {args}") | |
| 647 | ||
| 648 | # Execute the function | |
| 649 | if function_name == 'opensearch_query' or function_name == 'search_web': | |
| 650 | results = self.execute_search(args.get('query', message)) | |
| 651 | context = self.extract_search_context(results) | |
| 652 | response = f"Here's what I found about '{args.get('query', message)}':\n\n{context}" | |
| 653 | elif hasattr(self, function_name) and callable(getattr(self, function_name)): | |
| 654 | # Execute a method of this class | |
| 655 | method = getattr(self, function_name) | |
| 656 | result = method(**args) | |
| 657 | response = f"Here are the results for '{function_name}':\n\n{result}" | |
| 658 | elif function_name in self.agent.registered_functions: | |
| 659 | # Execute a registered function | |
| 660 | result = self.agent.registered_functions[function_name](**args) | |
| 661 | response = f"Here are the results for '{function_name}':\n\n{result}" | |
| 662 | else: | |
| 663 | self.print_error(f"Function '{function_name}' not found") | |
| 664 | response = f"I tried to call a function '{function_name}' but it's not available. I'll try to help you directly instead." | |
| 665 | except json.JSONDecodeError: | |
| 666 | self.print_info("Response looked like JSON but couldn't be parsed as a function call") | |
| 667 | except Exception as e: | |
| 668 | self.print_error(f"Error executing function from response: {str(e)}") | |
| 669 | response = f"I tried to execute a function to help with your request, but encountered an error: {str(e)}" | |
| 670 | except Exception as e: | |
| 671 | self.print_error(f"Error communicating with Ollama: {str(e)}") | |
| 672 | response = f"I encountered an error communicating with the language model: {str(e)}" | |
| 673 | ||
| 674 | # Update context | |
| 675 | self.context.append({"role": "user", "content": message}) | |
| 676 | self.context.append({"role": "assistant", "content": response}) | |
| 677 | ||
| 678 | # Limit context size | |
| 679 | if len(self.context) > 10: | |
| 680 | self.context = self.context[-10:] | |
| 681 | ||
| 682 | # Calculate execution time | |
| 683 | execution_time = time.time() - start_time | |
| 684 | self.print_success(f"Response generated in {execution_time:.2f} seconds") | |
| 685 | ||
| 686 | return response | |
| 687 | ||
| 688 | except OllamaToolsError as e: | |
| 689 | error_details = handle_exception(e, log_error=True) | |
| 690 | ||
| 691 | self.print_error(f"Error: {error_details['error_message']}") | |
| 692 | self.suggest_fix(self._generate_error_fix_suggestion(e)) | |
| 693 | ||
| 694 | return f"I encountered an error: {error_details['error_message']}\n\n" + \ | |
| 695 | f"Suggested fix: {self._generate_error_fix_suggestion(e)}" | |
| 696 | ||
| 697 | except Exception as e: | |
| 698 | error_details = handle_exception(e, log_error=True) | |
| 699 | ||
| 700 | self.print_error(f"Unexpected error: {str(e)}") | |
| 701 | ||
| 702 | return f"I encountered an unexpected error: {str(e)}" |
| … | … | @@ -0,0 +1,122 @@ |
| 1 | """ | |
| 2 | Command management functionality for the ModularOllamaAgent. | |
| 3 | ||
| 4 | This module provides functions for managing the command allowlist, | |
| 5 | including listing, adding, and removing commands. | |
| 6 | """ | |
| 7 | ||
| 8 | from typing import List, Set | |
| 9 | from .utils import print_info, print_error, print_success, print_warning | |
| 10 | ||
| 11 | def list_allowed_commands(command_allowlist: Set[str]) -> List[str]: | |
| 12 | """ | |
| 13 | List all currently allowed commands. | |
| 14 | ||
| 15 | Args: | |
| 16 | command_allowlist: Set of currently allowed commands | |
| 17 | ||
| 18 | Returns: | |
| 19 | List[str]: List of allowed commands | |
| 20 | """ | |
| 21 | # Get the current allowlist | |
| 22 | all_commands = sorted(list(command_allowlist)) | |
| 23 | ||
| 24 | # Print the commands | |
| 25 | print_info(f"Currently allowed commands ({len(all_commands)}):") | |
| 26 | ||
| 27 | # Format into categories for better readability | |
| 28 | categories = { | |
| 29 | "File Operations": ["ls", "cat", "find", "grep", "head", "tail"], | |
| 30 | "Directory Operations": ["pwd", "cd", "mkdir", "rmdir"], | |
| 31 | "System Information": ["uname", "hostname", "whoami", "ps", "top", "df", "du", "free"], | |
| 32 | "Network Utilities": ["ping", "curl", "wget", "netstat", "dig", "nslookup"], | |
| 33 | "Text Processing": ["echo", "printf", "sed", "awk", "cut", "sort", "uniq", "wc"], | |
| 34 | "File Manipulation": ["cp", "mv", "rm", "touch", "chmod", "chown", "chgrp", "ln"], | |
| 35 | "Other": [] | |
| 36 | } | |
| 37 | ||
| 38 | # Categorize commands | |
| 39 | for cmd in all_commands: | |
| 40 | categorized = False | |
| 41 | for category, commands in categories.items(): | |
| 42 | if cmd in commands: | |
| 43 | categorized = True | |
| 44 | break | |
| 45 | if not categorized: | |
| 46 | categories["Other"].append(cmd) | |
| 47 | ||
| 48 | # Print by category | |
| 49 | for category, commands in categories.items(): | |
| 50 | if commands: | |
| 51 | print(f"\n{category}:") | |
| 52 | for i, cmd in enumerate(sorted(commands)): | |
| 53 | print(f" {cmd}", end="\t") | |
| 54 | # New line every 5 commands for readability | |
| 55 | if (i + 1) % 5 == 0: | |
| 56 | print() | |
| 57 | print() # Ensure we end with a newline | |
| 58 | ||
| 59 | return all_commands | |
| 60 | ||
| 61 | def add_allowed_command(command: str, command_allowlist: Set[str]) -> bool: | |
| 62 | """ | |
| 63 | Add a command to the allowed list. | |
| 64 | ||
| 65 | Args: | |
| 66 | command: Command to add | |
| 67 | command_allowlist: Set of currently allowed commands | |
| 68 | ||
| 69 | Returns: | |
| 70 | bool: True if added successfully | |
| 71 | """ | |
| 72 | if not command or not isinstance(command, str): | |
| 73 | print_error("Invalid command name") | |
| 74 | return False | |
| 75 | ||
| 76 | # Clean up the command name | |
| 77 | command = command.strip().lower() | |
| 78 | ||
| 79 | # Check if already in allowlist | |
| 80 | if command in command_allowlist: | |
| 81 | print_info(f"Command '{command}' is already allowed") | |
| 82 | return True | |
| 83 | ||
| 84 | # Add to custom allowlist | |
| 85 | command_allowlist.add(command) | |
| 86 | print_success(f"Command '{command}' added to allowed list") | |
| 87 | ||
| 88 | # Show warning for potentially dangerous commands | |
| 89 | dangerous_commands = {"rm", "mv", "chmod", "chown", "dd", "mkfs", "iptables"} | |
| 90 | if command in dangerous_commands: | |
| 91 | print_warning(f"'{command}' is a potentially dangerous command. Use with caution!") | |
| 92 | ||
| 93 | return True | |
| 94 | ||
| 95 | def remove_allowed_command(command: str, command_allowlist: Set[str]) -> bool: | |
| 96 | """ | |
| 97 | Remove a command from the allowed list. | |
| 98 | ||
| 99 | Args: | |
| 100 | command: Command to remove | |
| 101 | command_allowlist: Set of currently allowed commands | |
| 102 | ||
| 103 | Returns: | |
| 104 | bool: True if removed successfully | |
| 105 | """ | |
| 106 | if not command or not isinstance(command, str): | |
| 107 | print_error("Invalid command name") | |
| 108 | return False | |
| 109 | ||
| 110 | # Clean up the command name | |
| 111 | command = command.strip().lower() | |
| 112 | ||
| 113 | # Check if in allowlist | |
| 114 | if command not in command_allowlist: | |
| 115 | print_error(f"Command '{command}' is not in the allowed list") | |
| 116 | return False | |
| 117 | ||
| 118 | # Remove from custom allowlist | |
| 119 | command_allowlist.remove(command) | |
| 120 | print_success(f"Command '{command}' removed from allowed list") | |
| 121 | ||
| 122 | return True |
| … | … | @@ -0,0 +1,586 @@ |
| 1 | """ | |
| 2 | Feedback system for the ModularOllamaAgent. | |
| 3 | ||
| 4 | This module provides a feedback loop system that improves the agent's | |
| 5 | semantic understanding of user queries by learning from past interactions. | |
| 6 | It helps map natural language queries to appropriate functions and commands. | |
| 7 | """ | |
| 8 | ||
| 9 | import os | |
| 10 | import re | |
| 11 | import json | |
| 12 | import time | |
| 13 | import logging | |
| 14 | from typing import Dict, Any, List, Optional, Tuple, Set, Union | |
| 15 | ||
| 16 | # Set up logging | |
| 17 | logger = logging.getLogger(__name__) | |
| 18 | ||
| 19 | class FunctionSelectionFeedback: | |
| 20 | """Manages feedback on function selection for continuous improvement.""" | |
| 21 | ||
| 22 | def __init__(self, feedback_file_path=None): | |
| 23 | """ | |
| 24 | Initialize the feedback system. | |
| 25 | ||
| 26 | Args: | |
| 27 | feedback_file_path: Path to the feedback file (default: ~/.ollamatools/function_selection_feedback.jsonl) | |
| 28 | """ | |
| 29 | if feedback_file_path is None: | |
| 30 | self.feedback_file_path = os.path.join( | |
| 31 | os.path.expanduser('~'), | |
| 32 | '.ollamatools', | |
| 33 | 'function_selection_feedback.jsonl' | |
| 34 | ) | |
| 35 | else: | |
| 36 | self.feedback_file_path = feedback_file_path | |
| 37 | ||
| 38 | # Create directory if it doesn't exist | |
| 39 | os.makedirs(os.path.dirname(self.feedback_file_path), exist_ok=True) | |
| 40 | ||
| 41 | # Cache of recent feedback for quick lookup | |
| 42 | self.recent_feedback = [] | |
| 43 | self.max_cache_size = 1000 | |
| 44 | ||
| 45 | # Load existing feedback into cache | |
| 46 | self._load_recent_feedback() | |
| 47 | ||
| 48 | def _load_recent_feedback(self): | |
| 49 | """Load recent feedback entries into memory cache.""" | |
| 50 | try: | |
| 51 | if os.path.exists(self.feedback_file_path): | |
| 52 | with open(self.feedback_file_path, 'r') as f: | |
| 53 | # Load the most recent entries (up to max_cache_size) | |
| 54 | lines = f.readlines() | |
| 55 | for line in lines[-self.max_cache_size:]: | |
| 56 | try: | |
| 57 | entry = json.loads(line.strip()) | |
| 58 | self.recent_feedback.append(entry) | |
| 59 | except json.JSONDecodeError: | |
| 60 | continue | |
| 61 | logger.info(f"Loaded {len(self.recent_feedback)} feedback entries from {self.feedback_file_path}") | |
| 62 | else: | |
| 63 | logger.info(f"No feedback file found at {self.feedback_file_path}, starting with empty feedback") | |
| 64 | except Exception as e: | |
| 65 | logger.error(f"Error loading feedback data: {str(e)}") | |
| 66 | ||
| 67 | def record_explicit_feedback(self, query, intended_function, actual_function, user_correction=True): | |
| 68 | """ | |
| 69 | Record explicit feedback when a function selection was incorrect. | |
| 70 | ||
| 71 | Args: | |
| 72 | query: The original user query | |
| 73 | intended_function: The function that should have been used | |
| 74 | actual_function: The function that was actually used | |
| 75 | user_correction: Whether this feedback came directly from a user | |
| 76 | ||
| 77 | Returns: | |
| 78 | dict: The recorded feedback entry | |
| 79 | """ | |
| 80 | feedback_entry = { | |
| 81 | "query": query, | |
| 82 | "intended_function": intended_function, | |
| 83 | "actual_function": actual_function, | |
| 84 | "timestamp": time.time(), | |
| 85 | "source": "user_correction" if user_correction else "system_detection", | |
| 86 | "similarity_score": self._calculate_query_similarity(query) | |
| 87 | } | |
| 88 | ||
| 89 | # Add to cache | |
| 90 | self.recent_feedback.append(feedback_entry) | |
| 91 | if len(self.recent_feedback) > self.max_cache_size: | |
| 92 | self.recent_feedback.pop(0) | |
| 93 | ||
| 94 | # Write to file | |
| 95 | try: | |
| 96 | with open(self.feedback_file_path, 'a') as f: | |
| 97 | f.write(json.dumps(feedback_entry) + '\n') | |
| 98 | logger.info(f"Recorded explicit feedback for query: {query}") | |
| 99 | except Exception as e: | |
| 100 | logger.error(f"Error recording feedback: {str(e)}") | |
| 101 | ||
| 102 | return feedback_entry | |
| 103 | ||
| 104 | def record_implicit_feedback(self, query, function, confidence_score, execution_result): | |
| 105 | """ | |
| 106 | Record implicit feedback based on execution results. | |
| 107 | ||
| 108 | Args: | |
| 109 | query: The original user query | |
| 110 | function: The function that was used | |
| 111 | confidence_score: Confidence score for the function selection (0-1) | |
| 112 | execution_result: Result of executing the function | |
| 113 | ||
| 114 | Returns: | |
| 115 | dict: The recorded feedback entry | |
| 116 | """ | |
| 117 | # Determine if the execution was likely successful | |
| 118 | success_indicators = { | |
| 119 | "execute_command": lambda r: r.get("returncode", 1) == 0 and not r.get("stderr"), | |
| 120 | "read_file": lambda r: not (isinstance(r, str) and (r.startswith("Error") or r.startswith("Path validation error"))), | |
| 121 | "search_web": lambda r: len(r) > 0 if isinstance(r, list) else True | |
| 122 | } | |
| 123 | ||
| 124 | likely_success = False | |
| 125 | if function in success_indicators: | |
| 126 | likely_success = success_indicators[function](execution_result) | |
| 127 | ||
| 128 | feedback_entry = { | |
| 129 | "query": query, | |
| 130 | "function": function, | |
| 131 | "timestamp": time.time(), | |
| 132 | "source": "implicit_feedback", | |
| 133 | "confidence_score": confidence_score, | |
| 134 | "likely_success": likely_success, | |
| 135 | "similarity_score": self._calculate_query_similarity(query) | |
| 136 | } | |
| 137 | ||
| 138 | # Only record if confidence was low or execution likely failed | |
| 139 | if confidence_score < 0.7 or not likely_success: | |
| 140 | # Add to cache | |
| 141 | self.recent_feedback.append(feedback_entry) | |
| 142 | if len(self.recent_feedback) > self.max_cache_size: | |
| 143 | self.recent_feedback.pop(0) | |
| 144 | ||
| 145 | # Write to file | |
| 146 | try: | |
| 147 | with open(self.feedback_file_path, 'a') as f: | |
| 148 | f.write(json.dumps(feedback_entry) + '\n') | |
| 149 | logger.info(f"Recorded implicit feedback for query: {query}") | |
| 150 | except Exception as e: | |
| 151 | logger.error(f"Error recording feedback: {str(e)}") | |
| 152 | ||
| 153 | return feedback_entry | |
| 154 | ||
| 155 | def _calculate_query_similarity(self, query): | |
| 156 | """ | |
| 157 | Calculate similarity between this query and previous queries. | |
| 158 | ||
| 159 | Args: | |
| 160 | query: The query to compare | |
| 161 | ||
| 162 | Returns: | |
| 163 | list: List of similar queries with their similarity scores | |
| 164 | """ | |
| 165 | # Simple word-based similarity for now | |
| 166 | query_words = set(query.lower().split()) | |
| 167 | ||
| 168 | similarities = [] | |
| 169 | for entry in self.recent_feedback: | |
| 170 | past_query = entry.get("query", "") | |
| 171 | past_words = set(past_query.lower().split()) | |
| 172 | ||
| 173 | # Calculate Jaccard similarity | |
| 174 | if not query_words or not past_words: | |
| 175 | similarity = 0 | |
| 176 | else: | |
| 177 | intersection = len(query_words.intersection(past_words)) | |
| 178 | union = len(query_words.union(past_words)) | |
| 179 | similarity = intersection / union if union > 0 else 0 | |
| 180 | ||
| 181 | if similarity > 0.3: # Only include reasonably similar queries | |
| 182 | similarities.append({ | |
| 183 | "query": past_query, | |
| 184 | "similarity": similarity, | |
| 185 | "intended_function": entry.get("intended_function") or entry.get("function"), | |
| 186 | "timestamp": entry.get("timestamp") | |
| 187 | }) | |
| 188 | ||
| 189 | # Sort by similarity score | |
| 190 | similarities.sort(key=lambda x: x["similarity"], reverse=True) | |
| 191 | return similarities[:5] # Return top 5 similar queries | |
| 192 | ||
| 193 | def get_function_suggestion(self, query): | |
| 194 | """ | |
| 195 | Get function suggestion based on feedback history. | |
| 196 | ||
| 197 | Args: | |
| 198 | query: The user query | |
| 199 | ||
| 200 | Returns: | |
| 201 | tuple: (suggested_function, confidence_score, similar_queries) | |
| 202 | """ | |
| 203 | # Get similar queries | |
| 204 | similar_queries = self._calculate_query_similarity(query) | |
| 205 | ||
| 206 | if not similar_queries: | |
| 207 | return None, 0, [] | |
| 208 | ||
| 209 | # Count function occurrences weighted by similarity | |
| 210 | function_scores = {} | |
| 211 | for item in similar_queries: | |
| 212 | function = item["intended_function"] | |
| 213 | similarity = item["similarity"] | |
| 214 | ||
| 215 | if function not in function_scores: | |
| 216 | function_scores[function] = 0 | |
| 217 | ||
| 218 | function_scores[function] += similarity | |
| 219 | ||
| 220 | # Find the function with the highest score | |
| 221 | best_function = None | |
| 222 | highest_score = 0 | |
| 223 | ||
| 224 | for function, score in function_scores.items(): | |
| 225 | if score > highest_score: | |
| 226 | highest_score = score | |
| 227 | best_function = function | |
| 228 | ||
| 229 | # Calculate confidence based on the highest score and number of similar queries | |
| 230 | confidence = highest_score / len(similar_queries) if similar_queries else 0 | |
| 231 | ||
| 232 | return best_function, confidence, similar_queries | |
| 233 | ||
| 234 | def generate_improved_patterns(self): | |
| 235 | """ | |
| 236 | Generate improved regex patterns based on feedback data. | |
| 237 | ||
| 238 | Returns: | |
| 239 | dict: Dictionary mapping regex patterns to functions | |
| 240 | """ | |
| 241 | # Analyze the feedback data | |
| 242 | analysis = self.analyze_function_selection_patterns() | |
| 243 | ||
| 244 | if "error" in analysis: | |
| 245 | return {} | |
| 246 | ||
| 247 | improved_patterns = {} | |
| 248 | ||
| 249 | # Process mismatches to generate patterns | |
| 250 | for mismatch_info in analysis.get("top_mismatches", []): | |
| 251 | mismatch, data = mismatch_info | |
| 252 | actual, intended = mismatch.split(" → ") | |
| 253 | ||
| 254 | # Skip if we don't have enough examples | |
| 255 | if len(data["examples"]) < 2: | |
| 256 | continue | |
| 257 | ||
| 258 | # Generate a pattern from the examples | |
| 259 | pattern = self._generate_regex_from_examples(data["examples"], intended) | |
| 260 | if pattern: | |
| 261 | improved_patterns[pattern] = intended | |
| 262 | ||
| 263 | # Process query patterns | |
| 264 | for pattern_info in analysis.get("top_query_patterns", []): | |
| 265 | pattern_text, data = pattern_info | |
| 266 | functions = data["functions"] | |
| 267 | ||
| 268 | if not functions: | |
| 269 | continue | |
| 270 | ||
| 271 | # Find the most common function | |
| 272 | most_common = max(functions.items(), key=lambda x: x[1]) | |
| 273 | function_name, count = most_common | |
| 274 | ||
| 275 | # Only include if it's a strong pattern (used for the same function >70% of the time) | |
| 276 | confidence = count / data["count"] | |
| 277 | if confidence > 0.7: | |
| 278 | # Create a regex pattern from the text pattern | |
| 279 | regex_pattern = r"\b" + re.escape(pattern_text) + r".*" | |
| 280 | improved_patterns[regex_pattern] = function_name | |
| 281 | ||
| 282 | return improved_patterns | |
| 283 | ||
| 284 | def _generate_regex_from_examples(self, examples, function): | |
| 285 | """ | |
| 286 | Generate a regex pattern from example queries. | |
| 287 | ||
| 288 | Args: | |
| 289 | examples: List of example queries | |
| 290 | function: The target function | |
| 291 | ||
| 292 | Returns: | |
| 293 | str: Regex pattern | |
| 294 | """ | |
| 295 | if not examples: | |
| 296 | return None | |
| 297 | ||
| 298 | # Extract common words across examples | |
| 299 | common_words = set(examples[0].lower().split()) | |
| 300 | for example in examples[1:]: | |
| 301 | common_words &= set(example.lower().split()) | |
| 302 | ||
| 303 | # Remove very common words | |
| 304 | stop_words = {"what", "is", "the", "a", "an", "in", "on", "at", "to", "for", "of", "with", "by"} | |
| 305 | common_words -= stop_words | |
| 306 | ||
| 307 | if not common_words: | |
| 308 | # If no common words, try to find common starting words | |
| 309 | starts_with = [] | |
| 310 | for example in examples: | |
| 311 | words = example.lower().split() | |
| 312 | if words: | |
| 313 | starts_with.append(words[0]) | |
| 314 | ||
| 315 | # If we have consistent starting words | |
| 316 | if len(set(starts_with)) == 1 and starts_with[0] not in stop_words: | |
| 317 | return r"\b" + re.escape(starts_with[0]) + r".*" | |
| 318 | ||
| 319 | return None | |
| 320 | ||
| 321 | # Create a pattern with the common words | |
| 322 | pattern_parts = [] | |
| 323 | for word in common_words: | |
| 324 | pattern_parts.append(r"\b" + re.escape(word) + r"\b") | |
| 325 | ||
| 326 | # Combine with lookaheads for flexibility in word order | |
| 327 | pattern = "".join(f"(?=.*{part})" for part in pattern_parts) + ".*" | |
| 328 | return pattern | |
| 329 | ||
| 330 | def analyze_function_selection_patterns(self): | |
| 331 | """ | |
| 332 | Analyze patterns in function selection feedback. | |
| 333 | ||
| 334 | Returns: | |
| 335 | dict: Analysis results | |
| 336 | """ | |
| 337 | try: | |
| 338 | # Load all feedback data | |
| 339 | all_feedback = [] | |
| 340 | if os.path.exists(self.feedback_file_path): | |
| 341 | with open(self.feedback_file_path, 'r') as f: | |
| 342 | for line in f: | |
| 343 | try: | |
| 344 | entry = json.loads(line.strip()) | |
| 345 | all_feedback.append(entry) | |
| 346 | except json.JSONDecodeError: | |
| 347 | continue | |
| 348 | ||
| 349 | if not all_feedback: | |
| 350 | return {"error": "No feedback data available"} | |
| 351 | ||
| 352 | # Analyze common mismatches | |
| 353 | mismatches = {} | |
| 354 | for entry in all_feedback: | |
| 355 | if "intended_function" in entry and "actual_function" in entry: | |
| 356 | if entry["intended_function"] != entry["actual_function"]: | |
| 357 | key = f"{entry['actual_function']} → {entry['intended_function']}" | |
| 358 | if key not in mismatches: | |
| 359 | mismatches[key] = { | |
| 360 | "count": 0, | |
| 361 | "examples": [] | |
| 362 | } | |
| 363 | ||
| 364 | mismatches[key]["count"] += 1 | |
| 365 | if len(mismatches[key]["examples"]) < 5: # Limit examples | |
| 366 | mismatches[key]["examples"].append(entry["query"]) | |
| 367 | ||
| 368 | # Sort mismatches by frequency | |
| 369 | sorted_mismatches = sorted( | |
| 370 | [(k, v) for k, v in mismatches.items()], | |
| 371 | key=lambda x: x[1]["count"], | |
| 372 | reverse=True | |
| 373 | ) | |
| 374 | ||
| 375 | # Analyze query patterns | |
| 376 | query_patterns = {} | |
| 377 | for entry in all_feedback: | |
| 378 | query = entry.get("query", "") | |
| 379 | words = query.lower().split() | |
| 380 | ||
| 381 | # Extract potential patterns (first 2-3 words) | |
| 382 | if len(words) >= 2: | |
| 383 | pattern = " ".join(words[:2]) | |
| 384 | if pattern not in query_patterns: | |
| 385 | query_patterns[pattern] = { | |
| 386 | "count": 0, | |
| 387 | "functions": {} | |
| 388 | } | |
| 389 | ||
| 390 | query_patterns[pattern]["count"] += 1 | |
| 391 | ||
| 392 | function = entry.get("intended_function") or entry.get("function") | |
| 393 | if function: | |
| 394 | if function not in query_patterns[pattern]["functions"]: | |
| 395 | query_patterns[pattern]["functions"][function] = 0 | |
| 396 | query_patterns[pattern]["functions"][function] += 1 | |
| 397 | ||
| 398 | # Sort patterns by frequency | |
| 399 | sorted_patterns = sorted( | |
| 400 | [(k, v) for k, v in query_patterns.items()], | |
| 401 | key=lambda x: x[1]["count"], | |
| 402 | reverse=True | |
| 403 | ) | |
| 404 | ||
| 405 | return { | |
| 406 | "total_feedback_entries": len(all_feedback), | |
| 407 | "top_mismatches": sorted_mismatches[:10], | |
| 408 | "top_query_patterns": sorted_patterns[:10], | |
| 409 | "improvement_suggestions": self._generate_improvement_suggestions(sorted_mismatches, sorted_patterns) | |
| 410 | } | |
| 411 | except Exception as e: | |
| 412 | logger.error(f"Error analyzing feedback: {str(e)}") | |
| 413 | return {"error": f"Error analyzing feedback: {str(e)}"} | |
| 414 | ||
| 415 | def _generate_improvement_suggestions(self, mismatches, patterns): | |
| 416 | """ | |
| 417 | Generate suggestions for improving function selection. | |
| 418 | ||
| 419 | Args: | |
| 420 | mismatches: List of function mismatches | |
| 421 | patterns: List of query patterns | |
| 422 | ||
| 423 | Returns: | |
| 424 | list: Improvement suggestions | |
| 425 | """ | |
| 426 | suggestions = [] | |
| 427 | ||
| 428 | # Suggest improvements based on common mismatches | |
| 429 | if mismatches: | |
| 430 | for mismatch, data in mismatches[:5]: | |
| 431 | actual, intended = mismatch.split(" → ") | |
| 432 | examples = data["examples"] | |
| 433 | ||
| 434 | if examples: | |
| 435 | pattern_words = set() | |
| 436 | for example in examples: | |
| 437 | pattern_words.update(example.lower().split()) | |
| 438 | ||
| 439 | # Filter out common words | |
| 440 | common_words = {"what", "is", "the", "a", "an", "in", "on", "at", "to", "for", "of", "with", "by"} | |
| 441 | pattern_words = pattern_words - common_words | |
| 442 | ||
| 443 | if pattern_words: | |
| 444 | suggestion = { | |
| 445 | "type": "function_mapping", | |
| 446 | "description": f"Improve mapping from '{actual}' to '{intended}'", | |
| 447 | "examples": examples[:3], | |
| 448 | "suggested_keywords": list(pattern_words)[:5], | |
| 449 | "implementation_hint": f"Add pattern matching for these keywords to select '{intended}'" | |
| 450 | } | |
| 451 | suggestions.append(suggestion) | |
| 452 | ||
| 453 | # Suggest improvements based on query patterns | |
| 454 | if patterns: | |
| 455 | for pattern, data in patterns[:5]: | |
| 456 | functions = data["functions"] | |
| 457 | if functions: | |
| 458 | # Find the most common function for this pattern | |
| 459 | most_common = max(functions.items(), key=lambda x: x[1]) | |
| 460 | function_name, count = most_common | |
| 461 | ||
| 462 | suggestion = { | |
| 463 | "type": "query_pattern", | |
| 464 | "description": f"Add explicit handling for queries starting with '{pattern}'", | |
| 465 | "suggested_function": function_name, | |
| 466 | "confidence": count / data["count"], | |
| 467 | "implementation_hint": f"Add pattern matching for queries starting with '{pattern}' to select '{function_name}'" | |
| 468 | } | |
| 469 | suggestions.append(suggestion) | |
| 470 | ||
| 471 | return suggestions | |
| 472 | ||
| 473 | def detect_command_intent(message, feedback_system=None): | |
| 474 | """ | |
| 475 | Detect if a message is asking for system information that requires a command. | |
| 476 | ||
| 477 | Args: | |
| 478 | message: User message | |
| 479 | feedback_system: Optional FunctionSelectionFeedback instance | |
| 480 | ||
| 481 | Returns: | |
| 482 | Tuple[bool, Optional[str]]: (has_command_intent, suggested_command) | |
| 483 | """ | |
| 484 | # First check feedback-based patterns if available | |
| 485 | if feedback_system: | |
| 486 | suggested_function, confidence, similar_queries = feedback_system.get_function_suggestion(message) | |
| 487 | ||
| 488 | if suggested_function == "execute_command" and confidence > 0.7: | |
| 489 | # Extract command from similar queries | |
| 490 | command = extract_command_from_similar_queries(message, similar_queries) | |
| 491 | if command: | |
| 492 | return True, command | |
| 493 | ||
| 494 | # Map common questions to shell commands | |
| 495 | command_patterns = { | |
| 496 | r"(?:what|which)(?:\s+is)?(?:\s+the)?(?:\s+current)?\s+directory": "pwd", | |
| 497 | r"where\s+am\s+i": "pwd", | |
| 498 | r"list\s+(?:the\s+)?(?:files|contents)": "ls", | |
| 499 | r"show\s+(?:the\s+)?(?:files|contents)": "ls", | |
| 500 | r"(?:what|which)(?:\s+is)?(?:\s+the)?(?:\s+current)?\s+user": "whoami", | |
| 501 | r"who\s+am\s+i": "whoami", | |
| 502 | r"(?:what|which)(?:\s+is)?(?:\s+the)?(?:\s+current)?\s+date": "date", | |
| 503 | r"(?:what|which)(?:\s+is)?(?:\s+the)?(?:\s+current)?\s+time": "date", | |
| 504 | # Add more patterns as needed | |
| 505 | } | |
| 506 | ||
| 507 | # Check if message matches any pattern | |
| 508 | for pattern, command in command_patterns.items(): | |
| 509 | if re.search(pattern, message.lower()): | |
| 510 | return True, command | |
| 511 | ||
| 512 | return False, None | |
| 513 | ||
| 514 | def extract_command_from_similar_queries(message, similar_queries): | |
| 515 | """ | |
| 516 | Extract the most appropriate command from similar queries. | |
| 517 | ||
| 518 | Args: | |
| 519 | message: Current user message | |
| 520 | similar_queries: List of similar queries with their metadata | |
| 521 | ||
| 522 | Returns: | |
| 523 | str: Extracted command or None | |
| 524 | """ | |
| 525 | # Count command occurrences in similar queries | |
| 526 | command_counts = {} | |
| 527 | ||
| 528 | for query_info in similar_queries: | |
| 529 | # Skip if not execute_command | |
| 530 | if query_info.get("intended_function") != "execute_command": | |
| 531 | continue | |
| 532 | ||
| 533 | # Try to extract the command from the query | |
| 534 | query = query_info.get("query", "") | |
| 535 | ||
| 536 | # Check for common command patterns | |
| 537 | command_mapping = { | |
| 538 | r"directory|where\s+am\s+i": "pwd", | |
| 539 | r"list\s+files|show\s+files|contents": "ls", | |
| 540 | r"user|who\s+am\s+i": "whoami", | |
| 541 | r"date|time": "date" | |
| 542 | } | |
| 543 | ||
| 544 | for pattern, cmd in command_mapping.items(): | |
| 545 | if re.search(pattern, query.lower()): | |
| 546 | if cmd not in command_counts: | |
| 547 | command_counts[cmd] = 0 | |
| 548 | command_counts[cmd] += query_info.get("similarity", 0.5) | |
| 549 | ||
| 550 | # Return the most common command | |
| 551 | if command_counts: | |
| 552 | return max(command_counts.items(), key=lambda x: x[1])[0] | |
| 553 | ||
| 554 | return None | |
| 555 | ||
| 556 | def format_command_result(command, result): | |
| 557 | """ | |
| 558 | Format the result of a command execution in a user-friendly way. | |
| 559 | ||
| 560 | Args: | |
| 561 | command: The command that was executed | |
| 562 | result: The command execution result | |
| 563 | ||
| 564 | Returns: | |
| 565 | str: Formatted result | |
| 566 | """ | |
| 567 | if "error" in result: | |
| 568 | return f"Error executing command: {result['error']}" | |
| 569 | ||
| 570 | stdout = result.get("stdout", "").strip() | |
| 571 | stderr = result.get("stderr", "").strip() | |
| 572 | ||
| 573 | if command == "pwd": | |
| 574 | return f"You are currently in: {stdout}" | |
| 575 | elif command == "ls": | |
| 576 | return f"Files in the current directory:\n{stdout}" | |
| 577 | elif command == "whoami": | |
| 578 | return f"Current user: {stdout}" | |
| 579 | elif command == "date": | |
| 580 | return f"Current date and time: {stdout}" | |
| 581 | else: | |
| 582 | # Generic formatting | |
| 583 | response = f"Command result:\n{stdout}" | |
| 584 | if stderr: | |
| 585 | response += f"\nErrors/Warnings:\n{stderr}" | |
| 586 | return response |
| … | … | @@ -0,0 +1,189 @@ |
| 1 | """ | |
| 2 | Issue tracking functionality for the ModularOllamaAgent. | |
| 3 | ||
| 4 | This module provides functions for managing issues, including | |
| 5 | loading, saving, adding, listing, and updating issues. | |
| 6 | """ | |
| 7 | ||
| 8 | import os | |
| 9 | import json | |
| 10 | import time | |
| 11 | from typing import Dict, Any, List, Optional | |
| 12 | ||
| 13 | from .utils import print_info, print_error, print_success | |
| 14 | ||
| 15 | def load_issues(agent) -> None: | |
| 16 | """ | |
| 17 | Load saved issues from file. | |
| 18 | ||
| 19 | Args: | |
| 20 | agent: The agent instance | |
| 21 | """ | |
| 22 | if os.path.exists(agent.issues_file): | |
| 23 | try: | |
| 24 | with open(agent.issues_file, 'r') as f: | |
| 25 | data = json.load(f) | |
| 26 | agent.issues = data.get('issues', []) | |
| 27 | agent.issue_id_counter = data.get('next_id', 1) | |
| 28 | agent.print_info(f"Loaded {len(agent.issues)} issues from {agent.issues_file}") | |
| 29 | except Exception as e: | |
| 30 | agent.print_error(f"Failed to load issues: {str(e)}") | |
| 31 | agent.issues = [] | |
| 32 | agent.issue_id_counter = 1 | |
| 33 | ||
| 34 | def save_issues(agent) -> None: | |
| 35 | """ | |
| 36 | Save issues to file. | |
| 37 | ||
| 38 | Args: | |
| 39 | agent: The agent instance | |
| 40 | """ | |
| 41 | try: | |
| 42 | with open(agent.issues_file, 'w') as f: | |
| 43 | json.dump({ | |
| 44 | 'issues': agent.issues, | |
| 45 | 'next_id': agent.issue_id_counter | |
| 46 | }, f, indent=2) | |
| 47 | agent.print_info(f"Saved {len(agent.issues)} issues to {agent.issues_file}") | |
| 48 | except Exception as e: | |
| 49 | agent.print_error(f"Failed to save issues: {str(e)}") | |
| 50 | ||
| 51 | def add_issue(agent, title: str, description: str, severity: str = 'medium') -> int: | |
| 52 | """ | |
| 53 | Add a new issue to the tracker. | |
| 54 | ||
| 55 | Args: | |
| 56 | agent: The agent instance | |
| 57 | title: Issue title | |
| 58 | description: Issue description | |
| 59 | severity: Issue severity (low, medium, high) | |
| 60 | ||
| 61 | Returns: | |
| 62 | int: Issue ID | |
| 63 | """ | |
| 64 | # Validate severity | |
| 65 | if severity not in ['low', 'medium', 'high']: | |
| 66 | severity = 'medium' | |
| 67 | ||
| 68 | # Create issue | |
| 69 | issue = { | |
| 70 | 'id': agent.issue_id_counter, | |
| 71 | 'title': title, | |
| 72 | 'description': description, | |
| 73 | 'severity': severity, | |
| 74 | 'status': 'open', | |
| 75 | 'created_at': time.strftime('%Y-%m-%d %H:%M:%S'), | |
| 76 | 'updated_at': time.strftime('%Y-%m-%d %H:%M:%S') | |
| 77 | } | |
| 78 | ||
| 79 | # Add to list and increment counter | |
| 80 | agent.issues.append(issue) | |
| 81 | agent.issue_id_counter += 1 | |
| 82 | ||
| 83 | # Save issues | |
| 84 | save_issues(agent) | |
| 85 | ||
| 86 | agent.print_success(f"Issue #{issue['id']} added: {title}") | |
| 87 | return issue['id'] | |
| 88 | ||
| 89 | def list_issues(agent, status: Optional[str] = None) -> List[Dict[str, Any]]: | |
| 90 | """ | |
| 91 | List all issues, optionally filtered by status. | |
| 92 | ||
| 93 | Args: | |
| 94 | agent: The agent instance | |
| 95 | status: Optional status filter (open, resolved) | |
| 96 | ||
| 97 | Returns: | |
| 98 | List[Dict[str, Any]]: List of issues | |
| 99 | """ | |
| 100 | # Filter issues if status provided | |
| 101 | filtered_issues = agent.issues | |
| 102 | if status: | |
| 103 | filtered_issues = [i for i in agent.issues if i['status'] == status] | |
| 104 | ||
| 105 | # Print issues | |
| 106 | if not filtered_issues: | |
| 107 | agent.print_info("No issues found") | |
| 108 | return [] | |
| 109 | ||
| 110 | agent.print_info(f"Issues ({len(filtered_issues)}):") | |
| 111 | print("\nID | Severity | Status | Title") | |
| 112 | print("----|----------|----------|-------------------") | |
| 113 | ||
| 114 | for issue in filtered_issues: | |
| 115 | print(f"{issue['id']:<4}| {issue['severity']:<9}| {issue['status']:<9}| {issue['title']}") | |
| 116 | ||
| 117 | return filtered_issues | |
| 118 | ||
| 119 | def get_issue(agent, issue_id: int) -> Optional[Dict[str, Any]]: | |
| 120 | """ | |
| 121 | Get details of a specific issue. | |
| 122 | ||
| 123 | Args: | |
| 124 | agent: The agent instance | |
| 125 | issue_id: Issue ID | |
| 126 | ||
| 127 | Returns: | |
| 128 | Optional[Dict[str, Any]]: Issue details or None if not found | |
| 129 | """ | |
| 130 | # Find issue | |
| 131 | for issue in agent.issues: | |
| 132 | if issue['id'] == issue_id: | |
| 133 | # Print issue details | |
| 134 | agent.print_info(f"Issue #{issue['id']}: {issue['title']}") | |
| 135 | print(f"Status: {issue['status']}") | |
| 136 | print(f"Severity: {issue['severity']}") | |
| 137 | print(f"Created: {issue['created_at']}") | |
| 138 | print(f"Updated: {issue['updated_at']}") | |
| 139 | print("\nDescription:") | |
| 140 | print(issue['description']) | |
| 141 | return issue | |
| 142 | ||
| 143 | agent.print_error(f"Issue #{issue_id} not found") | |
| 144 | return None | |
| 145 | ||
| 146 | def update_issue( | |
| 147 | agent, | |
| 148 | issue_id: int, | |
| 149 | status: Optional[str] = None, | |
| 150 | severity: Optional[str] = None, | |
| 151 | description: Optional[str] = None | |
| 152 | ) -> bool: | |
| 153 | """ | |
| 154 | Update an existing issue. | |
| 155 | ||
| 156 | Args: | |
| 157 | agent: The agent instance | |
| 158 | issue_id: Issue ID | |
| 159 | status: New status (open, resolved) | |
| 160 | severity: New severity (low, medium, high) | |
| 161 | description: New description | |
| 162 | ||
| 163 | Returns: | |
| 164 | bool: True if updated successfully | |
| 165 | """ | |
| 166 | # Find issue | |
| 167 | for i, issue in enumerate(agent.issues): | |
| 168 | if issue['id'] == issue_id: | |
| 169 | # Update fields | |
| 170 | if status and status in ['open', 'resolved']: | |
| 171 | agent.issues[i]['status'] = status | |
| 172 | ||
| 173 | if severity and severity in ['low', 'medium', 'high']: | |
| 174 | agent.issues[i]['severity'] = severity | |
| 175 | ||
| 176 | if description: | |
| 177 | agent.issues[i]['description'] = description | |
| 178 | ||
| 179 | # Update timestamp | |
| 180 | agent.issues[i]['updated_at'] = time.strftime('%Y-%m-%d %H:%M:%S') | |
| 181 | ||
| 182 | # Save issues | |
| 183 | save_issues(agent) | |
| 184 | ||
| 185 | agent.print_success(f"Issue #{issue_id} updated") | |
| 186 | return True | |
| 187 | ||
| 188 | agent.print_error(f"Issue #{issue_id} not found") | |
| 189 | return False |
| … | … | @@ -0,0 +1,208 @@ |
| 1 | """ | |
| 2 | Search functionality for the ModularOllamaAgent. | |
| 3 | ||
| 4 | This module provides search-related functionality for the ModularOllamaAgent, | |
| 5 | including natural language detection of search intent, search execution, and | |
| 6 | search results processing. | |
| 7 | """ | |
| 8 | ||
| 9 | import time | |
| 10 | import logging | |
| 11 | from typing import Dict, Any, List, Optional, Set | |
| 12 | ||
| 13 | from ollamatools.tools.web_tools import search_web | |
| 14 | from ollamatools.errors import WebError | |
| 15 | from ollamatools.opensearch_web_tools import opensearch_query | |
| 16 | from .utils import print_status, print_success, print_error, print_warning, print_info | |
| 17 | ||
| 18 | # Set up logger | |
| 19 | logger = logging.getLogger(__name__) | |
| 20 | ||
| 21 | def detect_factual_intent(message: str) -> bool: | |
| 22 | """ | |
| 23 | Detect if a message is likely asking for factual information | |
| 24 | that would benefit from search. | |
| 25 | ||
| 26 | Args: | |
| 27 | message: User message | |
| 28 | ||
| 29 | Returns: | |
| 30 | bool: True if message likely needs external information | |
| 31 | """ | |
| 32 | # Basic topics that often require external information | |
| 33 | external_topics = [ | |
| 34 | "recipe", "weather", "news", "sports", "movie", "book", "author", | |
| 35 | "product", "review", "price", "stock", "company", "history", | |
| 36 | "person", "celebrity", "politician", "event", "scientific", | |
| 37 | "research", "study", "statistic", "data", "definition" | |
| 38 | ] | |
| 39 | ||
| 40 | # Check if message contains any of these topics | |
| 41 | contains_topic = any(topic in message.lower() for topic in external_topics) | |
| 42 | ||
| 43 | # Check if it's a complex question (longer questions tend to be more specific) | |
| 44 | is_complex = len(message.split()) > 6 | |
| 45 | ||
| 46 | # Simple heuristic: if it contains a topic of interest and is reasonably complex | |
| 47 | return contains_topic and is_complex | |
| 48 | ||
| 49 | def extract_search_context(search_results: Dict[str, Any]) -> str: | |
| 50 | """ | |
| 51 | Extract useful context from search results. | |
| 52 | ||
| 53 | Args: | |
| 54 | search_results: Results from opensearch_query | |
| 55 | ||
| 56 | Returns: | |
| 57 | str: Formatted context for the agent | |
| 58 | """ | |
| 59 | context_parts = [] | |
| 60 | ||
| 61 | # Add direct answer if available | |
| 62 | if search_results.get("answer"): | |
| 63 | try: | |
| 64 | # Handle both string and dict answers | |
| 65 | answer = search_results["answer"] | |
| 66 | if isinstance(answer, dict): | |
| 67 | answer_text = answer.get("content", str(answer)) | |
| 68 | else: | |
| 69 | answer_text = str(answer) | |
| 70 | context_parts.append(f"Direct answer: {answer_text}") | |
| 71 | except Exception as e: | |
| 72 | logger.warning(f"Error extracting answer for context: {str(e)}") | |
| 73 | ||
| 74 | # Add snippets from results | |
| 75 | if search_results.get("results"): | |
| 76 | context_parts.append("Information from search results:") | |
| 77 | for i, result in enumerate(search_results["results"][:3]): # Limit to 3 results | |
| 78 | title = result.get("title", "Untitled") | |
| 79 | content = result.get("content", "No content") | |
| 80 | url = result.get("url", "") | |
| 81 | context_parts.append(f"- {title}: {content} (Source: {url})") | |
| 82 | ||
| 83 | # Join everything with newlines | |
| 84 | return "\n".join(context_parts) | |
| 85 | ||
| 86 | def execute_search( | |
| 87 | query: str, | |
| 88 | opensearch_config: Dict[str, Any], | |
| 89 | permission_manager: Any, | |
| 90 | execution_history: List[Dict[str, Any]] | |
| 91 | ) -> Dict[str, Any]: | |
| 92 | """ | |
| 93 | Execute a search using OpenSearchNG or fall back to standard search. | |
| 94 | ||
| 95 | Args: | |
| 96 | query: Search query | |
| 97 | opensearch_config: OpenSearchNG configuration | |
| 98 | permission_manager: Permission manager instance | |
| 99 | execution_history: History of executed functions | |
| 100 | ||
| 101 | Returns: | |
| 102 | dict: Search results | |
| 103 | """ | |
| 104 | try: | |
| 105 | print_status(f"Searching for: {query}") | |
| 106 | ||
| 107 | # Try to use OpenSearchNG first | |
| 108 | try: | |
| 109 | # Execute the search with OpenSearchNG | |
| 110 | results = opensearch_query( | |
| 111 | query=query, | |
| 112 | config=opensearch_config, | |
| 113 | permission_manager=permission_manager | |
| 114 | ) | |
| 115 | ||
| 116 | # Show the number of results | |
| 117 | num_results = len(results.get("results", [])) | |
| 118 | print_success(f"Found {num_results} results using OpenSearchNG") | |
| 119 | ||
| 120 | # Show suggested answer if available | |
| 121 | if results.get("answer"): | |
| 122 | try: | |
| 123 | # Handle both string and dict answers | |
| 124 | answer = results["answer"] | |
| 125 | if isinstance(answer, dict): | |
| 126 | answer_text = answer.get("content", str(answer)) | |
| 127 | else: | |
| 128 | answer_text = str(answer) | |
| 129 | print_info(f"Direct answer: {answer_text}") | |
| 130 | except Exception as e: | |
| 131 | logger.warning(f"Error displaying answer: {str(e)}") | |
| 132 | ||
| 133 | # Register in execution history | |
| 134 | execution_history.append({ | |
| 135 | "function": "opensearch_query", | |
| 136 | "args": {"query": query}, | |
| 137 | "timestamp": time.time(), | |
| 138 | "success": True, | |
| 139 | "num_results": num_results | |
| 140 | }) | |
| 141 | ||
| 142 | return results | |
| 143 | ||
| 144 | except (WebError, ImportError, TypeError) as e: | |
| 145 | # If OpenSearchNG fails, fall back to standard web search | |
| 146 | if "Could not connect to OpenSearchNG server" in str(e) or isinstance(e, ImportError): | |
| 147 | print_warning("OpenSearchNG is not available. Using fallback search method.") | |
| 148 | ||
| 149 | # Use the fallback search method from web_tools | |
| 150 | search_results = search_web( | |
| 151 | query, | |
| 152 | num_results=5, | |
| 153 | permission_manager=permission_manager | |
| 154 | ) | |
| 155 | ||
| 156 | # Convert results to the expected format | |
| 157 | fallback_results = { | |
| 158 | "query": query, | |
| 159 | "results": [], | |
| 160 | "suggestion": None, | |
| 161 | "answer": None, | |
| 162 | "engines": ["fallback"], | |
| 163 | "timing": {"total": 0} | |
| 164 | } | |
| 165 | ||
| 166 | for result in search_results: | |
| 167 | fallback_results["results"].append({ | |
| 168 | "title": result.get("title", ""), | |
| 169 | "url": result.get("url", ""), | |
| 170 | "content": result.get("snippet", ""), | |
| 171 | "engine": "fallback" | |
| 172 | }) | |
| 173 | ||
| 174 | num_results = len(fallback_results["results"]) | |
| 175 | print_success(f"Found {num_results} results using fallback search") | |
| 176 | ||
| 177 | # Register in execution history | |
| 178 | execution_history.append({ | |
| 179 | "function": "search_web_fallback", | |
| 180 | "args": {"query": query}, | |
| 181 | "timestamp": time.time(), | |
| 182 | "success": True, | |
| 183 | "num_results": num_results | |
| 184 | }) | |
| 185 | ||
| 186 | return fallback_results | |
| 187 | else: | |
| 188 | # Re-raise other WebError exceptions | |
| 189 | raise | |
| 190 | ||
| 191 | except Exception as e: | |
| 192 | print_error(f"Search failed: {str(e)}") | |
| 193 | ||
| 194 | # Register in execution history | |
| 195 | execution_history.append({ | |
| 196 | "function": "execute_search", | |
| 197 | "args": {"query": query}, | |
| 198 | "timestamp": time.time(), | |
| 199 | "success": False, | |
| 200 | "error": str(e) | |
| 201 | }) | |
| 202 | ||
| 203 | # Return empty results | |
| 204 | return { | |
| 205 | "query": query, | |
| 206 | "results": [], | |
| 207 | "error": str(e) | |
| 208 | } |
| … | … | @@ -0,0 +1,136 @@ |
| 1 | """ | |
| 2 | Utility functions for the ModularOllamaAgent. | |
| 3 | ||
| 4 | This module provides utility functions for displaying status messages, | |
| 5 | formatting output, and suggesting fixes for errors. | |
| 6 | """ | |
| 7 | ||
| 8 | def print_status(message: str) -> None: | |
| 9 | """ | |
| 10 | Print a status message. | |
| 11 | ||
| 12 | Args: | |
| 13 | message: Status message to print | |
| 14 | """ | |
| 15 | print(f"\n🔄 {message}") | |
| 16 | ||
| 17 | def print_success(message: str) -> None: | |
| 18 | """ | |
| 19 | Print a success message. | |
| 20 | ||
| 21 | Args: | |
| 22 | message: Success message to print | |
| 23 | """ | |
| 24 | print(f"✅ {message}") | |
| 25 | ||
| 26 | def print_error(message: str) -> None: | |
| 27 | """ | |
| 28 | Print an error message. | |
| 29 | ||
| 30 | Args: | |
| 31 | message: Error message to print | |
| 32 | """ | |
| 33 | print(f"❌ {message}") | |
| 34 | ||
| 35 | def print_warning(message: str) -> None: | |
| 36 | """ | |
| 37 | Print a warning message. | |
| 38 | ||
| 39 | Args: | |
| 40 | message: Warning message to print | |
| 41 | """ | |
| 42 | print(f"⚠️ {message}") | |
| 43 | ||
| 44 | def print_info(message: str) -> None: | |
| 45 | """ | |
| 46 | Print an information message. | |
| 47 | ||
| 48 | Args: | |
| 49 | message: Information message to print | |
| 50 | """ | |
| 51 | print(f"ℹ️ {message}") | |
| 52 | ||
| 53 | def suggest_fix(suggestion: str) -> None: | |
| 54 | """ | |
| 55 | Print a fix suggestion. | |
| 56 | ||
| 57 | Args: | |
| 58 | suggestion: Fix suggestion to print | |
| 59 | """ | |
| 60 | print(f"💡 Suggested fix: {suggestion}") | |
| 61 | ||
| 62 | def format_code_block(code: str, language: str = "python") -> str: | |
| 63 | """ | |
| 64 | Format text as a code block. | |
| 65 | ||
| 66 | Args: | |
| 67 | code: Code to format | |
| 68 | language: Programming language for syntax highlighting | |
| 69 | ||
| 70 | Returns: | |
| 71 | str: Formatted code block | |
| 72 | """ | |
| 73 | return f"```{language}\n{code}\n```" | |
| 74 | ||
| 75 | def format_table(headers: list, rows: list) -> str: | |
| 76 | """ | |
| 77 | Format data as a table. | |
| 78 | ||
| 79 | Args: | |
| 80 | headers: Table headers | |
| 81 | rows: Table rows | |
| 82 | ||
| 83 | Returns: | |
| 84 | str: Formatted table | |
| 85 | """ | |
| 86 | # Determine column widths | |
| 87 | col_widths = [len(h) for h in headers] | |
| 88 | for row in rows: | |
| 89 | for i, cell in enumerate(row): | |
| 90 | col_widths[i] = max(col_widths[i], len(str(cell))) | |
| 91 | ||
| 92 | # Create the header | |
| 93 | header = "| " + " | ".join(h.ljust(w) for h, w in zip(headers, col_widths)) + " |" | |
| 94 | separator = "|-" + "-|-".join("-" * w for w in col_widths) + "-|" | |
| 95 | ||
| 96 | # Create the rows | |
| 97 | formatted_rows = [] | |
| 98 | for row in rows: | |
| 99 | formatted_row = "| " + " | ".join(str(cell).ljust(w) for cell, w in zip(row, col_widths)) + " |" | |
| 100 | formatted_rows.append(formatted_row) | |
| 101 | ||
| 102 | # Combine everything | |
| 103 | return "\n".join([header, separator] + formatted_rows) | |
| 104 | ||
| 105 | def truncate_text(text: str, max_length: int = 100, suffix: str = "...") -> str: | |
| 106 | """ | |
| 107 | Truncate text to a maximum length. | |
| 108 | ||
| 109 | Args: | |
| 110 | text: Text to truncate | |
| 111 | max_length: Maximum length | |
| 112 | suffix: Suffix to add to truncated text | |
| 113 | ||
| 114 | Returns: | |
| 115 | str: Truncated text | |
| 116 | """ | |
| 117 | if len(text) <= max_length: | |
| 118 | return text | |
| 119 | return text[:max_length - len(suffix)] + suffix | |
| 120 | ||
| 121 | def format_execution_time(seconds: float) -> str: | |
| 122 | """ | |
| 123 | Format execution time in a human-readable format. | |
| 124 | ||
| 125 | Args: | |
| 126 | seconds: Execution time in seconds | |
| 127 | ||
| 128 | Returns: | |
| 129 | str: Formatted execution time | |
| 130 | """ | |
| 131 | if seconds < 0.001: | |
| 132 | return f"{seconds * 1000000:.2f}μs" | |
| 133 | elif seconds < 1: | |
| 134 | return f"{seconds * 1000:.2f}ms" | |
| 135 | else: | |
| 136 | return f"{seconds:.2f}s" |
| … | … | @@ -4,20 +4,49 @@ OllamaTools Transparent Agent module initialization. |
| 4 | 4 | This package provides an enhanced OllamaAgent that offers transparent execution, |
| 5 | 5 | better feedback, and additional features like search integration, command |
| 6 | 6 | management, issue tracking, and semantic function selection feedback. |
| 7 | ||
| 8 | Note: This module is maintained for backward compatibility. New code should | |
| 9 | use the ModularOllamaAgent from ollamatools.agent.modular instead. | |
| 7 | 10 | """ |
| 8 | 11 | |
| 9 | from .agent import TransparentOllamaAgent | |
| 10 | from .search import detect_factual_intent, extract_search_context, execute_search | |
| 11 | from .commands import list_allowed_commands, add_allowed_command, remove_allowed_command | |
| 12 | from .issues import load_issues, save_issues, add_issue, list_issues, get_issue, update_issue | |
| 13 | from .utils import print_status, print_success, print_error, print_warning, print_info | |
| 14 | from .feedback import ( | |
| 15 | FunctionSelectionFeedback, | |
| 16 | detect_command_intent, | |
| 12 | import warnings | |
| 13 | import sys | |
| 14 | from ..modular import ( | |
| 15 | ModularOllamaAgent, | |
| 16 | print_status, print_success, print_error, print_warning, print_info, suggest_fix, | |
| 17 | FunctionSelectionFeedback, | |
| 18 | detect_factual_intent, extract_search_context, execute_search, | |
| 19 | list_allowed_commands, add_allowed_command, remove_allowed_command, | |
| 20 | load_issues, save_issues, add_issue, list_issues, get_issue, update_issue | |
| 21 | ) | |
| 22 | from ..modular.feedback import ( | |
| 23 | detect_command_intent, | |
| 17 | 24 | extract_command_from_similar_queries, |
| 18 | 25 | format_command_result |
| 19 | 26 | ) |
| 20 | 27 | |
| 28 | # For backward compatibility, alias ModularOllamaAgent as TransparentOllamaAgent | |
| 29 | TransparentOllamaAgent = ModularOllamaAgent | |
| 30 | ||
| 31 | # Show deprecation warning when TransparentOllamaAgent is imported | |
| 32 | warnings.warn( | |
| 33 | "TransparentOllamaAgent is deprecated and will be removed in a future version. " | |
| 34 | "Use ModularOllamaAgent from ollamatools.agent.modular instead.", | |
| 35 | DeprecationWarning, | |
| 36 | stacklevel=2 | |
| 37 | ) | |
| 38 | ||
| 39 | # For backward compatibility, also import from the original modules | |
| 40 | # This is needed for code that directly imports from these modules | |
| 41 | try: | |
| 42 | from .agent import TransparentOllamaAgent as _OriginalTransparentOllamaAgent | |
| 43 | from .search import detect_factual_intent as _original_detect_factual_intent | |
| 44 | from .commands import list_allowed_commands as _original_list_allowed_commands | |
| 45 | # ... and so on for other imports | |
| 46 | except ImportError: | |
| 47 | # If the original modules don't exist, that's fine | |
| 48 | pass | |
| 49 | ||
| 21 | 50 | __all__ = [ |
| 22 | 51 | 'TransparentOllamaAgent', |
| 23 | 52 | 'detect_factual_intent', |
| … | … | @@ -37,6 +66,7 @@ __all__ = [ |
| 37 | 66 | 'print_error', |
| 38 | 67 | 'print_warning', |
| 39 | 68 | 'print_info', |
| 69 | 'suggest_fix', | |
| 40 | 70 | 'FunctionSelectionFeedback', |
| 41 | 71 | 'detect_command_intent', |
| 42 | 72 | 'extract_command_from_similar_queries', |