quantalogic 0.35.0__py3-none-any.whl → 0.50.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. quantalogic/__init__.py +0 -4
  2. quantalogic/agent.py +603 -363
  3. quantalogic/agent_config.py +233 -46
  4. quantalogic/agent_factory.py +34 -22
  5. quantalogic/coding_agent.py +16 -14
  6. quantalogic/config.py +2 -1
  7. quantalogic/console_print_events.py +4 -8
  8. quantalogic/console_print_token.py +2 -2
  9. quantalogic/docs_cli.py +15 -10
  10. quantalogic/event_emitter.py +258 -83
  11. quantalogic/flow/__init__.py +23 -0
  12. quantalogic/flow/flow.py +595 -0
  13. quantalogic/flow/flow_extractor.py +672 -0
  14. quantalogic/flow/flow_generator.py +89 -0
  15. quantalogic/flow/flow_manager.py +407 -0
  16. quantalogic/flow/flow_manager_schema.py +169 -0
  17. quantalogic/flow/flow_yaml.md +419 -0
  18. quantalogic/generative_model.py +109 -77
  19. quantalogic/get_model_info.py +5 -5
  20. quantalogic/interactive_text_editor.py +100 -73
  21. quantalogic/main.py +17 -21
  22. quantalogic/model_info_list.py +3 -3
  23. quantalogic/model_info_litellm.py +14 -14
  24. quantalogic/prompts.py +2 -1
  25. quantalogic/{llm.py → quantlitellm.py} +29 -39
  26. quantalogic/search_agent.py +4 -4
  27. quantalogic/server/models.py +4 -1
  28. quantalogic/task_file_reader.py +5 -5
  29. quantalogic/task_runner.py +20 -20
  30. quantalogic/tool_manager.py +10 -21
  31. quantalogic/tools/__init__.py +98 -68
  32. quantalogic/tools/composio/composio.py +416 -0
  33. quantalogic/tools/{generate_database_report_tool.py → database/generate_database_report_tool.py} +4 -9
  34. quantalogic/tools/database/sql_query_tool_advanced.py +261 -0
  35. quantalogic/tools/document_tools/markdown_to_docx_tool.py +620 -0
  36. quantalogic/tools/document_tools/markdown_to_epub_tool.py +438 -0
  37. quantalogic/tools/document_tools/markdown_to_html_tool.py +362 -0
  38. quantalogic/tools/document_tools/markdown_to_ipynb_tool.py +319 -0
  39. quantalogic/tools/document_tools/markdown_to_latex_tool.py +420 -0
  40. quantalogic/tools/document_tools/markdown_to_pdf_tool.py +623 -0
  41. quantalogic/tools/document_tools/markdown_to_pptx_tool.py +319 -0
  42. quantalogic/tools/duckduckgo_search_tool.py +2 -4
  43. quantalogic/tools/finance/alpha_vantage_tool.py +440 -0
  44. quantalogic/tools/finance/ccxt_tool.py +373 -0
  45. quantalogic/tools/finance/finance_llm_tool.py +387 -0
  46. quantalogic/tools/finance/google_finance.py +192 -0
  47. quantalogic/tools/finance/market_intelligence_tool.py +520 -0
  48. quantalogic/tools/finance/technical_analysis_tool.py +491 -0
  49. quantalogic/tools/finance/tradingview_tool.py +336 -0
  50. quantalogic/tools/finance/yahoo_finance.py +236 -0
  51. quantalogic/tools/git/bitbucket_clone_repo_tool.py +181 -0
  52. quantalogic/tools/git/bitbucket_operations_tool.py +326 -0
  53. quantalogic/tools/git/clone_repo_tool.py +189 -0
  54. quantalogic/tools/git/git_operations_tool.py +532 -0
  55. quantalogic/tools/google_packages/google_news_tool.py +480 -0
  56. quantalogic/tools/grep_app_tool.py +123 -186
  57. quantalogic/tools/{dalle_e.py → image_generation/dalle_e.py} +37 -27
  58. quantalogic/tools/jinja_tool.py +6 -10
  59. quantalogic/tools/language_handlers/__init__.py +22 -9
  60. quantalogic/tools/list_directory_tool.py +131 -42
  61. quantalogic/tools/llm_tool.py +45 -15
  62. quantalogic/tools/llm_vision_tool.py +59 -7
  63. quantalogic/tools/markitdown_tool.py +17 -5
  64. quantalogic/tools/nasa_packages/models.py +47 -0
  65. quantalogic/tools/nasa_packages/nasa_apod_tool.py +232 -0
  66. quantalogic/tools/nasa_packages/nasa_neows_tool.py +147 -0
  67. quantalogic/tools/nasa_packages/services.py +82 -0
  68. quantalogic/tools/presentation_tools/presentation_llm_tool.py +396 -0
  69. quantalogic/tools/product_hunt/product_hunt_tool.py +258 -0
  70. quantalogic/tools/product_hunt/services.py +63 -0
  71. quantalogic/tools/rag_tool/__init__.py +48 -0
  72. quantalogic/tools/rag_tool/document_metadata.py +15 -0
  73. quantalogic/tools/rag_tool/query_response.py +20 -0
  74. quantalogic/tools/rag_tool/rag_tool.py +566 -0
  75. quantalogic/tools/rag_tool/rag_tool_beta.py +264 -0
  76. quantalogic/tools/read_html_tool.py +24 -38
  77. quantalogic/tools/replace_in_file_tool.py +10 -10
  78. quantalogic/tools/safe_python_interpreter_tool.py +10 -24
  79. quantalogic/tools/search_definition_names.py +2 -2
  80. quantalogic/tools/sequence_tool.py +14 -23
  81. quantalogic/tools/sql_query_tool.py +17 -19
  82. quantalogic/tools/tool.py +39 -15
  83. quantalogic/tools/unified_diff_tool.py +1 -1
  84. quantalogic/tools/utilities/csv_processor_tool.py +234 -0
  85. quantalogic/tools/utilities/download_file_tool.py +179 -0
  86. quantalogic/tools/utilities/mermaid_validator_tool.py +661 -0
  87. quantalogic/tools/utils/__init__.py +1 -4
  88. quantalogic/tools/utils/create_sample_database.py +24 -38
  89. quantalogic/tools/utils/generate_database_report.py +74 -82
  90. quantalogic/tools/wikipedia_search_tool.py +17 -21
  91. quantalogic/utils/ask_user_validation.py +1 -1
  92. quantalogic/utils/async_utils.py +35 -0
  93. quantalogic/utils/check_version.py +3 -5
  94. quantalogic/utils/get_all_models.py +2 -1
  95. quantalogic/utils/git_ls.py +21 -7
  96. quantalogic/utils/lm_studio_model_info.py +9 -7
  97. quantalogic/utils/python_interpreter.py +113 -43
  98. quantalogic/utils/xml_utility.py +178 -0
  99. quantalogic/version_check.py +1 -1
  100. quantalogic/welcome_message.py +7 -7
  101. quantalogic/xml_parser.py +0 -1
  102. {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/METADATA +40 -1
  103. quantalogic-0.50.0.dist-info/RECORD +148 -0
  104. quantalogic-0.35.0.dist-info/RECORD +0 -102
  105. {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/LICENSE +0 -0
  106. {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/WHEEL +0 -0
  107. {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,181 @@
1
+ """Tool for cloning Bitbucket repositories using Repository Access Tokens.
2
+
3
+ Following Bitbucket's official documentation for Repository Access Tokens:
4
+ https://support.atlassian.com/bitbucket-cloud/docs/using-repository-access-tokens/
5
+ """
6
+
7
+ import os
8
+ import shutil
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ from git import Repo
13
+ from git.exc import GitCommandError
14
+ from loguru import logger
15
+ from pydantic import Field
16
+
17
+ from quantalogic.tools.tool import Tool, ToolArgument
18
+
19
+ # Base directory for all cloned repositories
20
+ BITBUCKET_REPOS_BASE_DIR = "/tmp/bitbucket_repos"
21
+
22
+ class BitbucketCloneTool(Tool):
23
+ """Tool for cloning Bitbucket repositories using Repository Access Tokens."""
24
+
25
+ name: str = "bitbucket_clone_tool"
26
+ description: str = (
27
+ "Clones a Bitbucket repository using Repository Access Token authentication. "
28
+ "Requires a token with 'Repository Read' and 'Repository Write' permissions."
29
+ )
30
+ need_validation: bool = False
31
+ access_token: Optional[str] = Field(
32
+ default=None,
33
+ description="Bitbucket Repository Access Token. Must have 'Repository Read' and 'Repository Write' permissions."
34
+ )
35
+
36
+ def __init__(self, access_token: Optional[str] = None, **data):
37
+ """Initialize the tool with a Bitbucket Repository Access Token."""
38
+ super().__init__(**data)
39
+ # Treat empty string as None
40
+ self.access_token = access_token if access_token and access_token.strip() else None
41
+ logger.info(f"BitbucketCloneTool initialized with access token: {'provided' if self.access_token else 'not provided'}")
42
+
43
+ # Add token validation logging
44
+ if self.access_token:
45
+ logger.info(f"Access token provided, length: {len(self.access_token)}")
46
+ logger.info(f"First 4 chars of token (masked): {self.access_token[:4]}***")
47
+ else:
48
+ logger.warning("No access token provided during initialization")
49
+
50
+ arguments: list = [
51
+ ToolArgument(
52
+ name="repo_url",
53
+ arg_type="string",
54
+ description="The URL of the Bitbucket repository to clone (HTTPS format)",
55
+ required=True,
56
+ example="https://bitbucket.org/workspace/repository.git",
57
+ ),
58
+ ToolArgument(
59
+ name="target_path",
60
+ arg_type="string",
61
+ description="The local path where the repository should be cloned",
62
+ required=True,
63
+ example="/tmp/bitbucket_repos/repo_name",
64
+ ),
65
+ ToolArgument(
66
+ name="branch",
67
+ arg_type="string",
68
+ description="Specific branch to clone (defaults to main/master)",
69
+ required=False,
70
+ default="main",
71
+ ),
72
+ ]
73
+
74
+ def _prepare_directory(self, path: str) -> None:
75
+ """Prepare the target directory for cloning."""
76
+ path = os.path.abspath(path)
77
+ if not path.startswith(BITBUCKET_REPOS_BASE_DIR):
78
+ raise ValueError(f"Target path must be within {BITBUCKET_REPOS_BASE_DIR}")
79
+
80
+ os.makedirs(BITBUCKET_REPOS_BASE_DIR, exist_ok=True)
81
+ if os.path.exists(path):
82
+ shutil.rmtree(path)
83
+ os.makedirs(path)
84
+ logger.debug(f"Prepared directory: {path}")
85
+
86
+ def _get_clone_url(self, repo_url: str) -> str:
87
+ """Get the proper clone URL with Repository Access Token authentication.
88
+
89
+ Following Bitbucket's official format:
90
+ https://x-token-auth:{repository_access_token}@bitbucket.org/{workspace}/{repository}.git
91
+ """
92
+ # Log token state at URL construction
93
+ logger.debug(f"Token state in _get_clone_url: {'present' if self.access_token else 'missing'}")
94
+ if self.access_token:
95
+ logger.debug(f"Token length: {len(self.access_token)}")
96
+
97
+ logger.debug(f"Original URL: {repo_url}")
98
+
99
+ # Remove /src/branch/ if present (from web URLs)
100
+ if '/src/' in repo_url:
101
+ repo_url = repo_url.split('/src/')[0]
102
+ logger.debug(f"URL after removing /src/: {repo_url}")
103
+
104
+ # Ensure URL ends with .git
105
+ if not repo_url.endswith('.git'):
106
+ repo_url = f"{repo_url}.git"
107
+ logger.debug(f"URL after adding .git: {repo_url}")
108
+
109
+ # Add authentication using the official Bitbucket format
110
+ if self.access_token and self.access_token.strip():
111
+ # First, extract the workspace and repository from the URL
112
+ parts = repo_url.split('bitbucket.org/')
113
+ if len(parts) != 2:
114
+ raise ValueError("Invalid Bitbucket URL format")
115
+
116
+ workspace_repo = parts[1].rstrip('.git')
117
+ # Construct the URL exactly as per Bitbucket docs
118
+ clone_url = f"https://x-token-auth:{self.access_token}@bitbucket.org/{workspace_repo}.git"
119
+ logger.debug("Added Repository Access Token authentication to clone URL")
120
+ logger.debug(f"Final URL format (token masked): {clone_url.replace(self.access_token, '***')}")
121
+ return clone_url
122
+
123
+ logger.debug("No token provided, using public URL")
124
+ return repo_url
125
+
126
+ def execute(self, **kwargs) -> str:
127
+ """Execute the repository cloning operation using Repository Access Token."""
128
+ # Log token state at execution start
129
+ logger.debug("=== Token Tracing ===")
130
+ logger.debug(f"Token present at execute start: {bool(self.access_token)}")
131
+ if self.access_token:
132
+ logger.debug(f"Token length at execute: {len(self.access_token)}")
133
+ logger.debug(f"Token first 4 chars (masked): {self.access_token[:4]}***")
134
+ else:
135
+ logger.warning("No access token available during execution")
136
+ logger.debug("==================")
137
+
138
+ repo_url = kwargs.get("repo_url")
139
+ target_path = kwargs.get("target_path")
140
+ branch = kwargs.get("branch", "main")
141
+
142
+ if not repo_url or not target_path:
143
+ raise ValueError("Both repo_url and target_path are required")
144
+
145
+ try:
146
+ logger.info(f"Starting clone of {repo_url} to {target_path}")
147
+ self._prepare_directory(target_path)
148
+
149
+ clone_url = self._get_clone_url(repo_url)
150
+
151
+ # Log environment before clone
152
+ logger.debug("=== Clone Environment ===")
153
+ logger.debug(f"Branch: {branch}")
154
+ logger.debug(f"Target path: {target_path}")
155
+ logger.debug("======================")
156
+
157
+ # Clone with authentication disabled to prevent prompts
158
+ repo = Repo.clone_from(
159
+ clone_url,
160
+ target_path,
161
+ branch=branch,
162
+ env={"GIT_TERMINAL_PROMPT": "0"}
163
+ )
164
+
165
+ logger.info(f"Successfully cloned repository to {target_path}")
166
+ return f"Repository successfully cloned to {target_path}"
167
+
168
+ except GitCommandError as e:
169
+ error_msg = str(e)
170
+ if self.access_token:
171
+ error_msg = error_msg.replace(self.access_token, '***')
172
+ logger.error(f"Git command error: {error_msg}")
173
+ raise GitCommandError("git clone", error_msg)
174
+
175
+ except Exception as e:
176
+ logger.error(f"Error during repository cloning: {str(e)}")
177
+ raise
178
+
179
+ if __name__ == "__main__":
180
+ tool = BitbucketCloneTool(access_token="your_access_token_here")
181
+ print(tool.to_markdown())
@@ -0,0 +1,326 @@
1
+ """Tool for performing Bitbucket operations including cloning, branch management, and commits.
2
+
3
+ This tool provides a comprehensive interface for Bitbucket operations, supporting both
4
+ public and private repositories through Repository Access Token authentication.
5
+ """
6
+
7
+ import os
8
+ import re
9
+ import shutil
10
+ from pathlib import Path
11
+ from typing import ClassVar, Dict, List, Optional
12
+ from urllib.parse import urlparse
13
+
14
+ from git import Repo
15
+ from git.exc import GitCommandError
16
+ from loguru import logger
17
+ from pydantic import Field, validator
18
+
19
+ from quantalogic.tools.tool import Tool, ToolArgument
20
+
21
+ # Base directory for all Bitbucket repositories
22
+ BITBUCKET_REPOS_BASE_DIR = "/tmp/bitbucket_repos"
23
+
24
+ class BitbucketOperationsTool(Tool):
25
+ """Tool for comprehensive Bitbucket operations.
26
+
27
+ Provides functionality for:
28
+ - Cloning repositories
29
+ - Creating and managing branches
30
+ - Making commits
31
+ - Pushing and pulling changes
32
+ - Managing repository access
33
+
34
+ All operations use Bitbucket's Repository Access Token for authentication.
35
+ """
36
+
37
+ name: str = "bitbucket_operations_tool"
38
+ description: str = (
39
+ "Comprehensive tool for Bitbucket operations including cloning, branch management, "
40
+ "commits, and repository access. Uses Repository Access Token authentication."
41
+ )
42
+ need_validation: bool = False
43
+ access_token: Optional[str] = Field(
44
+ default=None,
45
+ description="Bitbucket Repository Access Token with 'Repository Read' and 'Repository Write' permissions"
46
+ )
47
+
48
+ def __init__(self, access_token: Optional[str] = None, **data):
49
+ """Initialize the tool with a Bitbucket Repository Access Token.
50
+
51
+ Args:
52
+ access_token: Bitbucket Repository Access Token
53
+ **data: Additional tool configuration data
54
+ """
55
+ super().__init__(**data)
56
+ self.access_token = access_token if access_token and access_token.strip() else None
57
+ logger.info(f"BitbucketOperationsTool initialized with token: {'provided' if self.access_token else 'not provided'}")
58
+
59
+ arguments: list = [
60
+ ToolArgument(
61
+ name="operation",
62
+ arg_type="string",
63
+ description=(
64
+ "The Git operation to perform. One of: clone, create_branch, checkout, "
65
+ "commit, push, pull, status"
66
+ ),
67
+ required=True,
68
+ example="clone",
69
+ ),
70
+ ToolArgument(
71
+ name="repo_url",
72
+ arg_type="string",
73
+ description="The Bitbucket repository URL (HTTPS format)",
74
+ required=False,
75
+ example="https://bitbucket.org/workspace/repository.git",
76
+ ),
77
+ ToolArgument(
78
+ name="repo_path",
79
+ arg_type="string",
80
+ description="Local repository path (must be within /tmp/bitbucket_repos)",
81
+ required=True,
82
+ example="/tmp/bitbucket_repos/repo_name",
83
+ ),
84
+ ToolArgument(
85
+ name="branch_name",
86
+ arg_type="string",
87
+ description="Branch name for operations that require it",
88
+ required=False,
89
+ example="feature/new-feature",
90
+ ),
91
+ ToolArgument(
92
+ name="commit_message",
93
+ arg_type="string",
94
+ description="Commit message for commit operations",
95
+ required=False,
96
+ example="Add new feature implementation",
97
+ ),
98
+ ToolArgument(
99
+ name="files_to_commit",
100
+ arg_type="string",
101
+ description="Comma-separated list of files to commit",
102
+ required=False,
103
+ example="file1.py,file2.py",
104
+ ),
105
+ ]
106
+
107
+ def _prepare_directory(self, path: str) -> None:
108
+ """Prepare the target directory for repository operations."""
109
+ path = os.path.abspath(path)
110
+ if not path.startswith(BITBUCKET_REPOS_BASE_DIR):
111
+ raise ValueError(f"Target path must be within {BITBUCKET_REPOS_BASE_DIR}")
112
+
113
+ os.makedirs(BITBUCKET_REPOS_BASE_DIR, exist_ok=True)
114
+ if os.path.exists(path):
115
+ shutil.rmtree(path)
116
+ os.makedirs(path)
117
+ logger.debug(f"Prepared directory: {path}")
118
+
119
+ def _get_clone_url(self, repo_url: str) -> str:
120
+ """Get the proper clone URL with Repository Access Token authentication."""
121
+ if not self.access_token:
122
+ return repo_url
123
+
124
+ # Remove /src/branch/ if present (from web URLs)
125
+ if '/src/' in repo_url:
126
+ repo_url = repo_url.split('/src/')[0]
127
+
128
+ # Remove .git if present
129
+ repo_url = repo_url.rstrip('.git')
130
+
131
+ # Construct authenticated URL
132
+ return f"https://x-token-auth:{self.access_token}@bitbucket.org/{repo_url.split('bitbucket.org/')[-1]}.git"
133
+
134
+ def _clone_repository(self, repo_url: str, target_path: str, branch: str = "main") -> Repo:
135
+ """Clone a Bitbucket repository.
136
+
137
+ Args:
138
+ repo_url: Repository URL
139
+ target_path: Local path for cloning
140
+ branch: Branch to clone (default: main)
141
+
142
+ Returns:
143
+ git.Repo: Cloned repository object
144
+ """
145
+ try:
146
+ self._prepare_directory(target_path)
147
+ clone_url = self._get_clone_url(repo_url)
148
+
149
+ logger.info(f"Cloning repository to {target_path}")
150
+ repo = Repo.clone_from(
151
+ clone_url,
152
+ target_path,
153
+ branch=branch,
154
+ )
155
+ logger.info("Repository cloned successfully")
156
+ return repo
157
+
158
+ except GitCommandError as e:
159
+ logger.error(f"Failed to clone repository: {str(e)}")
160
+ raise
161
+
162
+ def _create_branch(self, repo: Repo, branch_name: str) -> None:
163
+ """Create and checkout a new branch.
164
+
165
+ Args:
166
+ repo: Repository object
167
+ branch_name: Name of the branch to create
168
+ """
169
+ try:
170
+ current = repo.active_branch
171
+ new_branch = repo.create_head(branch_name)
172
+ new_branch.checkout()
173
+ logger.info(f"Created and checked out branch: {branch_name}")
174
+
175
+ except GitCommandError as e:
176
+ logger.error(f"Failed to create branch: {str(e)}")
177
+ raise
178
+
179
+ def _commit_changes(self, repo: Repo, message: str, files: Optional[List[str]] = None) -> None:
180
+ """Commit changes to the repository.
181
+
182
+ Args:
183
+ repo: Repository object
184
+ message: Commit message
185
+ files: Optional list of specific files to commit
186
+ """
187
+ try:
188
+ if files:
189
+ repo.index.add(files)
190
+ else:
191
+ repo.git.add(A=True)
192
+
193
+ repo.index.commit(message)
194
+ logger.info(f"Changes committed with message: {message}")
195
+
196
+ except GitCommandError as e:
197
+ logger.error(f"Failed to commit changes: {str(e)}")
198
+ raise
199
+
200
+ def _push_changes(self, repo: Repo, branch_name: Optional[str] = None) -> None:
201
+ """Push changes to remote repository.
202
+
203
+ Args:
204
+ repo: Repository object
205
+ branch_name: Optional branch name to push
206
+ """
207
+ try:
208
+ if branch_name:
209
+ repo.git.push('origin', branch_name)
210
+ else:
211
+ repo.git.push()
212
+ logger.info("Changes pushed to remote")
213
+
214
+ except GitCommandError as e:
215
+ logger.error(f"Failed to push changes: {str(e)}")
216
+ raise
217
+
218
+ def _pull_changes(self, repo: Repo) -> None:
219
+ """Pull latest changes from remote repository.
220
+
221
+ Args:
222
+ repo: Repository object
223
+ """
224
+ try:
225
+ repo.git.pull()
226
+ logger.info("Latest changes pulled from remote")
227
+
228
+ except GitCommandError as e:
229
+ logger.error(f"Failed to pull changes: {str(e)}")
230
+ raise
231
+
232
+ def execute(self, **kwargs) -> Dict:
233
+ """Execute the requested Bitbucket operation.
234
+
235
+ Args:
236
+ **kwargs: Operation-specific arguments
237
+
238
+ Returns:
239
+ Dict: Operation result status and details
240
+ """
241
+ operation = kwargs.get('operation')
242
+ repo_path = kwargs.get('repo_path')
243
+
244
+ try:
245
+ if operation == 'clone':
246
+ repo_url = kwargs.get('repo_url')
247
+ if not repo_url:
248
+ raise ValueError("repo_url is required for clone operation")
249
+
250
+ repo = self._clone_repository(
251
+ repo_url,
252
+ repo_path,
253
+ kwargs.get('branch_name', 'main')
254
+ )
255
+ return {"status": "success", "message": f"Repository cloned to {repo_path}"}
256
+
257
+ # For other operations, we need an existing repository
258
+ if not os.path.exists(repo_path):
259
+ raise ValueError(f"Repository not found at {repo_path}")
260
+
261
+ repo = Repo(repo_path)
262
+
263
+ if operation == 'create_branch':
264
+ branch_name = kwargs.get('branch_name')
265
+ if not branch_name:
266
+ raise ValueError("branch_name is required for create_branch operation")
267
+
268
+ self._create_branch(repo, branch_name)
269
+ return {"status": "success", "message": f"Created branch: {branch_name}"}
270
+
271
+ elif operation == 'commit':
272
+ message = kwargs.get('commit_message')
273
+ if not message:
274
+ raise ValueError("commit_message is required for commit operation")
275
+
276
+ files = kwargs.get('files_to_commit', '').split(',') if kwargs.get('files_to_commit') else None
277
+ self._commit_changes(repo, message, files)
278
+ return {"status": "success", "message": f"Changes committed: {message}"}
279
+
280
+ elif operation == 'push':
281
+ self._push_changes(repo, kwargs.get('branch_name'))
282
+ return {"status": "success", "message": "Changes pushed to remote"}
283
+
284
+ elif operation == 'pull':
285
+ self._pull_changes(repo)
286
+ return {"status": "success", "message": "Latest changes pulled from remote"}
287
+
288
+ elif operation == 'checkout':
289
+ branch_name = kwargs.get('branch_name')
290
+ if not branch_name:
291
+ raise ValueError("branch_name is required for checkout operation")
292
+
293
+ repo.git.checkout(branch_name)
294
+ return {"status": "success", "message": f"Checked out branch: {branch_name}"}
295
+
296
+ elif operation == 'status':
297
+ status = repo.git.status()
298
+ return {"status": "success", "message": status}
299
+
300
+ else:
301
+ raise ValueError(f"Unsupported operation: {operation}")
302
+
303
+ except Exception as e:
304
+ logger.error(f"Operation failed: {str(e)}")
305
+ return {"status": "error", "message": str(e)}
306
+
307
+
308
+ if __name__ == "__main__":
309
+ # Example usage
310
+ tool = BitbucketOperationsTool(access_token="your_access_token_here")
311
+
312
+ # Clone a repository
313
+ result = tool.execute(
314
+ operation="clone",
315
+ repo_url="https://bitbucket.org/workspace/repository.git",
316
+ repo_path="/tmp/bitbucket_repos/repository"
317
+ )
318
+ print(result)
319
+
320
+ # Create a new branch
321
+ result = tool.execute(
322
+ operation="create_branch",
323
+ repo_path="/tmp/bitbucket_repos/repository",
324
+ branch_name="feature/new-feature"
325
+ )
326
+ print(result)
@@ -0,0 +1,189 @@
1
+ """Tool for cloning Git repositories with support for both public and private repositories."""
2
+
3
+ import os
4
+ import shutil
5
+ from pathlib import Path
6
+
7
+ import requests
8
+ from git import Repo
9
+ from git.exc import GitCommandError
10
+ from loguru import logger
11
+ from pydantic import Field
12
+
13
+ from quantalogic.tools.tool import Tool, ToolArgument
14
+
15
+ # Base directory for all cloned repositories
16
+ GITHUB_REPOS_BASE_DIR = "/tmp/github_repos"
17
+
18
+ class CloneRepoTool(Tool):
19
+ """Tool for cloning Git repositories."""
20
+
21
+ name: str = "clone_repo_tool"
22
+ description: str = (
23
+ "Clones a Git repository (public or private) to a specified location. "
24
+ "Automatically handles authentication for private repositories using the provided token."
25
+ )
26
+ need_validation: bool = False
27
+ auth_token: str = Field(default=None, description="GitHub authentication token for private repositories")
28
+
29
+ def __init__(self, auth_token: str = None, **data):
30
+ """Initialize the tool with an optional auth token.
31
+
32
+ Args:
33
+ auth_token: GitHub authentication token for private repositories
34
+ **data: Additional tool configuration data
35
+ """
36
+ super().__init__(**data)
37
+ self.auth_token = auth_token
38
+
39
+ arguments: list = [
40
+ ToolArgument(
41
+ name="repo_url",
42
+ arg_type="string",
43
+ description="The URL of the Git repository to clone (HTTPS format)",
44
+ required=True,
45
+ example="https://github.com/username/repo.git",
46
+ ),
47
+ ToolArgument(
48
+ name="target_path",
49
+ arg_type="string",
50
+ description="The local path where the repository should be cloned (must be within /tmp/github_repos)",
51
+ required=True,
52
+ example="/tmp/github_repos/repo_name",
53
+ ),
54
+ ToolArgument(
55
+ name="branch",
56
+ arg_type="string",
57
+ description="Specific branch to clone (defaults to main/master)",
58
+ required=False,
59
+ default="main",
60
+ ),
61
+ ]
62
+
63
+ def is_private_repo(self, repo_url: str) -> bool:
64
+ """Check if a GitHub repository is private.
65
+
66
+ Args:
67
+ repo_url: Repository URL in format https://github.com/username/repo.git
68
+
69
+ Returns:
70
+ bool: True if repository is private, False otherwise
71
+ """
72
+ try:
73
+ # Extract owner and repo name from URL
74
+ parts = repo_url.rstrip(".git").split("/")
75
+ owner, repo = parts[-2], parts[-1]
76
+
77
+ # Try to access repo info without token first
78
+ response = requests.get(f"https://api.github.com/repos/{owner}/{repo}")
79
+
80
+ if response.status_code == 404 and self.auth_token:
81
+ # Try again with token
82
+ headers = {"Authorization": f"token {self.auth_token}"}
83
+ response = requests.get(
84
+ f"https://api.github.com/repos/{owner}/{repo}",
85
+ headers=headers
86
+ )
87
+ return response.status_code == 200 # If accessible with token, it's private
88
+
89
+ return False # Repository is public
90
+
91
+ except Exception as e:
92
+ logger.warning(f"Error checking repository visibility: {str(e)}")
93
+ return True # Assume private if can't determine
94
+
95
+ def _prepare_target_directory(self, target_path: str) -> None:
96
+ """Prepare the target directory for cloning.
97
+
98
+ Ensures the target directory is within GITHUB_REPOS_BASE_DIR and prepares it for cloning.
99
+
100
+ Args:
101
+ target_path: Path where the repository will be cloned
102
+
103
+ Raises:
104
+ ValueError: If the target path is not within GITHUB_REPOS_BASE_DIR
105
+ """
106
+ # Ensure base directory exists
107
+ os.makedirs(GITHUB_REPOS_BASE_DIR, exist_ok=True)
108
+
109
+ # Convert to absolute path and ensure it's within GITHUB_REPOS_BASE_DIR
110
+ abs_target = os.path.abspath(target_path)
111
+ if not abs_target.startswith(GITHUB_REPOS_BASE_DIR):
112
+ raise ValueError(f"Target directory must be within {GITHUB_REPOS_BASE_DIR}")
113
+
114
+ if os.path.exists(target_path):
115
+ logger.info(f"Target directory exists, removing: {target_path}")
116
+ try:
117
+ # Remove directory and all its contents
118
+ shutil.rmtree(target_path)
119
+ except Exception as e:
120
+ logger.error(f"Error removing existing directory: {str(e)}")
121
+ raise ValueError(f"Failed to remove existing directory: {str(e)}")
122
+
123
+ # Create new empty directory
124
+ os.makedirs(target_path, exist_ok=True)
125
+ logger.info(f"Created clean target directory: {target_path}")
126
+
127
+ def execute(self, repo_url: str, target_path: str, branch: str = "main") -> str:
128
+ """Clones a Git repository to the specified path within GITHUB_REPOS_BASE_DIR.
129
+
130
+ Args:
131
+ repo_url: URL of the Git repository
132
+ target_path: Local path where to clone the repository (must be within GITHUB_REPOS_BASE_DIR)
133
+ branch: Branch to clone (defaults to main)
134
+
135
+ Returns:
136
+ str: Path where the repository was cloned
137
+
138
+ Raises:
139
+ GitCommandError: If there's an error during cloning
140
+ ValueError: If the parameters are invalid or target_path is outside GITHUB_REPOS_BASE_DIR
141
+ """
142
+ try:
143
+ # Ensure target_path is within GITHUB_REPOS_BASE_DIR
144
+ if not os.path.abspath(target_path).startswith(GITHUB_REPOS_BASE_DIR):
145
+ target_path = os.path.join(GITHUB_REPOS_BASE_DIR, os.path.basename(target_path))
146
+ logger.info(f"Adjusting target path to: {target_path}")
147
+
148
+ # Prepare target directory (remove if exists and create new)
149
+ self._prepare_target_directory(target_path)
150
+
151
+ # Check if repo is private and token is needed
152
+ is_private = self.is_private_repo(repo_url)
153
+
154
+ if is_private and not self.auth_token:
155
+ raise ValueError("Authentication token required for private repository")
156
+
157
+ # Prepare the clone URL with auth token if needed
158
+ clone_url = repo_url
159
+ if is_private and self.auth_token:
160
+ clone_url = repo_url.replace("https://", f"https://{self.auth_token}@")
161
+
162
+ logger.info(f"Cloning repository to {target_path}")
163
+
164
+ # Clone the repository
165
+ repo = Repo.clone_from(
166
+ url=clone_url,
167
+ to_path=target_path,
168
+ branch=branch,
169
+ )
170
+
171
+ logger.info(f"Successfully cloned repository to {target_path}")
172
+ return f"Repository successfully cloned to: {target_path}"
173
+
174
+ except GitCommandError as e:
175
+ error_msg = str(e)
176
+ # Remove sensitive information from error message if present
177
+ if self.auth_token:
178
+ error_msg = error_msg.replace(self.auth_token, "***")
179
+ logger.error(f"Failed to clone repository: {error_msg}")
180
+ raise GitCommandError(f"Failed to clone repository: {error_msg}", e.status)
181
+
182
+ except Exception as e:
183
+ logger.error(f"An error occurred while cloning the repository: {str(e)}")
184
+ raise ValueError(f"An error occurred while cloning the repository: {str(e)}")
185
+
186
+
187
+ if __name__ == "__main__":
188
+ tool = CloneRepoTool(auth_token="your_token_here")
189
+ print(tool.to_markdown())