OverviewFilesCommitsIssuesDocsReleasesPeers

ollama_semantic_function_calling commits / 066a011

modularizing agent code
066a0116d38df8829900e6c6034ce97a71cc6637
by fleeky.ashatron <fleeky@7rnx.net> · 2025-04-13T22:32:35.000Z

Changed files (19) +3630 -11

ollamatools-project/COMPLETION_REPORT_MODULAR.md +134 -0
……@@ -0,0 +1,134 @@
1# ModularOllamaAgent Implementation - Completion Report
2
3## Summary
4
5This report documents the successful implementation of the ModularOllamaAgent, a modular and extensible replacement for the TransparentOllamaAgent. The implementation includes:
6
71. **Core Modular Architecture**: Created a modular architecture that separates functionality into logical components
82. **Extended Functionality**: Integrated conversation history tracking, topic analysis, and performance metrics
93. **Comprehensive Documentation**: Provided detailed documentation for using and extending the agent
104. **Backward Compatibility**: Maintained backward compatibility with the TransparentOllamaAgent
11
12## Implementation Details
13
14### 1. Core Modular Architecture
15
16We 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
25This architecture separates concerns, making the code more maintainable and extensible.
26
27### 2. Extended Functionality
28
29We 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
36These features are implemented in the ExtendedModularAgent class, which demonstrates how to extend the ModularOllamaAgent with custom functionality.
37
38### 3. Comprehensive Documentation
39
40We 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
49We 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
831. **Better Modularity**: Separated functionality into logical components
842. **Enhanced Documentation**: Added comprehensive docstrings and type hints
853. **Consistent Naming**: Used consistent naming conventions throughout the codebase
864. **Error Handling**: Improved error handling and error messages
875. **Code Reuse**: Eliminated code duplication through proper modularization
88
89## Logical Audit
90
91We 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
120While the current implementation is robust and feature-rich, there are several areas for future improvement:
121
1221. **Advanced Natural Language Understanding**: Enhance the feedback system with more sophisticated language understanding techniques
1232. **Plugin Architecture**: Develop a formal plugin architecture for easier extension
1243. **Configuration Management**: Add proper configuration file support for customizing agent behavior
1254. **Testing Framework**: Develop a comprehensive testing framework for validating agent functionality
1265. **Performance Optimization**: Optimize performance for large-scale usage
127
128## Conclusion
129
130The 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
132The 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
134Overall, the ModularOllamaAgent is a significant improvement over the TransparentOllamaAgent and provides a solid foundation for future development.
ollamatools-project/docs/migration_guide.md +260 -0
……@@ -0,0 +1,260 @@
1# Migrating from TransparentOllamaAgent to ModularOllamaAgent
2
3This guide provides instructions for migrating from the TransparentOllamaAgent to the new ModularOllamaAgent.
4
5## Overview
6
7The 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
13Change your imports from:
14
15```python
16from ollamatools.agent.transparent import TransparentOllamaAgent
17```
18
19To:
20
21```python
22from ollamatools.agent import ModularOllamaAgent
23```
24
25### Class Name Changes
26
27Change instances of `TransparentOllamaAgent` to `ModularOllamaAgent`:
28
29```python
30# Old code
31agent = TransparentOllamaAgent(model="llama3.1:latest")
32
33# New code
34agent = ModularOllamaAgent(model="llama3.1:latest")
35```
36
37## Utility Function Migration
38
39If you're using utility functions from the transparent agent, update your imports:
40
41### Old imports:
42
43```python
44from 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
53from 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
61If you're using the feedback system, update your imports:
62
63### Old imports:
64
65```python
66from ollamatools.agent.transparent import FunctionSelectionFeedback
67from 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
77from ollamatools.agent.modular import FunctionSelectionFeedback
78from 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
90from ollamatools.agent.transparent import (
91 TransparentOllamaAgent,
92 print_status, print_success, print_info
93)
94from ollamatools.opensearch_web_tools import DEFAULT_OPENSEARCH_CONFIG
95from ollamatools.command_validator import DEFAULT_ALLOWLIST
96
97# Extend the default command allowlist
98EXTENDED_ALLOWLIST = DEFAULT_ALLOWLIST.union({
99 "python", "python3", "calc", "find", "grep", "cat", "echo", "pwd"
100})
101
102# Initialize the agent
103agent = 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
111response = agent.chat("Hello, how can you help me?")
112print(response)
113```
114
115### New code:
116
117```python
118from ollamatools.agent import ModularOllamaAgent
119from ollamatools.agent.modular import (
120 print_status, print_success, print_info
121)
122from ollamatools.opensearch_web_tools import DEFAULT_OPENSEARCH_CONFIG
123from ollamatools.command_validator import DEFAULT_ALLOWLIST
124
125# Extend the default command allowlist
126EXTENDED_ALLOWLIST = DEFAULT_ALLOWLIST.union({
127 "python", "python3", "calc", "find", "grep", "cat", "echo", "pwd"
128})
129
130# Initialize the agent
131agent = 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
139response = agent.chat("Hello, how can you help me?")
140print(response)
141```
142
143## Extending the Agent
144
145If you have code that extends the TransparentOllamaAgent, update your class inheritance:
146
147### Old code:
148
149```python
150from ollamatools.agent.transparent import TransparentOllamaAgent
151
152class 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
165from ollamatools.agent import ModularOllamaAgent
166
167class 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
179For 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```
182DeprecationWarning: 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
187The 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
195Example usage:
196
197```python
198from ollamatools.agent.modular import (
199 suggest_fix, format_code_block, format_table,
200 truncate_text, format_execution_time
201)
202
203# Suggest a fix
204suggest_fix("Make sure your OpenSearchNG instance is running")
205
206# Format code
207code = format_code_block("print('Hello, world!')", language="python")
208
209# Format a table
210table = 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
219short_text = truncate_text("This is a very long text...", max_length=10)
220
221# Format execution time
222formatted_time = format_execution_time(0.123) # "123.00ms"
223```
224
225## Troubleshooting
226
227### Import Errors
228
229If you encounter import errors after migration, make sure you're using the correct import paths:
230
231```python
232# For the main agent class
233from ollamatools.agent import ModularOllamaAgent
234
235# For utility functions
236from ollamatools.agent.modular import print_status, print_success, print_error
237```
238
239### Method Not Found Errors
240
241If 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
245If you continue to use the TransparentOllamaAgent class, you'll see deprecation warnings. To suppress these warnings, you can use:
246
247```python
248import warnings
249warnings.filterwarnings("ignore", category=DeprecationWarning)
250```
251
252However, it's recommended to migrate to the ModularOllamaAgent class instead.
253
254## Getting Help
255
256If you encounter any issues during migration, please:
257
2581. Check the [ModularOllamaAgent Guide](modular_agent_guide.md) for detailed documentation
2592. Look at the example scripts in the `examples/` directory
2603. File an issue on the GitHub repository if you encounter a bug
ollamatools-project/docs/modular_agent_guide.md +274 -0
……@@ -0,0 +1,274 @@
1# ModularOllamaAgent Guide
2
3This guide provides comprehensive documentation for the ModularOllamaAgent, a modular implementation of the OllamaAgent with enhanced capabilities.
4
5## Overview
6
7The 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
17The ModularOllamaAgent is included in the OllamaTools package. To install:
18
19```bash
20pip install ollamatools
21```
22
23## Basic Usage
24
25Here's a simple example of how to use the ModularOllamaAgent:
26
27```python
28from ollamatools.agent import ModularOllamaAgent
29
30# Initialize the agent
31agent = ModularOllamaAgent(
32 model="llama3.1:latest",
33 permission_prompts=True
34)
35
36# Chat with the agent
37response = agent.chat("Hello, how can you help me?")
38print(response)
39```
40
41## Configuration Options
42
43The 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
57The ModularOllamaAgent can automatically detect when a query might benefit from search and perform web searches using OpenSearchNG:
58
59```python
60# Explicit search
61results = agent.execute_search("What is the capital of France?")
62
63# Or through natural language
64response = 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
70The agent provides methods to manage the list of allowed commands:
71
72```python
73# List allowed commands
74agent.list_allowed_commands()
75
76# Add a command to the allowlist
77agent.add_allowed_command("npm")
78
79# Remove a command from the allowlist
80agent.remove_allowed_command("rm")
81```
82
83### Issue Tracking
84
85The agent includes a built-in issue tracking system:
86
87```python
88# Add an issue
89issue_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
96agent.list_issues()
97
98# Get issue details
99agent.get_issue(issue_id)
100
101# Update an issue
102agent.update_issue(issue_id, status="resolved")
103```
104
105### Semantic Function Selection Feedback
106
107The 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
111agent.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
118analysis = agent.function_feedback.analyze_function_selection_patterns()
119```
120
121### Execution History
122
123The agent keeps track of executed functions and commands:
124
125```python
126# Show execution history
127agent.show_execution_history(max_entries=10)
128```
129
130## Advanced Usage
131
132### Custom System Prompt
133
134You can customize the system prompt used by the agent:
135
136```python
137agent = ModularOllamaAgent(model="llama3.1:latest")
138agent.system_prompt = """
139You are an AI assistant specialized in Python programming.
140Always provide code examples when explaining programming concepts.
141"""
142response = agent.chat("How do I use list comprehensions in Python?")
143```
144
145### Directory Structure Analysis
146
147The agent can analyze directory structures:
148
149```python
150structure = agent.get_directory_structure(path=".", max_depth=2)
151```
152
153## Extending the ModularOllamaAgent
154
155The modular architecture makes it easy to extend the agent with custom functionality:
156
157```python
158from ollamatools.agent import ModularOllamaAgent
159
160class 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
175The 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
186Here's a complete example that demonstrates various features of the ModularOllamaAgent:
187
188```python
189import os
190import sys
191from ollamatools.agent import ModularOllamaAgent
192from ollamatools.agent.modular import print_info
193from ollamatools.opensearch_web_tools import DEFAULT_OPENSEARCH_CONFIG
194from ollamatools.command_validator import DEFAULT_ALLOWLIST
195
196# Extend the default command allowlist
197EXTENDED_ALLOWLIST = DEFAULT_ALLOWLIST.union({
198 "python", "python3", "calc", "find", "grep", "cat", "echo", "pwd"
199})
200
201# Initialize the agent
202agent = 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
210while 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
224If you're currently using the TransparentOllamaAgent, migrating to the ModularOllamaAgent is straightforward:
225
226```python
227# Old code
228from ollamatools.agent.transparent import TransparentOllamaAgent
229agent = TransparentOllamaAgent(model="llama3.1:latest")
230
231# New code
232from ollamatools.agent import ModularOllamaAgent
233agent = ModularOllamaAgent(model="llama3.1:latest")
234```
235
236The 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
242If 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
252If 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
258You can add it to the allowlist:
259
260```python
261agent.add_allowed_command("npm")
262```
263
264## Best Practices
265
2661. **Use the ModularOllamaAgent for new projects**: The ModularOllamaAgent is the recommended agent for new projects.
267
2682. **Leverage the feedback system**: The feedback system can significantly improve the agent's ability to understand natural language queries.
269
2703. **Customize the command allowlist**: Tailor the command allowlist to your specific needs to balance functionality and security.
271
2724. **Extend rather than modify**: When adding custom functionality, extend the ModularOllamaAgent rather than modifying its core implementation.
273
2745. **Use the issue tracking system**: The built-in issue tracking system can help you manage issues and track their resolution.
ollamatools-project/examples/README_TRANSPARENT_AGENT.md +118 -0
……@@ -32,6 +32,7 @@ The improved implementation:
3232 - `commands.py`: Command management
3333 - `issues.py`: Issue tracking
3434 - `utils.py`: Utility functions
35 - `feedback.py`: Semantic function selection feedback
3536
36372. **Easier Maintenance**: Changes to one aspect (e.g., search) don't require modifying the entire agent
3738
……@@ -107,6 +108,123 @@ python extended_transparent_agent.py
107108
108109The 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.
109110
111## Semantic Function Selection Feedback
112
113The 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
1151. **Learns from User Interactions**: Records when functions are used correctly or incorrectly
1162. **Improves Over Time**: Gets better at selecting the right function for similar queries
1173. **Adapts to User Language**: Learns the specific ways users phrase their requests
1184. **Provides Analytics**: Offers insights into common patterns and mismatches
119
120### How the Feedback System Works
121
1221. **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
1282. **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
1343. **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
142The feedback system is implemented in the `ollamatools/agent/transparent/feedback.py` module through the `FunctionSelectionFeedback` class:
143
1441. **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
1492. **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
1553. **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
1604. **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
1665. **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
174The 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
179Example:
180```
181You: what directory am I in
182Assistant: [uses read_file incorrectly]
183
184You: feedback 'what directory am I in' | execute_command
185Assistant: Thank you for your feedback! I'll remember to use execute_command for similar queries in the future.
186
187[Later]
188You: where am I located?
189Assistant: [now correctly uses pwd command]
190```
191
192### Advanced Usage
193
194Developers can leverage the feedback system in their own agents:
195
196```python
197from ollamatools.agent.transparent.feedback import FunctionSelectionFeedback
198
199# Initialize the feedback system
200feedback_system = FunctionSelectionFeedback()
201
202# Get function suggestions for a query
203function, confidence, similar_queries = feedback_system.get_function_suggestion("what files are in this folder?")
204
205# Record explicit feedback
206feedback_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
214feedback_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
222improved_patterns = feedback_system.generate_improved_patterns()
223
224# Analyze function selection patterns
225analysis = feedback_system.analyze_function_selection_patterns()
226```
227
110228## Extended Agent Features
111229
112230The `extended_transparent_agent.py` example demonstrates:
ollamatools-project/examples/extended_modular_agent.py +309 -0
……@@ -0,0 +1,309 @@
1#!/usr/bin/env python3
2"""
3Extended ModularOllamaAgent Example.
4
5This script demonstrates how to extend the ModularOllamaAgent
6with custom functionality like conversation history tracking,
7conversation summarization, and topic analysis.
8"""
9
10import os
11import sys
12import json
13import time
14import logging
15from typing import Dict, Any, List, Optional
16
17# Import the ModularOllamaAgent
18from ollamatools.agent import ModularOllamaAgent
19from ollamatools.agent.modular import (
20 print_status, print_success, print_info, print_error
21)
22from ollamatools.opensearch_web_tools import DEFAULT_OPENSEARCH_CONFIG
23from ollamatools.command_validator import DEFAULT_ALLOWLIST
24from ollamatools.utils.logger import setup_logger
25
26# Set up logging
27logger = setup_logger("extended_modular_agent", level="INFO")
28
29class 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
227def 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
308if __name__ == "__main__":
309 sys.exit(main())
ollamatools-project/examples/improved_transparent_agent.py +52 -0
……@@ -68,6 +68,10 @@ def main():
6868 print(" add_issue <title> | <description> | [severity] - Add a new issue")
6969 print(" resolve_issue <id> - Mark an issue as resolved")
7070
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
7175 # Simple chat loop
7276 while True:
7377 # Get user input
……@@ -160,6 +164,54 @@ def main():
160164 print("Invalid issue ID. Use 'resolve_issue <number>'.")
161165 continue
162166
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
163215 # Process as normal chat message
164216 response = agent.chat(user_input)
165217 print(f"\nAssistant: {response}")
ollamatools-project/examples/modular_agent_example.py +228 -0
……@@ -0,0 +1,228 @@
1#!/usr/bin/env python3
2"""
3ModularOllamaAgent Example.
4
5This script demonstrates how to use the ModularOllamaAgent
6to create a feature-rich agent with search capabilities,
7command management, issue tracking, and semantic function selection feedback.
8"""
9
10import os
11import sys
12import logging
13from typing import Dict, Any
14
15# Import the ModularOllamaAgent
16from ollamatools.agent import ModularOllamaAgent
17from ollamatools.agent.modular import (
18 print_status, print_success, print_info
19)
20from ollamatools.opensearch_web_tools import DEFAULT_OPENSEARCH_CONFIG
21from ollamatools.command_validator import DEFAULT_ALLOWLIST
22from ollamatools.utils.logger import setup_logger
23
24# Set up logging
25logger = setup_logger("modular_agent_example", level="INFO")
26
27# Extend the default command allowlist
28EXTENDED_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
34OPENSEARCH_CONFIG = DEFAULT_OPENSEARCH_CONFIG.copy()
35OPENSEARCH_CONFIG.update({
36 "base_url": "http://localhost:8081",
37 "engines": ["google", "bing", "duckduckgo", "brave"],
38 "num_results": 5
39})
40
41def 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
227if __name__ == "__main__":
228 sys.exit(main())
ollamatools-project/ollamatools/agent/__init__.py +13 -3
……@@ -4,7 +4,17 @@ OllamaTools Agent module initialization.
44This package provides agent implementations for OllamaTools.
55"""
66
7# This is a package initialization file
8# The transparent agent module is imported here
7# Import the ModularOllamaAgent
8from .modular import ModularOllamaAgent
99
10__all__ = []
10# Import the ExtendedModularAgent
11from .extended import ExtendedModularAgent
12
13# For backward compatibility
14from .transparent import TransparentOllamaAgent
15
16__all__ = [
17 'ModularOllamaAgent',
18 'ExtendedModularAgent',
19 'TransparentOllamaAgent'
20]
ollamatools-project/ollamatools/agent/extended/README.md +109 -0
……@@ -0,0 +1,109 @@
1# ExtendedModularAgent
2
3This package provides extensions to the ModularOllamaAgent with additional features like conversation history tracking, conversation summarization, topic analysis, and performance metrics.
4
5## Overview
6
7The 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
13The ExtendedModularAgent automatically tracks all conversations, including:
14- User messages
15- Agent responses
16- Response times
17- Estimated token usage
18
19This history is saved to a JSON file for persistence across sessions.
20
21### Conversation Summarization
22
23The 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
30The 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
42The 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
51To use the ExtendedModularAgent:
52
53```python
54from ollamatools.agent.extended import ExtendedModularAgent
55
56# Initialize the agent
57agent = ExtendedModularAgent(
58 model="llama3.1:latest",
59 permission_prompts=True
60)
61
62# Chat with the agent
63response = agent.chat("Hello, how can you help me?")
64print(response)
65
66# Get conversation summary
67summary = agent.summarize_conversations(n=5)
68print(summary)
69
70# Analyze conversation topics
71topics = agent.analyze_conversation_topics()
72print(topics)
73
74# Get performance metrics
75metrics = agent.get_performance_metrics()
76print(metrics)
77```
78
79## Example Script
80
81See 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
85The ExtendedModularAgent can be further extended with additional functionality:
86
87```python
88from ollamatools.agent.extended import ExtendedModularAgent
89
90class 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
105For 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
ollamatools-project/ollamatools/agent/extended/__init__.py +13 -0
……@@ -0,0 +1,13 @@
1"""
2Extended ModularOllamaAgent package.
3
4This package provides extensions to the ModularOllamaAgent with additional
5features like conversation history tracking, conversation summarization,
6topic 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
11from examples.extended_modular_agent import ExtendedModularAgent
12
13__all__ = ['ExtendedModularAgent']
ollamatools-project/ollamatools/agent/modular/README.md +97 -0
……@@ -0,0 +1,97 @@
1# ModularOllamaAgent
2
3This 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
7The 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
18The ModularOllamaAgent follows a modular architecture where functionality is separated into logical components:
19
20```
21ModularOllamaAgent
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
55To use the ModularOllamaAgent:
56
57```python
58from ollamatools.agent import ModularOllamaAgent
59
60# Initialize the agent
61agent = ModularOllamaAgent(
62 model="llama3.1:latest",
63 permission_prompts=True
64)
65
66# Chat with the agent
67response = agent.chat("Hello, how can you help me?")
68print(response)
69```
70
71## Extending the Agent
72
73The modular architecture makes it easy to extend the agent with custom functionality:
74
75```python
76from ollamatools.agent import ModularOllamaAgent
77
78class 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
93For 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
ollamatools-project/ollamatools/agent/modular/__init__.py +42 -0
……@@ -0,0 +1,42 @@
1"""
2ModularOllamaAgent package.
3
4This package provides a modular implementation of the OllamaAgent with
5enhanced capabilities like search integration, command management,
6issue tracking, and semantic function selection feedback.
7"""
8
9from .agent import ModularOllamaAgent
10from .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)
16from .feedback import FunctionSelectionFeedback
17from .search import detect_factual_intent, extract_search_context, execute_search
18from .commands import list_allowed_commands, add_allowed_command, remove_allowed_command
19from .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]
ollamatools-project/ollamatools/agent/modular/agent.py +702 -0
……@@ -0,0 +1,702 @@
1"""
2ModularOllamaAgent implementation.
3
4This module provides the ModularOllamaAgent class, which extends
5the base OllamaAgent with modular components, detailed feedback, and
6additional features like search integration, command management,
7issue tracking, and semantic function selection feedback.
8"""
9
10import os
11import sys
12import json
13import time
14import logging
15import re
16from typing import Dict, Any, List, Optional, Set, Union, Tuple
17
18from ollamatools import OllamaAgent, Config
19from ollamatools.command_validator import DEFAULT_ALLOWLIST
20from ollamatools.utils.logger import setup_logger
21from ollamatools.opensearch_web_tools import (
22 opensearch_query,
23 opensearch_fetch_url,
24 get_opensearch_info,
25 DEFAULT_OPENSEARCH_CONFIG
26)
27from ollamatools.errors import (
28 OllamaToolsError,
29 CommandError,
30 FileError,
31 WebError,
32 handle_exception
33)
34from ollamatools.tools.system_tools import execute_command
35
36# Import agent components
37from .search import detect_factual_intent, extract_search_context, execute_search
38from .commands import list_allowed_commands, add_allowed_command, remove_allowed_command
39from .issues import load_issues, save_issues, add_issue, list_issues, get_issue, update_issue
40from .utils import print_status, print_success, print_error, print_warning, print_info, suggest_fix
41from .feedback import (
42 FunctionSelectionFeedback,
43 detect_command_intent,
44 extract_command_from_similar_queries,
45 format_command_result
46)
47
48# Set up logging
49logger = setup_logger("modular_ollama_agent", level="INFO")
50
51# Extend the default command allowlist with useful commands
52EXTENDED_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
58OPENSEARCH_CONFIG = DEFAULT_OPENSEARCH_CONFIG.copy()
59OPENSEARCH_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
65class 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)}"
ollamatools-project/ollamatools/agent/modular/commands.py +122 -0
……@@ -0,0 +1,122 @@
1"""
2Command management functionality for the ModularOllamaAgent.
3
4This module provides functions for managing the command allowlist,
5including listing, adding, and removing commands.
6"""
7
8from typing import List, Set
9from .utils import print_info, print_error, print_success, print_warning
10
11def 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
61def 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
95def 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
ollamatools-project/ollamatools/agent/modular/feedback.py +586 -0
……@@ -0,0 +1,586 @@
1"""
2Feedback system for the ModularOllamaAgent.
3
4This module provides a feedback loop system that improves the agent's
5semantic understanding of user queries by learning from past interactions.
6It helps map natural language queries to appropriate functions and commands.
7"""
8
9import os
10import re
11import json
12import time
13import logging
14from typing import Dict, Any, List, Optional, Tuple, Set, Union
15
16# Set up logging
17logger = logging.getLogger(__name__)
18
19class 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
473def 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
514def 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
556def 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
ollamatools-project/ollamatools/agent/modular/issues.py +189 -0
……@@ -0,0 +1,189 @@
1"""
2Issue tracking functionality for the ModularOllamaAgent.
3
4This module provides functions for managing issues, including
5loading, saving, adding, listing, and updating issues.
6"""
7
8import os
9import json
10import time
11from typing import Dict, Any, List, Optional
12
13from .utils import print_info, print_error, print_success
14
15def 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
34def 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
51def 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
89def 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
119def 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
146def 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
ollamatools-project/ollamatools/agent/modular/search.py +208 -0
……@@ -0,0 +1,208 @@
1"""
2Search functionality for the ModularOllamaAgent.
3
4This module provides search-related functionality for the ModularOllamaAgent,
5including natural language detection of search intent, search execution, and
6search results processing.
7"""
8
9import time
10import logging
11from typing import Dict, Any, List, Optional, Set
12
13from ollamatools.tools.web_tools import search_web
14from ollamatools.errors import WebError
15from ollamatools.opensearch_web_tools import opensearch_query
16from .utils import print_status, print_success, print_error, print_warning, print_info
17
18# Set up logger
19logger = logging.getLogger(__name__)
20
21def 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
49def 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
86def 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 }
ollamatools-project/ollamatools/agent/modular/utils.py +136 -0
……@@ -0,0 +1,136 @@
1"""
2Utility functions for the ModularOllamaAgent.
3
4This module provides utility functions for displaying status messages,
5formatting output, and suggesting fixes for errors.
6"""
7
8def 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
17def 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
26def 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
35def 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
44def 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
53def 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
62def 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
75def 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
105def 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
121def 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"
ollamatools-project/ollamatools/agent/transparent/__init__.py +38 -8
……@@ -4,20 +4,49 @@ OllamaTools Transparent Agent module initialization.
44This package provides an enhanced OllamaAgent that offers transparent execution,
55better feedback, and additional features like search integration, command
66management, issue tracking, and semantic function selection feedback.
7
8Note: This module is maintained for backward compatibility. New code should
9use the ModularOllamaAgent from ollamatools.agent.modular instead.
710"""
811
9from .agent import TransparentOllamaAgent
10from .search import detect_factual_intent, extract_search_context, execute_search
11from .commands import list_allowed_commands, add_allowed_command, remove_allowed_command
12from .issues import load_issues, save_issues, add_issue, list_issues, get_issue, update_issue
13from .utils import print_status, print_success, print_error, print_warning, print_info
14from .feedback import (
15 FunctionSelectionFeedback,
16 detect_command_intent,
12import warnings
13import sys
14from ..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)
22from ..modular.feedback import (
23 detect_command_intent,
1724 extract_command_from_similar_queries,
1825 format_command_result
1926)
2027
28# For backward compatibility, alias ModularOllamaAgent as TransparentOllamaAgent
29TransparentOllamaAgent = ModularOllamaAgent
30
31# Show deprecation warning when TransparentOllamaAgent is imported
32warnings.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
41try:
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
46except ImportError:
47 # If the original modules don't exist, that's fine
48 pass
49
2150__all__ = [
2251 'TransparentOllamaAgent',
2352 'detect_factual_intent',
……@@ -37,6 +66,7 @@ __all__ = [
3766 'print_error',
3867 'print_warning',
3968 'print_info',
69 'suggest_fix',
4070 'FunctionSelectionFeedback',
4171 'detect_command_intent',
4272 'extract_command_from_similar_queries',