quantalogic 0.35.0__py3-none-any.whl → 0.40.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.
- quantalogic/__init__.py +0 -4
- quantalogic/agent.py +603 -363
- quantalogic/agent_config.py +233 -46
- quantalogic/agent_factory.py +34 -22
- quantalogic/coding_agent.py +16 -14
- quantalogic/config.py +2 -1
- quantalogic/console_print_events.py +4 -8
- quantalogic/console_print_token.py +2 -2
- quantalogic/docs_cli.py +15 -10
- quantalogic/event_emitter.py +258 -83
- quantalogic/flow/__init__.py +23 -0
- quantalogic/flow/flow.py +595 -0
- quantalogic/flow/flow_extractor.py +672 -0
- quantalogic/flow/flow_generator.py +89 -0
- quantalogic/flow/flow_manager.py +407 -0
- quantalogic/flow/flow_manager_schema.py +169 -0
- quantalogic/flow/flow_yaml.md +419 -0
- quantalogic/generative_model.py +109 -77
- quantalogic/get_model_info.py +5 -5
- quantalogic/interactive_text_editor.py +100 -73
- quantalogic/main.py +17 -21
- quantalogic/model_info_list.py +3 -3
- quantalogic/model_info_litellm.py +14 -14
- quantalogic/prompts.py +2 -1
- quantalogic/{llm.py → quantlitellm.py} +29 -39
- quantalogic/search_agent.py +4 -4
- quantalogic/server/models.py +4 -1
- quantalogic/task_file_reader.py +5 -5
- quantalogic/task_runner.py +20 -20
- quantalogic/tool_manager.py +10 -21
- quantalogic/tools/__init__.py +98 -68
- quantalogic/tools/composio/composio.py +416 -0
- quantalogic/tools/{generate_database_report_tool.py → database/generate_database_report_tool.py} +4 -9
- quantalogic/tools/database/sql_query_tool_advanced.py +261 -0
- quantalogic/tools/document_tools/markdown_to_docx_tool.py +620 -0
- quantalogic/tools/document_tools/markdown_to_epub_tool.py +438 -0
- quantalogic/tools/document_tools/markdown_to_html_tool.py +362 -0
- quantalogic/tools/document_tools/markdown_to_ipynb_tool.py +319 -0
- quantalogic/tools/document_tools/markdown_to_latex_tool.py +420 -0
- quantalogic/tools/document_tools/markdown_to_pdf_tool.py +623 -0
- quantalogic/tools/document_tools/markdown_to_pptx_tool.py +319 -0
- quantalogic/tools/duckduckgo_search_tool.py +2 -4
- quantalogic/tools/finance/alpha_vantage_tool.py +440 -0
- quantalogic/tools/finance/ccxt_tool.py +373 -0
- quantalogic/tools/finance/finance_llm_tool.py +387 -0
- quantalogic/tools/finance/google_finance.py +192 -0
- quantalogic/tools/finance/market_intelligence_tool.py +520 -0
- quantalogic/tools/finance/technical_analysis_tool.py +491 -0
- quantalogic/tools/finance/tradingview_tool.py +336 -0
- quantalogic/tools/finance/yahoo_finance.py +236 -0
- quantalogic/tools/git/bitbucket_clone_repo_tool.py +181 -0
- quantalogic/tools/git/bitbucket_operations_tool.py +326 -0
- quantalogic/tools/git/clone_repo_tool.py +189 -0
- quantalogic/tools/git/git_operations_tool.py +532 -0
- quantalogic/tools/google_packages/google_news_tool.py +480 -0
- quantalogic/tools/grep_app_tool.py +123 -186
- quantalogic/tools/{dalle_e.py → image_generation/dalle_e.py} +37 -27
- quantalogic/tools/jinja_tool.py +6 -10
- quantalogic/tools/language_handlers/__init__.py +22 -9
- quantalogic/tools/list_directory_tool.py +131 -42
- quantalogic/tools/llm_tool.py +45 -15
- quantalogic/tools/llm_vision_tool.py +59 -7
- quantalogic/tools/markitdown_tool.py +17 -5
- quantalogic/tools/nasa_packages/models.py +47 -0
- quantalogic/tools/nasa_packages/nasa_apod_tool.py +232 -0
- quantalogic/tools/nasa_packages/nasa_neows_tool.py +147 -0
- quantalogic/tools/nasa_packages/services.py +82 -0
- quantalogic/tools/presentation_tools/presentation_llm_tool.py +396 -0
- quantalogic/tools/product_hunt/product_hunt_tool.py +258 -0
- quantalogic/tools/product_hunt/services.py +63 -0
- quantalogic/tools/rag_tool/__init__.py +48 -0
- quantalogic/tools/rag_tool/document_metadata.py +15 -0
- quantalogic/tools/rag_tool/query_response.py +20 -0
- quantalogic/tools/rag_tool/rag_tool.py +566 -0
- quantalogic/tools/rag_tool/rag_tool_beta.py +264 -0
- quantalogic/tools/read_html_tool.py +24 -38
- quantalogic/tools/replace_in_file_tool.py +10 -10
- quantalogic/tools/safe_python_interpreter_tool.py +10 -24
- quantalogic/tools/search_definition_names.py +2 -2
- quantalogic/tools/sequence_tool.py +14 -23
- quantalogic/tools/sql_query_tool.py +17 -19
- quantalogic/tools/tool.py +39 -15
- quantalogic/tools/unified_diff_tool.py +1 -1
- quantalogic/tools/utilities/csv_processor_tool.py +234 -0
- quantalogic/tools/utilities/download_file_tool.py +179 -0
- quantalogic/tools/utilities/mermaid_validator_tool.py +661 -0
- quantalogic/tools/utils/__init__.py +1 -4
- quantalogic/tools/utils/create_sample_database.py +24 -38
- quantalogic/tools/utils/generate_database_report.py +74 -82
- quantalogic/tools/wikipedia_search_tool.py +17 -21
- quantalogic/utils/ask_user_validation.py +1 -1
- quantalogic/utils/async_utils.py +35 -0
- quantalogic/utils/check_version.py +3 -5
- quantalogic/utils/get_all_models.py +2 -1
- quantalogic/utils/git_ls.py +21 -7
- quantalogic/utils/lm_studio_model_info.py +9 -7
- quantalogic/utils/python_interpreter.py +113 -43
- quantalogic/utils/xml_utility.py +178 -0
- quantalogic/version_check.py +1 -1
- quantalogic/welcome_message.py +7 -7
- quantalogic/xml_parser.py +0 -1
- {quantalogic-0.35.0.dist-info → quantalogic-0.40.0.dist-info}/METADATA +41 -1
- quantalogic-0.40.0.dist-info/RECORD +148 -0
- quantalogic-0.35.0.dist-info/RECORD +0 -102
- {quantalogic-0.35.0.dist-info → quantalogic-0.40.0.dist-info}/LICENSE +0 -0
- {quantalogic-0.35.0.dist-info → quantalogic-0.40.0.dist-info}/WHEEL +0 -0
- {quantalogic-0.35.0.dist-info → quantalogic-0.40.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())
|