| … | … | @@ -89,7 +89,6 @@ ollamatools/ |
| 89 | 89 | opensearch_web_tools.py |
| 90 | 90 | path_validator.py |
| 91 | 91 | security.py |
| 92 | | security1.py |
| 93 | 92 | url_validator.py |
| 94 | 93 | ollamatools.egg-info/ |
| 95 | 94 | dependency_links.txt |
| … | … | @@ -119,6 +118,7 @@ COMPLETION_REPORT.md |
| 119 | 118 | CONTRIBUTING.md |
| 120 | 119 | example_doc.html |
| 121 | 120 | example_doc.md |
| 121 | ISSUES.md |
| 122 | 122 | LICENSE |
| 123 | 123 | make_executable.sh |
| 124 | 124 | mkdir_command.sh |
| … | … | @@ -3859,6 +3859,23 @@ class TransparentOllamaAgent: |
| 3859 | 3859 | self.context = [] |
| 3860 | 3860 | self.execution_history = [] |
| 3861 | 3861 | |
| 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 | |
| 3862 | 3879 | # Check if OpenSearchNG is available |
| 3863 | 3880 | try: |
| 3864 | 3881 | self._print_status("Checking OpenSearchNG availability...") |
| … | … | @@ -3910,9 +3927,48 @@ class TransparentOllamaAgent: |
| 3910 | 3927 | |
| 3911 | 3928 | Always check command permissions carefully and explain what each command does. |
| 3912 | 3929 | 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. |
| 3913 | 3940 | """ |
| 3914 | 3941 | |
| 3915 | 3942 | 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 | |
| 3916 | 3972 | # Print what we're doing |
| 3917 | 3973 | self._print_status(f"Processing message: {message}") |
| 3918 | 3974 | |
| … | … | @@ -4346,6 +4402,330 @@ class TransparentOllamaAgent: |
| 4346 | 4402 | """Print a fix suggestion.""" |
| 4347 | 4403 | print(f"💡 Suggested fix: {suggestion}") |
| 4348 | 4404 | |
| 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 | |
| 4349 | 4729 | |
| 4350 | 4730 | def main(): |
| 4351 | 4731 | """Run the transparent Ollama agent.""" |
| … | … | @@ -4362,10 +4742,19 @@ def main(): |
| 4362 | 4742 | print("========================") |
| 4363 | 4743 | print("Type 'exit' to quit") |
| 4364 | 4744 | 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") |
| 4365 | 4747 | print("Type 'search <query>' to search directly") |
| 4366 | 4748 | print("Type 'run <command>' to execute a command directly") |
| 4367 | 4749 | print("Type 'dir' to show current directory structure") |
| 4368 | 4750 | 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 | |
| 4369 | 4758 | print("\nOtherwise, just chat naturally with the agent!") |
| 4370 | 4759 | |
| 4371 | 4760 | # Simple chat loop |
| … | … | @@ -4380,17 +4769,17 @@ def main(): |
| 4380 | 4769 | |
| 4381 | 4770 | # Check for special commands |
| 4382 | 4771 | 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) |
| 4394 | 4783 | continue |
| 4395 | 4784 | |
| 4396 | 4785 | if user_input.lower() == 'history': |
| … | … | @@ -4446,6 +4835,39 @@ def main(): |
| 4446 | 4835 | print(f"Failed to get directory structure: {str(e)}") |
| 4447 | 4836 | continue |
| 4448 | 4837 | |
| 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 | |
| 4449 | 4871 | # Process as normal chat message |
| 4450 | 4872 | response = agent.chat(user_input) |
| 4451 | 4873 | print(f"\nAssistant: {response}") |
| … | … | @@ -8318,6 +8740,7 @@ Permission management system for OllamaTools. |
| 8318 | 8740 | This module provides security controls for executing functions, |
| 8319 | 8741 | with a focus on a prompt-based permission system. |
| 8320 | 8742 | """ |
| 8743 | import os |
| 8321 | 8744 | from typing import Dict, Any, Optional, Tuple, Set |
| 8322 | 8745 | import logging |
| 8323 | 8746 | import re |
| … | … | @@ -8357,10 +8780,44 @@ class PermissionManager: |
| 8357 | 8780 | self.prompt_enabled = prompt_enabled |
| 8358 | 8781 | self.sudo_always_prompt = sudo_always_prompt |
| 8359 | 8782 | 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 | } |
| 8360 | 8788 | |
| 8361 | 8789 | logger.debug("PermissionManager initialized with prompt_enabled=%s, sudo_always_prompt=%s", |
| 8362 | 8790 | prompt_enabled, sudo_always_prompt) |
| 8363 | 8791 | |
| 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 | |
| 8364 | 8821 | def request_permission( |
| 8365 | 8822 | self, |
| 8366 | 8823 | function_name: str, |
| … | … | @@ -8388,7 +8845,7 @@ class PermissionManager: |
| 8388 | 8845 | args_str = ', '.join(f"{k}={v!r}" for k, v in args.items()) |
| 8389 | 8846 | |
| 8390 | 8847 | # 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) |
| 8392 | 8849 | |
| 8393 | 8850 | # Check if we need to prompt for permission |
| 8394 | 8851 | should_prompt = self.prompt_enabled |
| … | … | @@ -8403,6 +8860,21 @@ class PermissionManager: |
| 8403 | 8860 | logger.debug("Permission already granted for session: %s", permission_key) |
| 8404 | 8861 | return True |
| 8405 | 8862 | |
| 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 | |
| 8406 | 8878 | # If prompting is disabled and not overridden by sudo, grant permission |
| 8407 | 8879 | if not should_prompt: |
| 8408 | 8880 | logger.debug("Permission prompts disabled, automatically granting permission for: %s", permission_key) |
| … | … | @@ -8435,6 +8907,20 @@ class PermissionManager: |
| 8435 | 8907 | |
| 8436 | 8908 | elif response == 'S': # Allow for session |
| 8437 | 8909 | 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 | |
| 8438 | 8924 | logger.info("Permission granted for session: %s", permission_key) |
| 8439 | 8925 | return True |
| 8440 | 8926 | |
| … | … | @@ -8608,6 +9094,9 @@ class PermissionManager: |
| 8608 | 9094 | """ |
| 8609 | 9095 | num_permissions = len(self.session_permissions) |
| 8610 | 9096 | self.session_permissions.clear() |
| 9097 | # Clear pattern-based permissions too |
| 9098 | for key in self.session_permission_patterns: |
| 9099 | self.session_permission_patterns[key] = {} |
| 8611 | 9100 | logger.info("Cleared %d session permissions", num_permissions) |
| 8612 | 9101 | |
| 8613 | 9102 | def add_session_permission(self, function_name: str, args: Dict[str, Any]): |
| … | … | @@ -8618,9 +9107,20 @@ class PermissionManager: |
| 8618 | 9107 | function_name: Name of the function |
| 8619 | 9108 | args: Function arguments |
| 8620 | 9109 | """ |
| 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) |
| 8623 | 9111 | 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 | |
| 8624 | 9124 | logger.info("Added session permission: %s", permission_key) |
| 8625 | 9125 | |
| 8626 | 9126 | def has_session_permission(self, function_name: str, args: Dict[str, Any]) -> bool: |
| … | … | @@ -8634,23 +9134,25 @@ class PermissionManager: |
| 8634 | 9134 | Returns: |
| 8635 | 9135 | bool: True if the operation has session permission |
| 8636 | 9136 | """ |
| 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 | | """ |
| 8644 | | Tests for the permission management system. |
| 8645 | | """ |
| 8646 | | import pytest |
| 8647 | | import sys |
| 8648 | | import os |
| 8649 | | |
| 8650 | | # Add the parent directory to the path so we can import the package |
| 8651 | | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) |
| 8652 | | |
| 8653 | | from 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 |
| 8654 | 9156 | </file> |
| 8655 | 9157 | |
| 8656 | 9158 | <file path="ollamatools/url_validator.py"> |
| … | … | @@ -12109,6 +12611,123 @@ html = generate_markdown(content, title, format_type='html') |
| 12109 | 12611 | ``` |
| 12110 | 12612 | </file> |
| 12111 | 12613 | |
| 12614 | <file path="ISSUES.md"> |
| 12615 | # Issues for the Transparent Ollama Agent |
| 12616 | |
| 12617 | ## Original Issues Status |
| 12618 | |
| 12619 | 1. **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 | |
| 12632 | 2. **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 | |
| 12639 | 3. **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 | |
| 12645 | 4. **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 | |
| 12653 | 5. **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 | |
| 12673 | 6. **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 | |
| 12679 | 7. **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 | |
| 12712 | 1. **Advanced Search Intent Detection**: The current implementation uses simple heuristics for intent detection. Future versions could use more sophisticated language understanding techniques. |
| 12713 | |
| 12714 | 2. **Fine-grained Permission System**: Enhance the permission system to support more granular permissions and remember user preferences across sessions. |
| 12715 | |
| 12716 | 3. **Command Group Permissions**: Allow users to grant permissions to groups of related commands rather than individual commands. |
| 12717 | |
| 12718 | 4. **Issue Categories and Labels**: Add support for categorizing issues and applying labels for better organization. |
| 12719 | |
| 12720 | 5. **Export/Import Issues**: Add functionality to export issues to standard formats like CSV or JSON for integration with external issue tracking systems. |
| 12721 | |
| 12722 | 6. **Advanced Code Analysis**: Implement code analysis features to help identify potential issues in code being developed with the agent. |
| 12723 | |
| 12724 | 7. **Plugin System**: Create a plugin architecture to allow for easy extension of agent capabilities. |
| 12725 | |
| 12726 | 8. **Configuration Management**: Add proper configuration file support for customizing agent behavior. |
| 12727 | |
| 12728 | 9. **Testing Framework**: Develop a comprehensive testing framework for validating agent functionality. |
| 12729 | </file> |
| 12730 | |
| 12112 | 12731 | <file path="LICENSE"> |
| 12113 | 12732 | MIT License |
| 12114 | 12733 | |