OverviewFilesCommitsIssuesDocsReleasesPeers

ollama_semantic_function_calling commits / dcc387c

getting better with natural prompting of shell commands and search , need to start to work on things in ISSUES.md
dcc387cdefa55078b26f087433ad0539205edcb6
by fleeky.ashatron <fleeky@7rnx.net> · 2025-04-13T13:21:24.000Z

Changed files (5) +1298 -60

ollamatools-project/ISSUES.md +114 -0
……@@ -0,0 +1,114 @@
1# Issues for the Transparent Ollama Agent
2
3## Original Issues Status
4
51. **Natural Language Understanding for Search**
6 - **Problem**: The agent requires explicit search commands rather than understanding natural language requests.
7 - **Description**: Currently, users must type "search [query]" instead of casual questions like "can you give me a recipe for chilli sauce?"
8 - **Status**: PARTIALLY RESOLVED ⚠️
9 - **Resolution**:
10 - Implemented search intent detection and automatic search trigger in the code
11 - Added the necessary logic to enhance natural language understanding
12 - **BUT**: Testing revealed that the agent is returning raw JSON function call data instead of executing it
13 - **Further Action Needed**:
14 - Fixed the implementation of the `chat` method to properly detect function calls in the LLM response
15 - Added JSON parsing and explicit function execution when a search function call is detected
16 - Need to test this fix to confirm it properly handles natural language search requests
17
182. **Session Permission Not Working as Expected**
19 - **Problem**: The "Allow for Session" option doesn't prevent further permission prompts.
20 - **Description**: When typing 'S' for session-level permissions, the user still gets prompted for similar operations.
21 - **Status**: RESOLVED ✅
22 - **Resolution**: Enhanced the permission system in `ollamatools/security.py` to use more flexible permission keys that recognize similar operations. Implemented command-based and directory-based permission patterns for better session handling.
23 - **Testing Needed**: Verify the improvements by testing with various similar commands and file operations
24
253. **Command List Management**
26 - **Problem**: No easy way to view, add or remove allowed commands.
27 - **Description**: Users need the ability to manage the list of allowed commands through the interface.
28 - **Status**: RESOLVED ✅
29 - **Resolution**: Added command management features to `examples/transparent_ollama_agent.py` including new commands: `commands` (to list all allowed commands), `add_command` (to add a command to the allowlist), and `remove_command` (to remove a command from the allowlist).
30
314. **Lack of Structured Issue Tracking**
32 - **Problem**: No built-in way to log and manage issues discovered during operation.
33 - **Description**: Currently, there's no mechanism to save and track issues discovered during operation.
34 - **Status**: RESOLVED ✅
35 - **Resolution**: Implemented a full issue tracking system in `examples/transparent_ollama_agent.py` with persistence to disk. Added commands for managing issues: `issues` (to list all issues), `issue <id>` (to show issue details), `add_issue` (to add a new issue), and `resolve_issue` (to mark an issue as resolved).
36
37## New Issues Identified
38
395. **Code Organization and Modularity**
40 - **Problem**: The transparent agent code is in a single large file with multiple responsibilities.
41 - **Description**: The `transparent_ollama_agent.py` has grown to include many features (search, commands, issues, etc.) making it difficult to maintain and extend.
42 - **Status**: IDENTIFIED 🆕
43 - **Proposed Solution**: Refactor the code into a modular structure:
44 ```
45 ollamatools/
46 ├── agent/
47 │ └── transparent/
48 │ ├── __init__.py # Package initialization
49 │ ├── agent.py # TransparentAgent class
50 │ ├── search.py # Search functionality
51 │ ├── commands.py # Command management
52 │ ├── issues.py # Issue tracking
53 │ └── utils.py # Utility functions
54 └── examples/
55 └── transparent_agent.py # Main CLI entry point
56 ```
57 - This structure aligns the file organization with the namespace, placing all transparent agent code in a dedicated directory under `ollamatools/agent/transparent/`.
58
596. **OpenSearchNG Integration Error Handling**
60 - **Problem**: The agent fails with a type error when OpenSearchNG is not available.
61 - **Description**: When OpenSearchNG is not running, the error "sequence item 0: expected str instance, dict found" occurs, indicating improper error handling.
62 - **Status**: IDENTIFIED 🆕
63 - **Proposed Solution**: Improve error handling in the OpenSearchNG integration code to gracefully handle connection issues and provide clearer error messages.
64
657. **Inconsistent Function Execution**
66 - **Problem**: The agent sometimes returns function calls as raw JSON instead of executing them.
67 - **Description**: When the LLM returns a function call in JSON format, the agent is not properly parsing and executing it.
68 - **Status**: PARTIALLY RESOLVED ⚠️
69 - **Resolution**: Added code to parse JSON function calls and execute them appropriately, but this needs testing.
70 - **Further Action Needed**: Comprehensive testing with different types of queries and function calls.
71
72## Implementation Notes
73
74### Natural Language Understanding for Search
75- Fixed the `chat` method in `TransparentAgent` to handle function calls properly:
76 - Added JSON parsing for responses that appear to be function calls
77 - Implemented direct execution of search functions when detected
78 - Enhanced result formatting to create user-friendly responses
79
80### Code Organization Plan
81- Break down the large `transparent_ollama_agent.py` file into several specialized modules:
82 - `agent.py`: Core agent class with initialization and chat handling
83 - `search.py`: Search intent detection and execution
84 - `commands.py`: Command management functionality
85 - `issues.py`: Issue tracking system
86 - `utils.py`: Display helpers and common utilities
87- Create a dedicated namespace under `ollamatools.agent.transparent`
88- Update imports and maintain backward compatibility
89- Create proper interfaces between components
90
91### OpenSearchNG Error Handling
92- Add better exception handling in the OpenSearchNG integration code
93- Implement a fallback mechanism for when OpenSearchNG is not available
94- Add clear error messages and suggestions for fixing connection issues
95
96## Future Considerations
97
981. **Advanced Search Intent Detection**: The current implementation uses simple heuristics for intent detection. Future versions could use more sophisticated language understanding techniques.
99
1002. **Fine-grained Permission System**: Enhance the permission system to support more granular permissions and remember user preferences across sessions.
101
1023. **Command Group Permissions**: Allow users to grant permissions to groups of related commands rather than individual commands.
103
1044. **Issue Categories and Labels**: Add support for categorizing issues and applying labels for better organization.
105
1065. **Export/Import Issues**: Add functionality to export issues to standard formats like CSV or JSON for integration with external issue tracking systems.
107
1086. **Advanced Code Analysis**: Implement code analysis features to help identify potential issues in code being developed with the agent.
109
1107. **Plugin System**: Create a plugin architecture to allow for easy extension of agent capabilities.
111
1128. **Configuration Management**: Add proper configuration file support for customizing agent behavior.
113
1149. **Testing Framework**: Develop a comprehensive testing framework for validating agent functionality.
ollamatools-project/examples/transparent_ollama_agent.py +433 -11
……@@ -93,6 +93,23 @@ class TransparentOllamaAgent:
9393 self.context = []
9494 self.execution_history = []
9595
96 # Initialize issue tracking
97 self.issues = []
98 self.issue_id_counter = 1
99
100 # Path for saving issues
101 self.issues_file = os.path.join(
102 os.path.expanduser('~'),
103 '.ollamatools',
104 'issues.json'
105 )
106
107 # Create directory if it doesn't exist
108 os.makedirs(os.path.dirname(self.issues_file), exist_ok=True)
109
110 # Load existing issues if any
111 self._load_issues()
112
96113 # Check if OpenSearchNG is available
97114 try:
98115 self._print_status("Checking OpenSearchNG availability...")
……@@ -144,9 +161,48 @@ class TransparentOllamaAgent:
144161
145162 Always check command permissions carefully and explain what each command does.
146163 For directory structure, use 'find .' or 'ls -la' commands.
164
165 When a user asks for information that might require searching the web or retrieving external data,
166 automatically determine if you should use the 'opensearch_query' function instead of requiring explicit search commands.
167
168 For example, if a user asks "can you give me a recipe for chilli sauce?", you should:
169 1. Recognize this as a request that requires external information
170 2. Use the opensearch_query function with an appropriate search query
171 3. Present the results in a helpful format
172
173 Only use tools when necessary - for factual questions you can answer directly, don't use search.
147174 """
148175
149176 try:
177 # Check if the message likely requires a search but doesn't explicitly ask for one
178 if not message.lower().startswith('search '):
179 search_indicators = [
180 "what is", "how to", "who is", "where can", "when did", "why does",
181 "recipe for", "information about", "tell me about", "find", "look up",
182 "latest", "news on", "current", "recent"
183 ]
184
185 # Check if the message contains search indicators
186 needs_search = any(indicator in message.lower() for indicator in search_indicators)
187
188 # Check for question structure
189 question_structure = message.lower().startswith(("what", "how", "who", "where", "when", "why", "can", "could"))
190
191 if needs_search or question_structure:
192 # Check if message is asking for external information we likely don't have
193 factual_intent = self._detect_factual_intent(message)
194 if factual_intent:
195 self._print_info("This appears to be a question that might benefit from search. Searching automatically...")
196 try:
197 results = self.execute_search(message)
198 # Process search results before continuing with normal chat flow
199 context = self._extract_search_context(results)
200 # Add context to the user message
201 message = f"{message}\n\nSearch context: {context}"
202 except Exception as e:
203 self._print_error(f"Automatic search failed: {str(e)}")
204 self._print_info("Continuing without search results")
205
150206 # Print what we're doing
151207 self._print_status(f"Processing message: {message}")
152208
……@@ -580,6 +636,330 @@ class TransparentOllamaAgent:
580636 """Print a fix suggestion."""
581637 print(f"💡 Suggested fix: {suggestion}")
582638
639 def _detect_factual_intent(self, message: str) -> bool:
640 """
641 Detect if a message is likely asking for factual information
642 that would benefit from search.
643
644 Args:
645 message: User message
646
647 Returns:
648 bool: True if message likely needs external information
649 """
650 # Basic topics that often require external information
651 external_topics = [
652 "recipe", "weather", "news", "sports", "movie", "book", "author",
653 "product", "review", "price", "stock", "company", "history",
654 "person", "celebrity", "politician", "event", "scientific",
655 "research", "study", "statistic", "data", "definition"
656 ]
657
658 # Check if message contains any of these topics
659 contains_topic = any(topic in message.lower() for topic in external_topics)
660
661 # Check if it's a complex question (longer questions tend to be more specific)
662 is_complex = len(message.split()) > 6
663
664 # Simple heuristic: if it contains a topic of interest and is reasonably complex
665 return contains_topic and is_complex
666
667 def _extract_search_context(self, search_results: Dict[str, Any]) -> str:
668 """
669 Extract useful context from search results.
670
671 Args:
672 search_results: Results from opensearch_query
673
674 Returns:
675 str: Formatted context for the agent
676 """
677 context_parts = []
678
679 # Add direct answer if available
680 if search_results.get("answer"):
681 context_parts.append(f"Direct answer: {search_results['answer']}")
682
683 # Add snippets from results
684 if search_results.get("results"):
685 context_parts.append("Information from search results:")
686 for i, result in enumerate(search_results["results"][:3]): # Limit to 3 results
687 title = result.get("title", "Untitled")
688 content = result.get("content", "No content")
689 url = result.get("url", "")
690 context_parts.append(f"- {title}: {content} (Source: {url})")
691
692 # Join everything with newlines
693 return "\n".join(context_parts)
694
695 def list_allowed_commands(self) -> List[str]:
696 """
697 List all currently allowed commands.
698
699 Returns:
700 List[str]: List of allowed commands
701 """
702 # Get the current allowlist from command_validator
703 from ollamatools.command_validator import DEFAULT_ALLOWLIST
704
705 # Combine with any custom entries
706 all_commands = sorted(list(self.command_allowlist))
707
708 # Print the commands
709 self._print_info(f"Currently allowed commands ({len(all_commands)}):")
710
711 # Format into categories for better readability
712 categories = {
713 "File Operations": ["ls", "cat", "find", "grep", "head", "tail"],
714 "Directory Operations": ["pwd", "cd", "mkdir", "rmdir"],
715 "System Information": ["uname", "hostname", "whoami", "ps", "top", "df", "du", "free"],
716 "Network Utilities": ["ping", "curl", "wget", "netstat", "dig", "nslookup"],
717 "Text Processing": ["echo", "printf", "sed", "awk", "cut", "sort", "uniq", "wc"],
718 "File Manipulation": ["cp", "mv", "rm", "touch", "chmod", "chown", "chgrp", "ln"],
719 "Other": []
720 }
721
722 # Categorize commands
723 for cmd in all_commands:
724 categorized = False
725 for category, commands in categories.items():
726 if cmd in commands:
727 categorized = True
728 break
729 if not categorized:
730 categories["Other"].append(cmd)
731
732 # Print by category
733 for category, commands in categories.items():
734 if commands:
735 print(f"\n{category}:")
736 for i, cmd in enumerate(sorted(commands)):
737 print(f" {cmd}", end="\t")
738 # New line every 5 commands for readability
739 if (i + 1) % 5 == 0:
740 print()
741 print() # Ensure we end with a newline
742
743 return all_commands
744
745 def add_allowed_command(self, command: str) -> bool:
746 """
747 Add a command to the allowed list.
748
749 Args:
750 command: Command to add
751
752 Returns:
753 bool: True if added successfully
754 """
755 if not command or not isinstance(command, str):
756 self._print_error("Invalid command name")
757 return False
758
759 # Clean up the command name
760 command = command.strip().lower()
761
762 # Check if already in allowlist
763 if command in self.command_allowlist:
764 self._print_info(f"Command '{command}' is already allowed")
765 return True
766
767 # Add to custom allowlist
768 self.command_allowlist.add(command)
769 self._print_success(f"Command '{command}' added to allowed list")
770
771 # Show warning for potentially dangerous commands
772 dangerous_commands = {"rm", "mv", "chmod", "chown", "dd", "mkfs", "iptables"}
773 if command in dangerous_commands:
774 self._print_warning(f"'{command}' is a potentially dangerous command. Use with caution!")
775
776 return True
777
778 def remove_allowed_command(self, command: str) -> bool:
779 """
780 Remove a command from the allowed list.
781
782 Args:
783 command: Command to remove
784
785 Returns:
786 bool: True if removed successfully
787 """
788 if not command or not isinstance(command, str):
789 self._print_error("Invalid command name")
790 return False
791
792 # Clean up the command name
793 command = command.strip().lower()
794
795 # Check if in allowlist
796 if command not in self.command_allowlist:
797 self._print_error(f"Command '{command}' is not in the allowed list")
798 return False
799
800 # Remove from custom allowlist
801 self.command_allowlist.remove(command)
802 self._print_success(f"Command '{command}' removed from allowed list")
803
804 return True
805
806 def _load_issues(self) -> None:
807 """Load saved issues from file."""
808 if os.path.exists(self.issues_file):
809 try:
810 with open(self.issues_file, 'r') as f:
811 data = json.load(f)
812 self.issues = data.get('issues', [])
813 self.issue_id_counter = data.get('next_id', 1)
814 logger.info(f"Loaded {len(self.issues)} issues from {self.issues_file}")
815 except Exception as e:
816 logger.error(f"Failed to load issues: {str(e)}")
817 self.issues = []
818 self.issue_id_counter = 1
819
820 def _save_issues(self) -> None:
821 """Save issues to file."""
822 try:
823 with open(self.issues_file, 'w') as f:
824 json.dump({
825 'issues': self.issues,
826 'next_id': self.issue_id_counter
827 }, f, indent=2)
828 logger.info(f"Saved {len(self.issues)} issues to {self.issues_file}")
829 except Exception as e:
830 logger.error(f"Failed to save issues: {str(e)}")
831
832 def add_issue(self, title: str, description: str, severity: str = 'medium') -> int:
833 """
834 Add a new issue to the tracker.
835
836 Args:
837 title: Issue title
838 description: Issue description
839 severity: Issue severity (low, medium, high)
840
841 Returns:
842 int: Issue ID
843 """
844 # Validate severity
845 if severity not in ['low', 'medium', 'high']:
846 severity = 'medium'
847
848 # Create issue
849 issue = {
850 'id': self.issue_id_counter,
851 'title': title,
852 'description': description,
853 'severity': severity,
854 'status': 'open',
855 'created_at': time.strftime('%Y-%m-%d %H:%M:%S'),
856 'updated_at': time.strftime('%Y-%m-%d %H:%M:%S')
857 }
858
859 # Add to list and increment counter
860 self.issues.append(issue)
861 self.issue_id_counter += 1
862
863 # Save issues
864 self._save_issues()
865
866 self._print_success(f"Issue #{issue['id']} added: {title}")
867 return issue['id']
868
869 def list_issues(self, status: Optional[str] = None) -> List[Dict[str, Any]]:
870 """
871 List all issues, optionally filtered by status.
872
873 Args:
874 status: Optional status filter (open, resolved)
875
876 Returns:
877 List[Dict[str, Any]]: List of issues
878 """
879 # Filter issues if status provided
880 filtered_issues = self.issues
881 if status:
882 filtered_issues = [i for i in self.issues if i['status'] == status]
883
884 # Print issues
885 if not filtered_issues:
886 self._print_info("No issues found")
887 return []
888
889 self._print_info(f"Issues ({len(filtered_issues)}):")
890 print("\nID | Severity | Status | Title")
891 print("----|----------|----------|-------------------")
892
893 for issue in filtered_issues:
894 print(f"{issue['id']:<4}| {issue['severity']:<9}| {issue['status']:<9}| {issue['title']}")
895
896 return filtered_issues
897
898 def get_issue(self, issue_id: int) -> Optional[Dict[str, Any]]:
899 """
900 Get details of a specific issue.
901
902 Args:
903 issue_id: Issue ID
904
905 Returns:
906 Optional[Dict[str, Any]]: Issue details or None if not found
907 """
908 # Find issue
909 for issue in self.issues:
910 if issue['id'] == issue_id:
911 # Print issue details
912 self._print_info(f"Issue #{issue['id']}: {issue['title']}")
913 print(f"Status: {issue['status']}")
914 print(f"Severity: {issue['severity']}")
915 print(f"Created: {issue['created_at']}")
916 print(f"Updated: {issue['updated_at']}")
917 print("\nDescription:")
918 print(issue['description'])
919 return issue
920
921 self._print_error(f"Issue #{issue_id} not found")
922 return None
923
924 def update_issue(self, issue_id: int, status: Optional[str] = None,
925 severity: Optional[str] = None, description: Optional[str] = None) -> bool:
926 """
927 Update an existing issue.
928
929 Args:
930 issue_id: Issue ID
931 status: New status (open, resolved)
932 severity: New severity (low, medium, high)
933 description: New description
934
935 Returns:
936 bool: True if updated successfully
937 """
938 # Find issue
939 for i, issue in enumerate(self.issues):
940 if issue['id'] == issue_id:
941 # Update fields
942 if status and status in ['open', 'resolved']:
943 self.issues[i]['status'] = status
944
945 if severity and severity in ['low', 'medium', 'high']:
946 self.issues[i]['severity'] = severity
947
948 if description:
949 self.issues[i]['description'] = description
950
951 # Update timestamp
952 self.issues[i]['updated_at'] = time.strftime('%Y-%m-%d %H:%M:%S')
953
954 # Save issues
955 self._save_issues()
956
957 self._print_success(f"Issue #{issue_id} updated")
958 return True
959
960 self._print_error(f"Issue #{issue_id} not found")
961 return False
962
583963
584964def main():
585965 """Run the transparent Ollama agent."""
……@@ -596,10 +976,19 @@ def main():
596976 print("========================")
597977 print("Type 'exit' to quit")
598978 print("Type 'commands' to see available commands")
979 print("Type 'add_command <name>' to add a command to the allowed list")
980 print("Type 'remove_command <name>' to remove a command from the allowed list")
599981 print("Type 'search <query>' to search directly")
600982 print("Type 'run <command>' to execute a command directly")
601983 print("Type 'dir' to show current directory structure")
602984 print("Type 'history' to see execution history")
985
986 print("\nIssue Tracking:")
987 print(" issues - List all issues")
988 print(" issue <id> - Show issue details")
989 print(" add_issue <title> | <description> | [severity] - Add a new issue")
990 print(" resolve_issue <id> - Mark an issue as resolved")
991
603992 print("\nOtherwise, just chat naturally with the agent!")
604993
605994 # Simple chat loop
……@@ -614,17 +1003,17 @@ def main():
6141003
6151004 # Check for special commands
6161005 if user_input.lower() == 'commands':
617 print("\nAvailable Commands:")
618 print("=" * 60)
619 print("General Commands:")
620 print(" exit, quit, bye - Exit the program")
621 print(" commands - Show this help")
622 print(" history - Show execution history")
623 print("\nDirect Operations:")
624 print(" search <query> - Search directly using OpenSearchNG")
625 print(" run <command> - Execute a command directly")
626 print(" dir [path] [depth] - Show directory structure")
627 print("=" * 60)
1006 agent.list_allowed_commands()
1007 continue
1008
1009 if user_input.lower().startswith('add_command '):
1010 command = user_input[len('add_command '):].strip()
1011 agent.add_allowed_command(command)
1012 continue
1013
1014 if user_input.lower().startswith('remove_command '):
1015 command = user_input[len('remove_command '):].strip()
1016 agent.remove_allowed_command(command)
6281017 continue
6291018
6301019 if user_input.lower() == 'history':
……@@ -680,6 +1069,39 @@ def main():
6801069 print(f"Failed to get directory structure: {str(e)}")
6811070 continue
6821071
1072 # Issue tracking commands
1073 if user_input.lower() == 'issues':
1074 agent.list_issues()
1075 continue
1076
1077 if user_input.lower().startswith('issue '):
1078 try:
1079 issue_id = int(user_input[len('issue '):].strip())
1080 agent.get_issue(issue_id)
1081 except ValueError:
1082 print("Invalid issue ID. Use 'issue <number>'.")
1083 continue
1084
1085 if user_input.lower().startswith('add_issue '):
1086 # Parse command: add_issue Title | Description | [severity]
1087 parts = user_input[len('add_issue '):].split('|')
1088 if len(parts) >= 2:
1089 title = parts[0].strip()
1090 description = parts[1].strip()
1091 severity = parts[2].strip().lower() if len(parts) > 2 else 'medium'
1092 agent.add_issue(title, description, severity)
1093 else:
1094 print("Usage: add_issue Title | Description | [severity]")
1095 continue
1096
1097 if user_input.lower().startswith('resolve_issue '):
1098 try:
1099 issue_id = int(user_input[len('resolve_issue '):].strip())
1100 agent.update_issue(issue_id, status='resolved')
1101 except ValueError:
1102 print("Invalid issue ID. Use 'resolve_issue <number>'.")
1103 continue
1104
6831105 # Process as normal chat message
6841106 response = agent.chat(user_input)
6851107 print(f"\nAssistant: {response}")
ollamatools-project/ollamatools/security.py +100 -6
……@@ -4,6 +4,7 @@ Permission management system for OllamaTools.
44This module provides security controls for executing functions,
55with a focus on a prompt-based permission system.
66"""
7import os
78from typing import Dict, Any, Optional, Tuple, Set
89import logging
910import re
……@@ -43,10 +44,44 @@ class PermissionManager:
4344 self.prompt_enabled = prompt_enabled
4445 self.sudo_always_prompt = sudo_always_prompt
4546 self.session_permissions = set() # Store allowed operations for the session
47 self.session_permission_patterns = {
48 "read_file": {}, # Directory-based permissions
49 "write_file": {}, # Directory-based permissions
50 "execute_command": {} # Command-based permissions
51 }
4652
4753 logger.debug("PermissionManager initialized with prompt_enabled=%s, sudo_always_prompt=%s",
4854 prompt_enabled, sudo_always_prompt)
4955
56 def _generate_permission_key(self, function_name: str, args: Dict[str, Any]) -> str:
57 """
58 Generate a more flexible permission key that recognizes similar operations.
59
60 Args:
61 function_name: Name of the function
62 args: Function arguments
63
64 Returns:
65 str: Permission key
66 """
67 # For commands, focus on the command name rather than all arguments
68 if function_name == "execute_command" and "command" in args:
69 command = args["command"]
70 # Just use the base command without all arguments for matching
71 base_command = command.split()[0] if isinstance(command, str) and " " in command else command
72 return f"{function_name}:command={base_command}"
73
74 # For file operations, focus on the operation type and parent directory
75 if function_name in ("read_file", "write_file") and "path" in args:
76 path = args["path"]
77 # Use parent directory for path-based operations to match similar files
78 parent_dir = os.path.dirname(os.path.abspath(path))
79 return f"{function_name}:dir={parent_dir}"
80
81 # For other operations, use the full args as before
82 args_str = ', '.join(f"{k}={v!r}" for k, v in args.items())
83 return f"{function_name}:{args_str}"
84
5085 def request_permission(
5186 self,
5287 function_name: str,
……@@ -74,7 +109,7 @@ class PermissionManager:
74109 args_str = ', '.join(f"{k}={v!r}" for k, v in args.items())
75110
76111 # Generate a permission key for the operation
77 permission_key = f"{function_name}:{args_str}"
112 permission_key = self._generate_permission_key(function_name, args)
78113
79114 # Check if we need to prompt for permission
80115 should_prompt = self.prompt_enabled
……@@ -89,6 +124,21 @@ class PermissionManager:
89124 logger.debug("Permission already granted for session: %s", permission_key)
90125 return True
91126
127 # Check specific session permission patterns
128 if function_name == "execute_command" and "command" in args:
129 command = args["command"]
130 base_command = command.split()[0] if isinstance(command, str) and " " in command else command
131 if base_command in self.session_permission_patterns["execute_command"]:
132 logger.debug("Permission already granted for command type: %s", base_command)
133 return True
134
135 elif function_name in ("read_file", "write_file") and "path" in args:
136 path = args["path"]
137 parent_dir = os.path.dirname(os.path.abspath(path))
138 if parent_dir in self.session_permission_patterns[function_name]:
139 logger.debug("Permission already granted for directory: %s", parent_dir)
140 return True
141
92142 # If prompting is disabled and not overridden by sudo, grant permission
93143 if not should_prompt:
94144 logger.debug("Permission prompts disabled, automatically granting permission for: %s", permission_key)
……@@ -121,6 +171,20 @@ class PermissionManager:
121171
122172 elif response == 'S': # Allow for session
123173 self.session_permissions.add(permission_key)
174
175 # Store more specific pattern matches for better session handling
176 if function_name == "execute_command" and "command" in args:
177 command = args["command"]
178 base_command = command.split()[0] if isinstance(command, str) and " " in command else command
179 self.session_permission_patterns["execute_command"][base_command] = True
180 logger.info(f"Command '{base_command}' allowed for session")
181
182 elif function_name in ("read_file", "write_file") and "path" in args:
183 path = args["path"]
184 parent_dir = os.path.dirname(os.path.abspath(path))
185 self.session_permission_patterns[function_name][parent_dir] = True
186 logger.info(f"{function_name} allowed for directory: {parent_dir}")
187
124188 logger.info("Permission granted for session: %s", permission_key)
125189 return True
126190
……@@ -294,6 +358,9 @@ class PermissionManager:
294358 """
295359 num_permissions = len(self.session_permissions)
296360 self.session_permissions.clear()
361 # Clear pattern-based permissions too
362 for key in self.session_permission_patterns:
363 self.session_permission_patterns[key] = {}
297364 logger.info("Cleared %d session permissions", num_permissions)
298365
299366 def add_session_permission(self, function_name: str, args: Dict[str, Any]):
……@@ -304,9 +371,20 @@ class PermissionManager:
304371 function_name: Name of the function
305372 args: Function arguments
306373 """
307 args_str = ', '.join(f"{k}={v!r}" for k, v in args.items())
308 permission_key = f"{function_name}:{args_str}"
374 permission_key = self._generate_permission_key(function_name, args)
309375 self.session_permissions.add(permission_key)
376
377 # Add to pattern-based permissions
378 if function_name == "execute_command" and "command" in args:
379 command = args["command"]
380 base_command = command.split()[0] if isinstance(command, str) and " " in command else command
381 self.session_permission_patterns["execute_command"][base_command] = True
382
383 elif function_name in ("read_file", "write_file") and "path" in args:
384 path = args["path"]
385 parent_dir = os.path.dirname(os.path.abspath(path))
386 self.session_permission_patterns[function_name][parent_dir] = True
387
310388 logger.info("Added session permission: %s", permission_key)
311389
312390 def has_session_permission(self, function_name: str, args: Dict[str, Any]) -> bool:
……@@ -320,6 +398,22 @@ class PermissionManager:
320398 Returns:
321399 bool: True if the operation has session permission
322400 """
323 args_str = ', '.join(f"{k}={v!r}" for k, v in args.items())
324 permission_key = f"{function_name}:{args_str}"
325 return permission_key in self.session_permissions
401 # Check specific permission key
402 permission_key = self._generate_permission_key(function_name, args)
403 if permission_key in self.session_permissions:
404 return True
405
406 # Check pattern-based permissions
407 if function_name == "execute_command" and "command" in args:
408 command = args["command"]
409 base_command = command.split()[0] if isinstance(command, str) and " " in command else command
410 if base_command in self.session_permission_patterns["execute_command"]:
411 return True
412
413 elif function_name in ("read_file", "write_file") and "path" in args:
414 path = args["path"]
415 parent_dir = os.path.dirname(os.path.abspath(path))
416 if parent_dir in self.session_permission_patterns[function_name]:
417 return True
418
419 return False
ollamatools-project/ollamatools/security1.py +0 -11
……@@ -1,11 +0,0 @@
1"""
2Tests for the permission management system.
3"""
4import pytest
5import sys
6import os
7
8# Add the parent directory to the path so we can import the package
9sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
10
11from ollamatools.security import PermissionManager
ollamatools-project/repomix-output.xml +651 -32
……@@ -89,7 +89,6 @@ ollamatools/
8989 opensearch_web_tools.py
9090 path_validator.py
9191 security.py
92 security1.py
9392 url_validator.py
9493ollamatools.egg-info/
9594 dependency_links.txt
……@@ -119,6 +118,7 @@ COMPLETION_REPORT.md
119118CONTRIBUTING.md
120119example_doc.html
121120example_doc.md
121ISSUES.md
122122LICENSE
123123make_executable.sh
124124mkdir_command.sh
……@@ -3859,6 +3859,23 @@ class TransparentOllamaAgent:
38593859 self.context = []
38603860 self.execution_history = []
38613861
3862 # Initialize issue tracking
3863 self.issues = []
3864 self.issue_id_counter = 1
3865
3866 # Path for saving issues
3867 self.issues_file = os.path.join(
3868 os.path.expanduser('~'),
3869 '.ollamatools',
3870 'issues.json'
3871 )
3872
3873 # Create directory if it doesn't exist
3874 os.makedirs(os.path.dirname(self.issues_file), exist_ok=True)
3875
3876 # Load existing issues if any
3877 self._load_issues()
3878
38623879 # Check if OpenSearchNG is available
38633880 try:
38643881 self._print_status("Checking OpenSearchNG availability...")
……@@ -3910,9 +3927,48 @@ class TransparentOllamaAgent:
39103927
39113928 Always check command permissions carefully and explain what each command does.
39123929 For directory structure, use 'find .' or 'ls -la' commands.
3930
3931 When a user asks for information that might require searching the web or retrieving external data,
3932 automatically determine if you should use the 'opensearch_query' function instead of requiring explicit search commands.
3933
3934 For example, if a user asks "can you give me a recipe for chilli sauce?", you should:
3935 1. Recognize this as a request that requires external information
3936 2. Use the opensearch_query function with an appropriate search query
3937 3. Present the results in a helpful format
3938
3939 Only use tools when necessary - for factual questions you can answer directly, don't use search.
39133940 """
39143941
39153942 try:
3943 # Check if the message likely requires a search but doesn't explicitly ask for one
3944 if not message.lower().startswith('search '):
3945 search_indicators = [
3946 "what is", "how to", "who is", "where can", "when did", "why does",
3947 "recipe for", "information about", "tell me about", "find", "look up",
3948 "latest", "news on", "current", "recent"
3949 ]
3950
3951 # Check if the message contains search indicators
3952 needs_search = any(indicator in message.lower() for indicator in search_indicators)
3953
3954 # Check for question structure
3955 question_structure = message.lower().startswith(("what", "how", "who", "where", "when", "why", "can", "could"))
3956
3957 if needs_search or question_structure:
3958 # Check if message is asking for external information we likely don't have
3959 factual_intent = self._detect_factual_intent(message)
3960 if factual_intent:
3961 self._print_info("This appears to be a question that might benefit from search. Searching automatically...")
3962 try:
3963 results = self.execute_search(message)
3964 # Process search results before continuing with normal chat flow
3965 context = self._extract_search_context(results)
3966 # Add context to the user message
3967 message = f"{message}\n\nSearch context: {context}"
3968 except Exception as e:
3969 self._print_error(f"Automatic search failed: {str(e)}")
3970 self._print_info("Continuing without search results")
3971
39163972 # Print what we're doing
39173973 self._print_status(f"Processing message: {message}")
39183974
……@@ -4346,6 +4402,330 @@ class TransparentOllamaAgent:
43464402 """Print a fix suggestion."""
43474403 print(f"💡 Suggested fix: {suggestion}")
43484404
4405 def _detect_factual_intent(self, message: str) -> bool:
4406 """
4407 Detect if a message is likely asking for factual information
4408 that would benefit from search.
4409
4410 Args:
4411 message: User message
4412
4413 Returns:
4414 bool: True if message likely needs external information
4415 """
4416 # Basic topics that often require external information
4417 external_topics = [
4418 "recipe", "weather", "news", "sports", "movie", "book", "author",
4419 "product", "review", "price", "stock", "company", "history",
4420 "person", "celebrity", "politician", "event", "scientific",
4421 "research", "study", "statistic", "data", "definition"
4422 ]
4423
4424 # Check if message contains any of these topics
4425 contains_topic = any(topic in message.lower() for topic in external_topics)
4426
4427 # Check if it's a complex question (longer questions tend to be more specific)
4428 is_complex = len(message.split()) > 6
4429
4430 # Simple heuristic: if it contains a topic of interest and is reasonably complex
4431 return contains_topic and is_complex
4432
4433 def _extract_search_context(self, search_results: Dict[str, Any]) -> str:
4434 """
4435 Extract useful context from search results.
4436
4437 Args:
4438 search_results: Results from opensearch_query
4439
4440 Returns:
4441 str: Formatted context for the agent
4442 """
4443 context_parts = []
4444
4445 # Add direct answer if available
4446 if search_results.get("answer"):
4447 context_parts.append(f"Direct answer: {search_results['answer']}")
4448
4449 # Add snippets from results
4450 if search_results.get("results"):
4451 context_parts.append("Information from search results:")
4452 for i, result in enumerate(search_results["results"][:3]): # Limit to 3 results
4453 title = result.get("title", "Untitled")
4454 content = result.get("content", "No content")
4455 url = result.get("url", "")
4456 context_parts.append(f"- {title}: {content} (Source: {url})")
4457
4458 # Join everything with newlines
4459 return "\n".join(context_parts)
4460
4461 def list_allowed_commands(self) -> List[str]:
4462 """
4463 List all currently allowed commands.
4464
4465 Returns:
4466 List[str]: List of allowed commands
4467 """
4468 # Get the current allowlist from command_validator
4469 from ollamatools.command_validator import DEFAULT_ALLOWLIST
4470
4471 # Combine with any custom entries
4472 all_commands = sorted(list(self.command_allowlist))
4473
4474 # Print the commands
4475 self._print_info(f"Currently allowed commands ({len(all_commands)}):")
4476
4477 # Format into categories for better readability
4478 categories = {
4479 "File Operations": ["ls", "cat", "find", "grep", "head", "tail"],
4480 "Directory Operations": ["pwd", "cd", "mkdir", "rmdir"],
4481 "System Information": ["uname", "hostname", "whoami", "ps", "top", "df", "du", "free"],
4482 "Network Utilities": ["ping", "curl", "wget", "netstat", "dig", "nslookup"],
4483 "Text Processing": ["echo", "printf", "sed", "awk", "cut", "sort", "uniq", "wc"],
4484 "File Manipulation": ["cp", "mv", "rm", "touch", "chmod", "chown", "chgrp", "ln"],
4485 "Other": []
4486 }
4487
4488 # Categorize commands
4489 for cmd in all_commands:
4490 categorized = False
4491 for category, commands in categories.items():
4492 if cmd in commands:
4493 categorized = True
4494 break
4495 if not categorized:
4496 categories["Other"].append(cmd)
4497
4498 # Print by category
4499 for category, commands in categories.items():
4500 if commands:
4501 print(f"\n{category}:")
4502 for i, cmd in enumerate(sorted(commands)):
4503 print(f" {cmd}", end="\t")
4504 # New line every 5 commands for readability
4505 if (i + 1) % 5 == 0:
4506 print()
4507 print() # Ensure we end with a newline
4508
4509 return all_commands
4510
4511 def add_allowed_command(self, command: str) -> bool:
4512 """
4513 Add a command to the allowed list.
4514
4515 Args:
4516 command: Command to add
4517
4518 Returns:
4519 bool: True if added successfully
4520 """
4521 if not command or not isinstance(command, str):
4522 self._print_error("Invalid command name")
4523 return False
4524
4525 # Clean up the command name
4526 command = command.strip().lower()
4527
4528 # Check if already in allowlist
4529 if command in self.command_allowlist:
4530 self._print_info(f"Command '{command}' is already allowed")
4531 return True
4532
4533 # Add to custom allowlist
4534 self.command_allowlist.add(command)
4535 self._print_success(f"Command '{command}' added to allowed list")
4536
4537 # Show warning for potentially dangerous commands
4538 dangerous_commands = {"rm", "mv", "chmod", "chown", "dd", "mkfs", "iptables"}
4539 if command in dangerous_commands:
4540 self._print_warning(f"'{command}' is a potentially dangerous command. Use with caution!")
4541
4542 return True
4543
4544 def remove_allowed_command(self, command: str) -> bool:
4545 """
4546 Remove a command from the allowed list.
4547
4548 Args:
4549 command: Command to remove
4550
4551 Returns:
4552 bool: True if removed successfully
4553 """
4554 if not command or not isinstance(command, str):
4555 self._print_error("Invalid command name")
4556 return False
4557
4558 # Clean up the command name
4559 command = command.strip().lower()
4560
4561 # Check if in allowlist
4562 if command not in self.command_allowlist:
4563 self._print_error(f"Command '{command}' is not in the allowed list")
4564 return False
4565
4566 # Remove from custom allowlist
4567 self.command_allowlist.remove(command)
4568 self._print_success(f"Command '{command}' removed from allowed list")
4569
4570 return True
4571
4572 def _load_issues(self) -> None:
4573 """Load saved issues from file."""
4574 if os.path.exists(self.issues_file):
4575 try:
4576 with open(self.issues_file, 'r') as f:
4577 data = json.load(f)
4578 self.issues = data.get('issues', [])
4579 self.issue_id_counter = data.get('next_id', 1)
4580 logger.info(f"Loaded {len(self.issues)} issues from {self.issues_file}")
4581 except Exception as e:
4582 logger.error(f"Failed to load issues: {str(e)}")
4583 self.issues = []
4584 self.issue_id_counter = 1
4585
4586 def _save_issues(self) -> None:
4587 """Save issues to file."""
4588 try:
4589 with open(self.issues_file, 'w') as f:
4590 json.dump({
4591 'issues': self.issues,
4592 'next_id': self.issue_id_counter
4593 }, f, indent=2)
4594 logger.info(f"Saved {len(self.issues)} issues to {self.issues_file}")
4595 except Exception as e:
4596 logger.error(f"Failed to save issues: {str(e)}")
4597
4598 def add_issue(self, title: str, description: str, severity: str = 'medium') -> int:
4599 """
4600 Add a new issue to the tracker.
4601
4602 Args:
4603 title: Issue title
4604 description: Issue description
4605 severity: Issue severity (low, medium, high)
4606
4607 Returns:
4608 int: Issue ID
4609 """
4610 # Validate severity
4611 if severity not in ['low', 'medium', 'high']:
4612 severity = 'medium'
4613
4614 # Create issue
4615 issue = {
4616 'id': self.issue_id_counter,
4617 'title': title,
4618 'description': description,
4619 'severity': severity,
4620 'status': 'open',
4621 'created_at': time.strftime('%Y-%m-%d %H:%M:%S'),
4622 'updated_at': time.strftime('%Y-%m-%d %H:%M:%S')
4623 }
4624
4625 # Add to list and increment counter
4626 self.issues.append(issue)
4627 self.issue_id_counter += 1
4628
4629 # Save issues
4630 self._save_issues()
4631
4632 self._print_success(f"Issue #{issue['id']} added: {title}")
4633 return issue['id']
4634
4635 def list_issues(self, status: Optional[str] = None) -> List[Dict[str, Any]]:
4636 """
4637 List all issues, optionally filtered by status.
4638
4639 Args:
4640 status: Optional status filter (open, resolved)
4641
4642 Returns:
4643 List[Dict[str, Any]]: List of issues
4644 """
4645 # Filter issues if status provided
4646 filtered_issues = self.issues
4647 if status:
4648 filtered_issues = [i for i in self.issues if i['status'] == status]
4649
4650 # Print issues
4651 if not filtered_issues:
4652 self._print_info("No issues found")
4653 return []
4654
4655 self._print_info(f"Issues ({len(filtered_issues)}):")
4656 print("\nID | Severity | Status | Title")
4657 print("----|----------|----------|-------------------")
4658
4659 for issue in filtered_issues:
4660 print(f"{issue['id']:<4}| {issue['severity']:<9}| {issue['status']:<9}| {issue['title']}")
4661
4662 return filtered_issues
4663
4664 def get_issue(self, issue_id: int) -> Optional[Dict[str, Any]]:
4665 """
4666 Get details of a specific issue.
4667
4668 Args:
4669 issue_id: Issue ID
4670
4671 Returns:
4672 Optional[Dict[str, Any]]: Issue details or None if not found
4673 """
4674 # Find issue
4675 for issue in self.issues:
4676 if issue['id'] == issue_id:
4677 # Print issue details
4678 self._print_info(f"Issue #{issue['id']}: {issue['title']}")
4679 print(f"Status: {issue['status']}")
4680 print(f"Severity: {issue['severity']}")
4681 print(f"Created: {issue['created_at']}")
4682 print(f"Updated: {issue['updated_at']}")
4683 print("\nDescription:")
4684 print(issue['description'])
4685 return issue
4686
4687 self._print_error(f"Issue #{issue_id} not found")
4688 return None
4689
4690 def update_issue(self, issue_id: int, status: Optional[str] = None,
4691 severity: Optional[str] = None, description: Optional[str] = None) -> bool:
4692 """
4693 Update an existing issue.
4694
4695 Args:
4696 issue_id: Issue ID
4697 status: New status (open, resolved)
4698 severity: New severity (low, medium, high)
4699 description: New description
4700
4701 Returns:
4702 bool: True if updated successfully
4703 """
4704 # Find issue
4705 for i, issue in enumerate(self.issues):
4706 if issue['id'] == issue_id:
4707 # Update fields
4708 if status and status in ['open', 'resolved']:
4709 self.issues[i]['status'] = status
4710
4711 if severity and severity in ['low', 'medium', 'high']:
4712 self.issues[i]['severity'] = severity
4713
4714 if description:
4715 self.issues[i]['description'] = description
4716
4717 # Update timestamp
4718 self.issues[i]['updated_at'] = time.strftime('%Y-%m-%d %H:%M:%S')
4719
4720 # Save issues
4721 self._save_issues()
4722
4723 self._print_success(f"Issue #{issue_id} updated")
4724 return True
4725
4726 self._print_error(f"Issue #{issue_id} not found")
4727 return False
4728
43494729
43504730def main():
43514731 """Run the transparent Ollama agent."""
……@@ -4362,10 +4742,19 @@ def main():
43624742 print("========================")
43634743 print("Type 'exit' to quit")
43644744 print("Type 'commands' to see available commands")
4745 print("Type 'add_command <name>' to add a command to the allowed list")
4746 print("Type 'remove_command <name>' to remove a command from the allowed list")
43654747 print("Type 'search <query>' to search directly")
43664748 print("Type 'run <command>' to execute a command directly")
43674749 print("Type 'dir' to show current directory structure")
43684750 print("Type 'history' to see execution history")
4751
4752 print("\nIssue Tracking:")
4753 print(" issues - List all issues")
4754 print(" issue <id> - Show issue details")
4755 print(" add_issue <title> | <description> | [severity] - Add a new issue")
4756 print(" resolve_issue <id> - Mark an issue as resolved")
4757
43694758 print("\nOtherwise, just chat naturally with the agent!")
43704759
43714760 # Simple chat loop
……@@ -4380,17 +4769,17 @@ def main():
43804769
43814770 # Check for special commands
43824771 if user_input.lower() == 'commands':
4383 print("\nAvailable Commands:")
4384 print("=" * 60)
4385 print("General Commands:")
4386 print(" exit, quit, bye - Exit the program")
4387 print(" commands - Show this help")
4388 print(" history - Show execution history")
4389 print("\nDirect Operations:")
4390 print(" search <query> - Search directly using OpenSearchNG")
4391 print(" run <command> - Execute a command directly")
4392 print(" dir [path] [depth] - Show directory structure")
4393 print("=" * 60)
4772 agent.list_allowed_commands()
4773 continue
4774
4775 if user_input.lower().startswith('add_command '):
4776 command = user_input[len('add_command '):].strip()
4777 agent.add_allowed_command(command)
4778 continue
4779
4780 if user_input.lower().startswith('remove_command '):
4781 command = user_input[len('remove_command '):].strip()
4782 agent.remove_allowed_command(command)
43944783 continue
43954784
43964785 if user_input.lower() == 'history':
……@@ -4446,6 +4835,39 @@ def main():
44464835 print(f"Failed to get directory structure: {str(e)}")
44474836 continue
44484837
4838 # Issue tracking commands
4839 if user_input.lower() == 'issues':
4840 agent.list_issues()
4841 continue
4842
4843 if user_input.lower().startswith('issue '):
4844 try:
4845 issue_id = int(user_input[len('issue '):].strip())
4846 agent.get_issue(issue_id)
4847 except ValueError:
4848 print("Invalid issue ID. Use 'issue <number>'.")
4849 continue
4850
4851 if user_input.lower().startswith('add_issue '):
4852 # Parse command: add_issue Title | Description | [severity]
4853 parts = user_input[len('add_issue '):].split('|')
4854 if len(parts) >= 2:
4855 title = parts[0].strip()
4856 description = parts[1].strip()
4857 severity = parts[2].strip().lower() if len(parts) > 2 else 'medium'
4858 agent.add_issue(title, description, severity)
4859 else:
4860 print("Usage: add_issue Title | Description | [severity]")
4861 continue
4862
4863 if user_input.lower().startswith('resolve_issue '):
4864 try:
4865 issue_id = int(user_input[len('resolve_issue '):].strip())
4866 agent.update_issue(issue_id, status='resolved')
4867 except ValueError:
4868 print("Invalid issue ID. Use 'resolve_issue <number>'.")
4869 continue
4870
44494871 # Process as normal chat message
44504872 response = agent.chat(user_input)
44514873 print(f"\nAssistant: {response}")
……@@ -8318,6 +8740,7 @@ Permission management system for OllamaTools.
83188740This module provides security controls for executing functions,
83198741with a focus on a prompt-based permission system.
83208742"""
8743import os
83218744from typing import Dict, Any, Optional, Tuple, Set
83228745import logging
83238746import re
……@@ -8357,10 +8780,44 @@ class PermissionManager:
83578780 self.prompt_enabled = prompt_enabled
83588781 self.sudo_always_prompt = sudo_always_prompt
83598782 self.session_permissions = set() # Store allowed operations for the session
8783 self.session_permission_patterns = {
8784 "read_file": {}, # Directory-based permissions
8785 "write_file": {}, # Directory-based permissions
8786 "execute_command": {} # Command-based permissions
8787 }
83608788
83618789 logger.debug("PermissionManager initialized with prompt_enabled=%s, sudo_always_prompt=%s",
83628790 prompt_enabled, sudo_always_prompt)
83638791
8792 def _generate_permission_key(self, function_name: str, args: Dict[str, Any]) -> str:
8793 """
8794 Generate a more flexible permission key that recognizes similar operations.
8795
8796 Args:
8797 function_name: Name of the function
8798 args: Function arguments
8799
8800 Returns:
8801 str: Permission key
8802 """
8803 # For commands, focus on the command name rather than all arguments
8804 if function_name == "execute_command" and "command" in args:
8805 command = args["command"]
8806 # Just use the base command without all arguments for matching
8807 base_command = command.split()[0] if isinstance(command, str) and " " in command else command
8808 return f"{function_name}:command={base_command}"
8809
8810 # For file operations, focus on the operation type and parent directory
8811 if function_name in ("read_file", "write_file") and "path" in args:
8812 path = args["path"]
8813 # Use parent directory for path-based operations to match similar files
8814 parent_dir = os.path.dirname(os.path.abspath(path))
8815 return f"{function_name}:dir={parent_dir}"
8816
8817 # For other operations, use the full args as before
8818 args_str = ', '.join(f"{k}={v!r}" for k, v in args.items())
8819 return f"{function_name}:{args_str}"
8820
83648821 def request_permission(
83658822 self,
83668823 function_name: str,
……@@ -8388,7 +8845,7 @@ class PermissionManager:
83888845 args_str = ', '.join(f"{k}={v!r}" for k, v in args.items())
83898846
83908847 # Generate a permission key for the operation
8391 permission_key = f"{function_name}:{args_str}"
8848 permission_key = self._generate_permission_key(function_name, args)
83928849
83938850 # Check if we need to prompt for permission
83948851 should_prompt = self.prompt_enabled
……@@ -8403,6 +8860,21 @@ class PermissionManager:
84038860 logger.debug("Permission already granted for session: %s", permission_key)
84048861 return True
84058862
8863 # Check specific session permission patterns
8864 if function_name == "execute_command" and "command" in args:
8865 command = args["command"]
8866 base_command = command.split()[0] if isinstance(command, str) and " " in command else command
8867 if base_command in self.session_permission_patterns["execute_command"]:
8868 logger.debug("Permission already granted for command type: %s", base_command)
8869 return True
8870
8871 elif function_name in ("read_file", "write_file") and "path" in args:
8872 path = args["path"]
8873 parent_dir = os.path.dirname(os.path.abspath(path))
8874 if parent_dir in self.session_permission_patterns[function_name]:
8875 logger.debug("Permission already granted for directory: %s", parent_dir)
8876 return True
8877
84068878 # If prompting is disabled and not overridden by sudo, grant permission
84078879 if not should_prompt:
84088880 logger.debug("Permission prompts disabled, automatically granting permission for: %s", permission_key)
……@@ -8435,6 +8907,20 @@ class PermissionManager:
84358907
84368908 elif response == 'S': # Allow for session
84378909 self.session_permissions.add(permission_key)
8910
8911 # Store more specific pattern matches for better session handling
8912 if function_name == "execute_command" and "command" in args:
8913 command = args["command"]
8914 base_command = command.split()[0] if isinstance(command, str) and " " in command else command
8915 self.session_permission_patterns["execute_command"][base_command] = True
8916 logger.info(f"Command '{base_command}' allowed for session")
8917
8918 elif function_name in ("read_file", "write_file") and "path" in args:
8919 path = args["path"]
8920 parent_dir = os.path.dirname(os.path.abspath(path))
8921 self.session_permission_patterns[function_name][parent_dir] = True
8922 logger.info(f"{function_name} allowed for directory: {parent_dir}")
8923
84388924 logger.info("Permission granted for session: %s", permission_key)
84398925 return True
84408926
……@@ -8608,6 +9094,9 @@ class PermissionManager:
86089094 """
86099095 num_permissions = len(self.session_permissions)
86109096 self.session_permissions.clear()
9097 # Clear pattern-based permissions too
9098 for key in self.session_permission_patterns:
9099 self.session_permission_patterns[key] = {}
86119100 logger.info("Cleared %d session permissions", num_permissions)
86129101
86139102 def add_session_permission(self, function_name: str, args: Dict[str, Any]):
……@@ -8618,9 +9107,20 @@ class PermissionManager:
86189107 function_name: Name of the function
86199108 args: Function arguments
86209109 """
8621 args_str = ', '.join(f"{k}={v!r}" for k, v in args.items())
8622 permission_key = f"{function_name}:{args_str}"
9110 permission_key = self._generate_permission_key(function_name, args)
86239111 self.session_permissions.add(permission_key)
9112
9113 # Add to pattern-based permissions
9114 if function_name == "execute_command" and "command" in args:
9115 command = args["command"]
9116 base_command = command.split()[0] if isinstance(command, str) and " " in command else command
9117 self.session_permission_patterns["execute_command"][base_command] = True
9118
9119 elif function_name in ("read_file", "write_file") and "path" in args:
9120 path = args["path"]
9121 parent_dir = os.path.dirname(os.path.abspath(path))
9122 self.session_permission_patterns[function_name][parent_dir] = True
9123
86249124 logger.info("Added session permission: %s", permission_key)
86259125
86269126 def has_session_permission(self, function_name: str, args: Dict[str, Any]) -> bool:
……@@ -8634,23 +9134,25 @@ class PermissionManager:
86349134 Returns:
86359135 bool: True if the operation has session permission
86369136 """
8637 args_str = ', '.join(f"{k}={v!r}" for k, v in args.items())
8638 permission_key = f"{function_name}:{args_str}"
8639 return permission_key in self.session_permissions
8640</file>
8641
8642<file path="ollamatools/security1.py">
8643"""
8644Tests for the permission management system.
8645"""
8646import pytest
8647import sys
8648import os
8649
8650# Add the parent directory to the path so we can import the package
8651sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
8652
8653from ollamatools.security import PermissionManager
9137 # Check specific permission key
9138 permission_key = self._generate_permission_key(function_name, args)
9139 if permission_key in self.session_permissions:
9140 return True
9141
9142 # Check pattern-based permissions
9143 if function_name == "execute_command" and "command" in args:
9144 command = args["command"]
9145 base_command = command.split()[0] if isinstance(command, str) and " " in command else command
9146 if base_command in self.session_permission_patterns["execute_command"]:
9147 return True
9148
9149 elif function_name in ("read_file", "write_file") and "path" in args:
9150 path = args["path"]
9151 parent_dir = os.path.dirname(os.path.abspath(path))
9152 if parent_dir in self.session_permission_patterns[function_name]:
9153 return True
9154
9155 return False
86549156</file>
86559157
86569158<file path="ollamatools/url_validator.py">
……@@ -12109,6 +12611,123 @@ html = generate_markdown(content, title, format_type='html')
1210912611```
1211012612</file>
1211112613
12614<file path="ISSUES.md">
12615# Issues for the Transparent Ollama Agent
12616
12617## Original Issues Status
12618
126191. **Natural Language Understanding for Search**
12620 - **Problem**: The agent requires explicit search commands rather than understanding natural language requests.
12621 - **Description**: Currently, users must type "search [query]" instead of casual questions like "can you give me a recipe for chilli sauce?"
12622 - **Status**: PARTIALLY RESOLVED ⚠️
12623 - **Resolution**:
12624 - Implemented search intent detection and automatic search trigger in the code
12625 - Added the necessary logic to enhance natural language understanding
12626 - **BUT**: Testing revealed that the agent is returning raw JSON function call data instead of executing it
12627 - **Further Action Needed**:
12628 - Fixed the implementation of the `chat` method to properly detect function calls in the LLM response
12629 - Added JSON parsing and explicit function execution when a search function call is detected
12630 - Need to test this fix to confirm it properly handles natural language search requests
12631
126322. **Session Permission Not Working as Expected**
12633 - **Problem**: The "Allow for Session" option doesn't prevent further permission prompts.
12634 - **Description**: When typing 'S' for session-level permissions, the user still gets prompted for similar operations.
12635 - **Status**: RESOLVED ✅
12636 - **Resolution**: Enhanced the permission system in `ollamatools/security.py` to use more flexible permission keys that recognize similar operations. Implemented command-based and directory-based permission patterns for better session handling.
12637 - **Testing Needed**: Verify the improvements by testing with various similar commands and file operations
12638
126393. **Command List Management**
12640 - **Problem**: No easy way to view, add or remove allowed commands.
12641 - **Description**: Users need the ability to manage the list of allowed commands through the interface.
12642 - **Status**: RESOLVED ✅
12643 - **Resolution**: Added command management features to `examples/transparent_ollama_agent.py` including new commands: `commands` (to list all allowed commands), `add_command` (to add a command to the allowlist), and `remove_command` (to remove a command from the allowlist).
12644
126454. **Lack of Structured Issue Tracking**
12646 - **Problem**: No built-in way to log and manage issues discovered during operation.
12647 - **Description**: Currently, there's no mechanism to save and track issues discovered during operation.
12648 - **Status**: RESOLVED ✅
12649 - **Resolution**: Implemented a full issue tracking system in `examples/transparent_ollama_agent.py` with persistence to disk. Added commands for managing issues: `issues` (to list all issues), `issue <id>` (to show issue details), `add_issue` (to add a new issue), and `resolve_issue` (to mark an issue as resolved).
12650
12651## New Issues Identified
12652
126535. **Code Organization and Modularity**
12654 - **Problem**: The transparent agent code is in a single large file with multiple responsibilities.
12655 - **Description**: The `transparent_ollama_agent.py` has grown to include many features (search, commands, issues, etc.) making it difficult to maintain and extend.
12656 - **Status**: IDENTIFIED 🆕
12657 - **Proposed Solution**: Refactor the code into a modular structure:
12658 ```
12659 ollamatools/
12660 ├── agent/
12661 │ └── transparent/
12662 │ ├── __init__.py # Package initialization
12663 │ ├── agent.py # TransparentAgent class
12664 │ ├── search.py # Search functionality
12665 │ ├── commands.py # Command management
12666 │ ├── issues.py # Issue tracking
12667 │ └── utils.py # Utility functions
12668 └── examples/
12669 └── transparent_agent.py # Main CLI entry point
12670 ```
12671 - This structure aligns the file organization with the namespace, placing all transparent agent code in a dedicated directory under `ollamatools/agent/transparent/`.
12672
126736. **OpenSearchNG Integration Error Handling**
12674 - **Problem**: The agent fails with a type error when OpenSearchNG is not available.
12675 - **Description**: When OpenSearchNG is not running, the error "sequence item 0: expected str instance, dict found" occurs, indicating improper error handling.
12676 - **Status**: IDENTIFIED 🆕
12677 - **Proposed Solution**: Improve error handling in the OpenSearchNG integration code to gracefully handle connection issues and provide clearer error messages.
12678
126797. **Inconsistent Function Execution**
12680 - **Problem**: The agent sometimes returns function calls as raw JSON instead of executing them.
12681 - **Description**: When the LLM returns a function call in JSON format, the agent is not properly parsing and executing it.
12682 - **Status**: PARTIALLY RESOLVED ⚠️
12683 - **Resolution**: Added code to parse JSON function calls and execute them appropriately, but this needs testing.
12684 - **Further Action Needed**: Comprehensive testing with different types of queries and function calls.
12685
12686## Implementation Notes
12687
12688### Natural Language Understanding for Search
12689- Fixed the `chat` method in `TransparentAgent` to handle function calls properly:
12690 - Added JSON parsing for responses that appear to be function calls
12691 - Implemented direct execution of search functions when detected
12692 - Enhanced result formatting to create user-friendly responses
12693
12694### Code Organization Plan
12695- Break down the large `transparent_ollama_agent.py` file into several specialized modules:
12696 - `agent.py`: Core agent class with initialization and chat handling
12697 - `search.py`: Search intent detection and execution
12698 - `commands.py`: Command management functionality
12699 - `issues.py`: Issue tracking system
12700 - `utils.py`: Display helpers and common utilities
12701- Create a dedicated namespace under `ollamatools.agent.transparent`
12702- Update imports and maintain backward compatibility
12703- Create proper interfaces between components
12704
12705### OpenSearchNG Error Handling
12706- Add better exception handling in the OpenSearchNG integration code
12707- Implement a fallback mechanism for when OpenSearchNG is not available
12708- Add clear error messages and suggestions for fixing connection issues
12709
12710## Future Considerations
12711
127121. **Advanced Search Intent Detection**: The current implementation uses simple heuristics for intent detection. Future versions could use more sophisticated language understanding techniques.
12713
127142. **Fine-grained Permission System**: Enhance the permission system to support more granular permissions and remember user preferences across sessions.
12715
127163. **Command Group Permissions**: Allow users to grant permissions to groups of related commands rather than individual commands.
12717
127184. **Issue Categories and Labels**: Add support for categorizing issues and applying labels for better organization.
12719
127205. **Export/Import Issues**: Add functionality to export issues to standard formats like CSV or JSON for integration with external issue tracking systems.
12721
127226. **Advanced Code Analysis**: Implement code analysis features to help identify potential issues in code being developed with the agent.
12723
127247. **Plugin System**: Create a plugin architecture to allow for easy extension of agent capabilities.
12725
127268. **Configuration Management**: Add proper configuration file support for customizing agent behavior.
12727
127289. **Testing Framework**: Develop a comprehensive testing framework for validating agent functionality.
12729</file>
12730
1211212731<file path="LICENSE">
1211312732MIT License
1211412733