mcli-framework 7.1.3__py3-none-any.whl → 7.3.1__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.
Potentially problematic release.
This version of mcli-framework might be problematic. Click here for more details.
- mcli/__init__.py +160 -0
- mcli/__main__.py +14 -0
- mcli/app/__init__.py +23 -0
- mcli/app/main.py +10 -0
- mcli/app/model/__init__.py +0 -0
- mcli/app/video/__init__.py +5 -0
- mcli/chat/__init__.py +34 -0
- mcli/lib/__init__.py +0 -0
- mcli/lib/api/__init__.py +0 -0
- mcli/lib/auth/__init__.py +1 -0
- mcli/lib/config/__init__.py +1 -0
- mcli/lib/custom_commands.py +424 -0
- mcli/lib/erd/__init__.py +25 -0
- mcli/lib/files/__init__.py +0 -0
- mcli/lib/fs/__init__.py +1 -0
- mcli/lib/logger/__init__.py +3 -0
- mcli/lib/paths.py +12 -0
- mcli/lib/performance/__init__.py +17 -0
- mcli/lib/pickles/__init__.py +1 -0
- mcli/lib/shell/__init__.py +0 -0
- mcli/lib/toml/__init__.py +1 -0
- mcli/lib/watcher/__init__.py +0 -0
- mcli/ml/__init__.py +16 -0
- mcli/ml/api/__init__.py +30 -0
- mcli/ml/api/routers/__init__.py +27 -0
- mcli/ml/api/schemas.py +2 -2
- mcli/ml/auth/__init__.py +45 -0
- mcli/ml/auth/models.py +2 -2
- mcli/ml/backtesting/__init__.py +39 -0
- mcli/ml/cli/__init__.py +5 -0
- mcli/ml/cli/main.py +1 -1
- mcli/ml/config/__init__.py +33 -0
- mcli/ml/configs/__init__.py +16 -0
- mcli/ml/dashboard/__init__.py +12 -0
- mcli/ml/dashboard/app.py +13 -13
- mcli/ml/dashboard/app_integrated.py +1309 -148
- mcli/ml/dashboard/app_supabase.py +46 -21
- mcli/ml/dashboard/app_training.py +14 -14
- mcli/ml/dashboard/components/__init__.py +7 -0
- mcli/ml/dashboard/components/charts.py +258 -0
- mcli/ml/dashboard/components/metrics.py +125 -0
- mcli/ml/dashboard/components/tables.py +228 -0
- mcli/ml/dashboard/pages/__init__.py +6 -0
- mcli/ml/dashboard/pages/cicd.py +382 -0
- mcli/ml/dashboard/pages/predictions_enhanced.py +834 -0
- mcli/ml/dashboard/pages/scrapers_and_logs.py +1060 -0
- mcli/ml/dashboard/pages/test_portfolio.py +373 -0
- mcli/ml/dashboard/pages/trading.py +714 -0
- mcli/ml/dashboard/pages/workflows.py +533 -0
- mcli/ml/dashboard/utils.py +154 -0
- mcli/ml/data_ingestion/__init__.py +39 -0
- mcli/ml/database/__init__.py +47 -0
- mcli/ml/experimentation/__init__.py +29 -0
- mcli/ml/features/__init__.py +39 -0
- mcli/ml/mlops/__init__.py +33 -0
- mcli/ml/models/__init__.py +94 -0
- mcli/ml/monitoring/__init__.py +25 -0
- mcli/ml/optimization/__init__.py +27 -0
- mcli/ml/predictions/__init__.py +5 -0
- mcli/ml/preprocessing/__init__.py +28 -0
- mcli/ml/scripts/__init__.py +1 -0
- mcli/ml/trading/__init__.py +60 -0
- mcli/ml/trading/alpaca_client.py +353 -0
- mcli/ml/trading/migrations.py +164 -0
- mcli/ml/trading/models.py +418 -0
- mcli/ml/trading/paper_trading.py +326 -0
- mcli/ml/trading/risk_management.py +370 -0
- mcli/ml/trading/trading_service.py +480 -0
- mcli/ml/training/__init__.py +10 -0
- mcli/ml/training/train_model.py +569 -0
- mcli/mygroup/__init__.py +3 -0
- mcli/public/__init__.py +1 -0
- mcli/public/commands/__init__.py +2 -0
- mcli/self/__init__.py +3 -0
- mcli/self/self_cmd.py +579 -91
- mcli/workflow/__init__.py +0 -0
- mcli/workflow/daemon/__init__.py +15 -0
- mcli/workflow/daemon/daemon.py +21 -3
- mcli/workflow/dashboard/__init__.py +5 -0
- mcli/workflow/docker/__init__.py +0 -0
- mcli/workflow/file/__init__.py +0 -0
- mcli/workflow/gcloud/__init__.py +1 -0
- mcli/workflow/git_commit/__init__.py +0 -0
- mcli/workflow/interview/__init__.py +0 -0
- mcli/workflow/politician_trading/__init__.py +4 -0
- mcli/workflow/politician_trading/data_sources.py +259 -1
- mcli/workflow/politician_trading/models.py +159 -1
- mcli/workflow/politician_trading/scrapers_corporate_registry.py +846 -0
- mcli/workflow/politician_trading/scrapers_free_sources.py +516 -0
- mcli/workflow/politician_trading/scrapers_third_party.py +391 -0
- mcli/workflow/politician_trading/seed_database.py +539 -0
- mcli/workflow/registry/__init__.py +0 -0
- mcli/workflow/repo/__init__.py +0 -0
- mcli/workflow/scheduler/__init__.py +25 -0
- mcli/workflow/search/__init__.py +0 -0
- mcli/workflow/sync/__init__.py +5 -0
- mcli/workflow/videos/__init__.py +1 -0
- mcli/workflow/wakatime/__init__.py +80 -0
- mcli/workflow/workflow.py +8 -27
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/METADATA +3 -1
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/RECORD +105 -29
- mcli/workflow/daemon/api_daemon.py +0 -800
- mcli/workflow/daemon/commands.py +0 -1196
- mcli/workflow/dashboard/dashboard_cmd.py +0 -120
- mcli/workflow/file/file.py +0 -100
- mcli/workflow/git_commit/commands.py +0 -430
- mcli/workflow/politician_trading/commands.py +0 -1939
- mcli/workflow/scheduler/commands.py +0 -493
- mcli/workflow/sync/sync_cmd.py +0 -437
- mcli/workflow/videos/videos.py +0 -242
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/WHEEL +0 -0
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/top_level.txt +0 -0
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
"""ML Dashboard commands for mcli."""
|
|
2
|
-
|
|
3
|
-
import subprocess
|
|
4
|
-
import sys
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
import click
|
|
8
|
-
|
|
9
|
-
from mcli.lib.logger.logger import get_logger
|
|
10
|
-
|
|
11
|
-
logger = get_logger(__name__)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@click.group(name="dashboard")
|
|
15
|
-
def dashboard():
|
|
16
|
-
"""ML monitoring dashboard commands."""
|
|
17
|
-
pass
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@dashboard.command()
|
|
21
|
-
@click.option("--port", "-p", default=8501, help="Port to run dashboard on")
|
|
22
|
-
@click.option("--host", "-h", default="localhost", help="Host to bind to")
|
|
23
|
-
@click.option("--debug", is_flag=True, help="Run in debug mode")
|
|
24
|
-
def launch(port, host, debug):
|
|
25
|
-
"""Launch the ML monitoring dashboard."""
|
|
26
|
-
|
|
27
|
-
click.echo(f"🚀 Starting ML Dashboard on http://{host}:{port}")
|
|
28
|
-
|
|
29
|
-
# Get the dashboard app path - use Supabase version to avoid joblib issues
|
|
30
|
-
dashboard_path = Path(__file__).parent.parent.parent / "ml" / "dashboard" / "app_supabase.py"
|
|
31
|
-
|
|
32
|
-
if not dashboard_path.exists():
|
|
33
|
-
click.echo("❌ Dashboard app not found!")
|
|
34
|
-
logger.error(f"Dashboard app not found at {dashboard_path}")
|
|
35
|
-
sys.exit(1)
|
|
36
|
-
|
|
37
|
-
# Build streamlit command
|
|
38
|
-
cmd = [
|
|
39
|
-
sys.executable,
|
|
40
|
-
"-m",
|
|
41
|
-
"streamlit",
|
|
42
|
-
"run",
|
|
43
|
-
str(dashboard_path),
|
|
44
|
-
"--server.port",
|
|
45
|
-
str(port),
|
|
46
|
-
"--server.address",
|
|
47
|
-
host,
|
|
48
|
-
"--browser.gatherUsageStats",
|
|
49
|
-
"false",
|
|
50
|
-
]
|
|
51
|
-
|
|
52
|
-
if debug:
|
|
53
|
-
cmd.extend(["--logger.level", "debug"])
|
|
54
|
-
|
|
55
|
-
click.echo("📊 Dashboard is starting...")
|
|
56
|
-
click.echo("Press Ctrl+C to stop")
|
|
57
|
-
|
|
58
|
-
try:
|
|
59
|
-
subprocess.run(cmd, check=True)
|
|
60
|
-
except KeyboardInterrupt:
|
|
61
|
-
click.echo("\n⏹️ Dashboard stopped")
|
|
62
|
-
except subprocess.CalledProcessError as e:
|
|
63
|
-
click.echo(f"❌ Failed to start dashboard: {e}")
|
|
64
|
-
logger.error(f"Dashboard failed to start: {e}")
|
|
65
|
-
sys.exit(1)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
@dashboard.command()
|
|
69
|
-
def info():
|
|
70
|
-
"""Show dashboard information and status."""
|
|
71
|
-
|
|
72
|
-
click.echo("📊 ML Dashboard Information")
|
|
73
|
-
click.echo("━" * 40)
|
|
74
|
-
|
|
75
|
-
# Check if dependencies are installed
|
|
76
|
-
try:
|
|
77
|
-
import plotly
|
|
78
|
-
import streamlit
|
|
79
|
-
|
|
80
|
-
click.echo("✅ Dashboard dependencies installed")
|
|
81
|
-
click.echo(f" Streamlit version: {streamlit.__version__}")
|
|
82
|
-
click.echo(f" Plotly version: {plotly.__version__}")
|
|
83
|
-
except ImportError as e:
|
|
84
|
-
click.echo(f"❌ Missing dependencies: {e}")
|
|
85
|
-
click.echo(" Run: uv sync --extra dashboard")
|
|
86
|
-
|
|
87
|
-
# Check database connection
|
|
88
|
-
try:
|
|
89
|
-
from mcli.ml.config import settings
|
|
90
|
-
|
|
91
|
-
click.echo(f"\n📁 Database URL: {settings.database.url}")
|
|
92
|
-
click.echo(f"📍 Redis URL: {settings.redis.url}")
|
|
93
|
-
except Exception as e:
|
|
94
|
-
click.echo(f"⚠️ Configuration not available: {e}")
|
|
95
|
-
|
|
96
|
-
click.echo("\n💡 Quick start:")
|
|
97
|
-
click.echo(" mcli workflow dashboard launch")
|
|
98
|
-
click.echo(" mcli workflow dashboard launch --port 8502 --host 0.0.0.0")
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
@dashboard.command()
|
|
102
|
-
@click.argument("action", type=click.Choice(["start", "stop", "restart"]))
|
|
103
|
-
def service(action):
|
|
104
|
-
"""Manage dashboard as a background service."""
|
|
105
|
-
|
|
106
|
-
if action == "start":
|
|
107
|
-
click.echo("🚀 Starting dashboard service...")
|
|
108
|
-
# Could implement systemd or pm2 integration here
|
|
109
|
-
click.echo("⚠️ Service mode not yet implemented")
|
|
110
|
-
click.echo(" Use 'mcli workflow dashboard launch' instead")
|
|
111
|
-
elif action == "stop":
|
|
112
|
-
click.echo("⏹️ Stopping dashboard service...")
|
|
113
|
-
click.echo("⚠️ Service mode not yet implemented")
|
|
114
|
-
elif action == "restart":
|
|
115
|
-
click.echo("🔄 Restarting dashboard service...")
|
|
116
|
-
click.echo("⚠️ Service mode not yet implemented")
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if __name__ == "__main__":
|
|
120
|
-
dashboard()
|
mcli/workflow/file/file.py
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import click
|
|
2
|
-
import fitz # PyMuPDF
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
@click.group(name="file")
|
|
6
|
-
def file():
|
|
7
|
-
"""Personal file utility to use with custom and/or default file system paths."""
|
|
8
|
-
pass
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@file.command()
|
|
12
|
-
@click.argument("input_oxps", type=click.Path(exists=True))
|
|
13
|
-
@click.argument("output_pdf", type=click.Path())
|
|
14
|
-
def oxps_to_pdf(input_oxps, output_pdf):
|
|
15
|
-
"""Converts an OXPS file (INPUT_OXPS) to a PDF file (OUTPUT_PDF)."""
|
|
16
|
-
try:
|
|
17
|
-
# Open the OXPS file
|
|
18
|
-
oxps_document = fitz.open(input_oxps)
|
|
19
|
-
|
|
20
|
-
# Convert to PDF bytes
|
|
21
|
-
pdf_bytes = oxps_document.convert_to_pdf()
|
|
22
|
-
|
|
23
|
-
# Open the PDF bytes as a new PDF document
|
|
24
|
-
pdf_document = fitz.open("pdf", pdf_bytes)
|
|
25
|
-
|
|
26
|
-
# Save the PDF document to a file
|
|
27
|
-
pdf_document.save(output_pdf)
|
|
28
|
-
|
|
29
|
-
click.echo(f"Successfully converted '{input_oxps}' to '{output_pdf}'")
|
|
30
|
-
|
|
31
|
-
except Exception as e:
|
|
32
|
-
click.echo(f"Error converting file: {e}", err=True)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
import os
|
|
36
|
-
import subprocess
|
|
37
|
-
from pathlib import Path
|
|
38
|
-
from typing import List, Optional
|
|
39
|
-
|
|
40
|
-
DEFAULT_DIRS = ["~/repos/lefv-vault", "~/Documents/OneDrive", "~/Documents/Documents"]
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@file.command(name="search")
|
|
44
|
-
@click.argument("search-string", type=str)
|
|
45
|
-
@click.argument("search-dirs", default=DEFAULT_DIRS)
|
|
46
|
-
@click.argument("context-lines", default=3, type=int)
|
|
47
|
-
def find_string_with_fzf(
|
|
48
|
-
search_string: str = "foo",
|
|
49
|
-
search_dirs: Optional[List[str]] = DEFAULT_DIRS,
|
|
50
|
-
context_lines: int = 3,
|
|
51
|
-
) -> List[str]:
|
|
52
|
-
"""
|
|
53
|
-
Search for a string with ripgrep in given directories and select matches with fzf.
|
|
54
|
-
|
|
55
|
-
Parameters:
|
|
56
|
-
search_string (str): The string to search for.
|
|
57
|
-
search_dirs (Optional[List[str]]): Directories to search in. Defaults to a predefined list.
|
|
58
|
-
context_lines (int): Number of lines of context above and below the match.
|
|
59
|
-
|
|
60
|
-
Returns:
|
|
61
|
-
List[str]: List of selected lines with context from fzf.
|
|
62
|
-
"""
|
|
63
|
-
if not search_string.strip():
|
|
64
|
-
raise ValueError("Search string cannot be empty")
|
|
65
|
-
|
|
66
|
-
dirs_to_search = search_dirs or DEFAULT_DIRS
|
|
67
|
-
expanded_dirs = [str(Path(d).expanduser()) for d in dirs_to_search]
|
|
68
|
-
|
|
69
|
-
# Validate directories exist
|
|
70
|
-
valid_dirs = [d for d in expanded_dirs if Path(d).exists()]
|
|
71
|
-
if not valid_dirs:
|
|
72
|
-
raise FileNotFoundError("None of the provided or default directories exist")
|
|
73
|
-
|
|
74
|
-
# Run ripgrep with context lines
|
|
75
|
-
rg_command = ["rg", "--color=always", f"-C{context_lines}", search_string, *valid_dirs]
|
|
76
|
-
|
|
77
|
-
try:
|
|
78
|
-
rg_proc = subprocess.run(rg_command, capture_output=True, text=True, check=True)
|
|
79
|
-
except subprocess.CalledProcessError as e:
|
|
80
|
-
print("No matches found or error running rg.")
|
|
81
|
-
return []
|
|
82
|
-
|
|
83
|
-
# Pipe the output through fzf
|
|
84
|
-
try:
|
|
85
|
-
fzf_proc = subprocess.run(
|
|
86
|
-
["fzf", "--ansi", "--multi"],
|
|
87
|
-
input=rg_proc.stdout,
|
|
88
|
-
capture_output=True,
|
|
89
|
-
text=True,
|
|
90
|
-
check=True,
|
|
91
|
-
)
|
|
92
|
-
selections = fzf_proc.stdout.strip().split("\n")
|
|
93
|
-
return [s for s in selections if s.strip()]
|
|
94
|
-
except subprocess.CalledProcessError:
|
|
95
|
-
# User exited fzf without selection
|
|
96
|
-
return []
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if __name__ == "__main__":
|
|
100
|
-
file()
|
|
@@ -1,430 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import subprocess
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Any, Dict, List, Optional
|
|
5
|
-
|
|
6
|
-
import click
|
|
7
|
-
|
|
8
|
-
from mcli.lib.logger.logger import get_logger
|
|
9
|
-
|
|
10
|
-
from .ai_service import GitCommitAIService
|
|
11
|
-
|
|
12
|
-
logger = get_logger(__name__)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class GitCommitWorkflow:
|
|
16
|
-
"""Workflow for automatic git commit message generation"""
|
|
17
|
-
|
|
18
|
-
def __init__(self, repo_path: Optional[str] = None, use_ai: bool = True):
|
|
19
|
-
self.repo_path = Path(repo_path) if repo_path else Path.cwd()
|
|
20
|
-
self.use_ai = use_ai
|
|
21
|
-
self.ai_service = GitCommitAIService() if use_ai else None
|
|
22
|
-
self.validate_git_repo()
|
|
23
|
-
|
|
24
|
-
def validate_git_repo(self):
|
|
25
|
-
"""Validate that we're in a git repository"""
|
|
26
|
-
git_dir = self.repo_path / ".git"
|
|
27
|
-
if not git_dir.exists():
|
|
28
|
-
raise ValueError(f"Not a git repository: {self.repo_path}")
|
|
29
|
-
|
|
30
|
-
def get_git_status(self) -> Dict[str, Any]:
|
|
31
|
-
"""Get current git status"""
|
|
32
|
-
try:
|
|
33
|
-
result = subprocess.run(
|
|
34
|
-
["git", "status", "--porcelain"],
|
|
35
|
-
cwd=self.repo_path,
|
|
36
|
-
capture_output=True,
|
|
37
|
-
text=True,
|
|
38
|
-
check=True,
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
status_lines = result.stdout.strip().split("\n") if result.stdout.strip() else []
|
|
42
|
-
|
|
43
|
-
changes = {"modified": [], "added": [], "deleted": [], "renamed": [], "untracked": []}
|
|
44
|
-
|
|
45
|
-
for line in status_lines:
|
|
46
|
-
if len(line) < 3:
|
|
47
|
-
continue
|
|
48
|
-
|
|
49
|
-
status_code = line[:2]
|
|
50
|
-
filename = line[3:]
|
|
51
|
-
|
|
52
|
-
if status_code[0] == "M" or status_code[1] == "M":
|
|
53
|
-
changes["modified"].append(filename)
|
|
54
|
-
elif status_code[0] == "A":
|
|
55
|
-
changes["added"].append(filename)
|
|
56
|
-
elif status_code[0] == "D":
|
|
57
|
-
changes["deleted"].append(filename)
|
|
58
|
-
elif status_code[0] == "R":
|
|
59
|
-
changes["renamed"].append(filename)
|
|
60
|
-
elif status_code == "??":
|
|
61
|
-
changes["untracked"].append(filename)
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
"has_changes": len(status_lines) > 0,
|
|
65
|
-
"changes": changes,
|
|
66
|
-
"total_files": len(status_lines),
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
except subprocess.CalledProcessError as e:
|
|
70
|
-
raise RuntimeError(f"Failed to get git status: {e}")
|
|
71
|
-
|
|
72
|
-
def get_git_diff(self) -> str:
|
|
73
|
-
"""Get git diff for all changes"""
|
|
74
|
-
try:
|
|
75
|
-
# Get diff for staged and unstaged changes
|
|
76
|
-
staged_result = subprocess.run(
|
|
77
|
-
["git", "diff", "--cached"],
|
|
78
|
-
cwd=self.repo_path,
|
|
79
|
-
capture_output=True,
|
|
80
|
-
text=True,
|
|
81
|
-
check=True,
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
unstaged_result = subprocess.run(
|
|
85
|
-
["git", "diff"], cwd=self.repo_path, capture_output=True, text=True, check=True
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
diff_content = ""
|
|
89
|
-
if staged_result.stdout:
|
|
90
|
-
diff_content += "=== STAGED CHANGES ===\n" + staged_result.stdout + "\n"
|
|
91
|
-
if unstaged_result.stdout:
|
|
92
|
-
diff_content += "=== UNSTAGED CHANGES ===\n" + unstaged_result.stdout + "\n"
|
|
93
|
-
|
|
94
|
-
return diff_content
|
|
95
|
-
|
|
96
|
-
except subprocess.CalledProcessError as e:
|
|
97
|
-
raise RuntimeError(f"Failed to get git diff: {e}")
|
|
98
|
-
|
|
99
|
-
def stage_all_changes(self) -> bool:
|
|
100
|
-
"""Stage all changes for commit"""
|
|
101
|
-
try:
|
|
102
|
-
subprocess.run(["git", "add", "."], cwd=self.repo_path, check=True)
|
|
103
|
-
logger.info("All changes staged successfully")
|
|
104
|
-
return True
|
|
105
|
-
|
|
106
|
-
except subprocess.CalledProcessError as e:
|
|
107
|
-
logger.error(f"Failed to stage changes: {e}")
|
|
108
|
-
return False
|
|
109
|
-
|
|
110
|
-
def generate_commit_message(self, changes: Dict[str, Any], diff_content: str) -> str:
|
|
111
|
-
"""Generate a commit message using AI or fallback to rule-based generation"""
|
|
112
|
-
|
|
113
|
-
if self.use_ai and self.ai_service:
|
|
114
|
-
try:
|
|
115
|
-
logger.info("Generating AI-powered commit message...")
|
|
116
|
-
ai_message = self.ai_service.generate_commit_message(changes, diff_content)
|
|
117
|
-
if ai_message:
|
|
118
|
-
return ai_message
|
|
119
|
-
else:
|
|
120
|
-
logger.warning("AI service returned empty message, falling back to rule-based")
|
|
121
|
-
except Exception as e:
|
|
122
|
-
logger.error(f"AI commit message generation failed: {e}")
|
|
123
|
-
logger.info("Falling back to rule-based commit message generation")
|
|
124
|
-
|
|
125
|
-
# Fallback to rule-based generation
|
|
126
|
-
return self._generate_rule_based_message(changes)
|
|
127
|
-
|
|
128
|
-
def _generate_rule_based_message(self, changes: Dict[str, Any]) -> str:
|
|
129
|
-
"""Generate rule-based commit message (original implementation)"""
|
|
130
|
-
summary_parts = []
|
|
131
|
-
|
|
132
|
-
# Analyze file changes
|
|
133
|
-
if changes["changes"]["added"]:
|
|
134
|
-
if len(changes["changes"]["added"]) == 1:
|
|
135
|
-
summary_parts.append(f"Add {changes['changes']['added'][0]}")
|
|
136
|
-
else:
|
|
137
|
-
summary_parts.append(f"Add {len(changes['changes']['added'])} new files")
|
|
138
|
-
|
|
139
|
-
if changes["changes"]["modified"]:
|
|
140
|
-
if len(changes["changes"]["modified"]) == 1:
|
|
141
|
-
summary_parts.append(f"Update {changes['changes']['modified'][0]}")
|
|
142
|
-
else:
|
|
143
|
-
summary_parts.append(f"Update {len(changes['changes']['modified'])} files")
|
|
144
|
-
|
|
145
|
-
if changes["changes"]["deleted"]:
|
|
146
|
-
if len(changes["changes"]["deleted"]) == 1:
|
|
147
|
-
summary_parts.append(f"Remove {changes['changes']['deleted'][0]}")
|
|
148
|
-
else:
|
|
149
|
-
summary_parts.append(f"Remove {len(changes['changes']['deleted'])} files")
|
|
150
|
-
|
|
151
|
-
if changes["changes"]["renamed"]:
|
|
152
|
-
summary_parts.append(f"Rename {len(changes['changes']['renamed'])} files")
|
|
153
|
-
|
|
154
|
-
# Create commit message
|
|
155
|
-
if not summary_parts:
|
|
156
|
-
commit_message = "Update repository files"
|
|
157
|
-
else:
|
|
158
|
-
commit_message = ", ".join(summary_parts)
|
|
159
|
-
|
|
160
|
-
# Add more context based on file patterns
|
|
161
|
-
modified_files = changes["changes"]["modified"] + changes["changes"]["added"]
|
|
162
|
-
|
|
163
|
-
# Check for specific file types
|
|
164
|
-
if any(".py" in f for f in modified_files):
|
|
165
|
-
commit_message += " (Python changes)"
|
|
166
|
-
elif any(".js" in f or ".ts" in f for f in modified_files):
|
|
167
|
-
commit_message += " (JavaScript/TypeScript changes)"
|
|
168
|
-
elif any(".md" in f for f in modified_files):
|
|
169
|
-
commit_message += " (Documentation changes)"
|
|
170
|
-
elif any(
|
|
171
|
-
"requirements" in f or "package.json" in f or "Cargo.toml" in f for f in modified_files
|
|
172
|
-
):
|
|
173
|
-
commit_message += " (Dependencies changes)"
|
|
174
|
-
|
|
175
|
-
return commit_message
|
|
176
|
-
|
|
177
|
-
def create_commit(self, message: str) -> bool:
|
|
178
|
-
"""Create a git commit with the given message"""
|
|
179
|
-
try:
|
|
180
|
-
subprocess.run(["git", "commit", "-m", message], cwd=self.repo_path, check=True)
|
|
181
|
-
logger.info(f"Commit created successfully: {message}")
|
|
182
|
-
return True
|
|
183
|
-
|
|
184
|
-
except subprocess.CalledProcessError as e:
|
|
185
|
-
logger.error(f"Failed to create commit: {e}")
|
|
186
|
-
return False
|
|
187
|
-
|
|
188
|
-
def run_auto_commit(self) -> Dict[str, Any]:
|
|
189
|
-
"""Run the complete auto-commit workflow"""
|
|
190
|
-
result = {
|
|
191
|
-
"success": False,
|
|
192
|
-
"message": "",
|
|
193
|
-
"commit_hash": None,
|
|
194
|
-
"changes_summary": {},
|
|
195
|
-
"error": None,
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
try:
|
|
199
|
-
# Check git status
|
|
200
|
-
status = self.get_git_status()
|
|
201
|
-
result["changes_summary"] = status
|
|
202
|
-
|
|
203
|
-
if not status["has_changes"]:
|
|
204
|
-
result["message"] = "No changes to commit"
|
|
205
|
-
result["success"] = True
|
|
206
|
-
return result
|
|
207
|
-
|
|
208
|
-
# Get diff content
|
|
209
|
-
diff_content = self.get_git_diff()
|
|
210
|
-
|
|
211
|
-
# Stage all changes
|
|
212
|
-
if not self.stage_all_changes():
|
|
213
|
-
result["error"] = "Failed to stage changes"
|
|
214
|
-
return result
|
|
215
|
-
|
|
216
|
-
# Generate commit message
|
|
217
|
-
commit_message = self.generate_commit_message(status, diff_content)
|
|
218
|
-
result["message"] = commit_message
|
|
219
|
-
|
|
220
|
-
# Create commit
|
|
221
|
-
if self.create_commit(commit_message):
|
|
222
|
-
# Get commit hash
|
|
223
|
-
try:
|
|
224
|
-
hash_result = subprocess.run(
|
|
225
|
-
["git", "rev-parse", "HEAD"],
|
|
226
|
-
cwd=self.repo_path,
|
|
227
|
-
capture_output=True,
|
|
228
|
-
text=True,
|
|
229
|
-
check=True,
|
|
230
|
-
)
|
|
231
|
-
result["commit_hash"] = hash_result.stdout.strip()
|
|
232
|
-
except:
|
|
233
|
-
pass
|
|
234
|
-
|
|
235
|
-
result["success"] = True
|
|
236
|
-
else:
|
|
237
|
-
result["error"] = "Failed to create commit"
|
|
238
|
-
|
|
239
|
-
except Exception as e:
|
|
240
|
-
result["error"] = str(e)
|
|
241
|
-
logger.error(f"Auto-commit workflow failed: {e}")
|
|
242
|
-
|
|
243
|
-
return result
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
@click.group(name="git-commit")
|
|
247
|
-
def git_commit_cli():
|
|
248
|
-
"""AI-powered git commit message generation"""
|
|
249
|
-
pass
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
@git_commit_cli.command()
|
|
253
|
-
@click.option(
|
|
254
|
-
"--repo-path", type=click.Path(), help="Path to git repository (default: current directory)"
|
|
255
|
-
)
|
|
256
|
-
@click.option(
|
|
257
|
-
"--dry-run", is_flag=True, help="Show what would be committed without actually committing"
|
|
258
|
-
)
|
|
259
|
-
@click.option("--no-ai", is_flag=True, help="Disable AI-powered commit message generation")
|
|
260
|
-
@click.option("--model", type=str, help="Override AI model for commit message generation")
|
|
261
|
-
def auto(repo_path: Optional[str], dry_run: bool, no_ai: bool, model: Optional[str]):
|
|
262
|
-
"""Automatically stage changes and create commit with AI-generated message"""
|
|
263
|
-
|
|
264
|
-
try:
|
|
265
|
-
workflow = GitCommitWorkflow(repo_path, use_ai=not no_ai)
|
|
266
|
-
|
|
267
|
-
# Override AI model if specified
|
|
268
|
-
if not no_ai and model and workflow.ai_service:
|
|
269
|
-
workflow.ai_service.model_name = model
|
|
270
|
-
|
|
271
|
-
if dry_run:
|
|
272
|
-
# Just show what would happen
|
|
273
|
-
status = workflow.get_git_status()
|
|
274
|
-
|
|
275
|
-
if not status["has_changes"]:
|
|
276
|
-
click.echo("No changes to commit")
|
|
277
|
-
return
|
|
278
|
-
|
|
279
|
-
click.echo("Changes that would be staged and committed:")
|
|
280
|
-
changes = status["changes"]
|
|
281
|
-
|
|
282
|
-
if changes["untracked"]:
|
|
283
|
-
click.echo(f" New files ({len(changes['untracked'])}):")
|
|
284
|
-
for file in changes["untracked"]:
|
|
285
|
-
click.echo(f" + {file}")
|
|
286
|
-
|
|
287
|
-
if changes["modified"]:
|
|
288
|
-
click.echo(f" Modified files ({len(changes['modified'])}):")
|
|
289
|
-
for file in changes["modified"]:
|
|
290
|
-
click.echo(f" M {file}")
|
|
291
|
-
|
|
292
|
-
if changes["deleted"]:
|
|
293
|
-
click.echo(f" Deleted files ({len(changes['deleted'])}):")
|
|
294
|
-
for file in changes["deleted"]:
|
|
295
|
-
click.echo(f" - {file}")
|
|
296
|
-
|
|
297
|
-
# Generate and show commit message
|
|
298
|
-
diff_content = workflow.get_git_diff()
|
|
299
|
-
commit_message = workflow.generate_commit_message(status, diff_content)
|
|
300
|
-
click.echo(f"\nProposed commit message: {commit_message}")
|
|
301
|
-
|
|
302
|
-
else:
|
|
303
|
-
# Actually run the workflow
|
|
304
|
-
result = workflow.run_auto_commit()
|
|
305
|
-
|
|
306
|
-
if result["success"]:
|
|
307
|
-
if result["commit_hash"]:
|
|
308
|
-
click.echo(f"✅ Commit created successfully: {result['commit_hash'][:8]}")
|
|
309
|
-
else:
|
|
310
|
-
click.echo("✅ Commit created successfully")
|
|
311
|
-
|
|
312
|
-
click.echo(f"Message: {result['message']}")
|
|
313
|
-
|
|
314
|
-
# Show summary
|
|
315
|
-
changes = result["changes_summary"]["changes"]
|
|
316
|
-
total = sum(len(files) for files in changes.values())
|
|
317
|
-
click.echo(f"Files changed: {total}")
|
|
318
|
-
|
|
319
|
-
else:
|
|
320
|
-
click.echo(f"❌ Failed to create commit: {result.get('error', 'Unknown error')}")
|
|
321
|
-
|
|
322
|
-
except Exception as e:
|
|
323
|
-
click.echo(f"❌ Error: {e}")
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
@git_commit_cli.command()
|
|
327
|
-
@click.option(
|
|
328
|
-
"--repo-path", type=click.Path(), help="Path to git repository (default: current directory)"
|
|
329
|
-
)
|
|
330
|
-
def status(repo_path: Optional[str]):
|
|
331
|
-
"""Show current git repository status"""
|
|
332
|
-
|
|
333
|
-
try:
|
|
334
|
-
workflow = GitCommitWorkflow(repo_path)
|
|
335
|
-
status_info = workflow.get_git_status()
|
|
336
|
-
|
|
337
|
-
if not status_info["has_changes"]:
|
|
338
|
-
click.echo("✅ Working tree is clean")
|
|
339
|
-
return
|
|
340
|
-
|
|
341
|
-
click.echo("📋 Repository Status:")
|
|
342
|
-
changes = status_info["changes"]
|
|
343
|
-
|
|
344
|
-
if changes["untracked"]:
|
|
345
|
-
click.echo(f"\n📄 Untracked files ({len(changes['untracked'])}):")
|
|
346
|
-
for file in changes["untracked"]:
|
|
347
|
-
click.echo(f" ?? {file}")
|
|
348
|
-
|
|
349
|
-
if changes["modified"]:
|
|
350
|
-
click.echo(f"\n✏️ Modified files ({len(changes['modified'])}):")
|
|
351
|
-
for file in changes["modified"]:
|
|
352
|
-
click.echo(f" M {file}")
|
|
353
|
-
|
|
354
|
-
if changes["added"]:
|
|
355
|
-
click.echo(f"\n➕ Added files ({len(changes['added'])}):")
|
|
356
|
-
for file in changes["added"]:
|
|
357
|
-
click.echo(f" A {file}")
|
|
358
|
-
|
|
359
|
-
if changes["deleted"]:
|
|
360
|
-
click.echo(f"\n🗑️ Deleted files ({len(changes['deleted'])}):")
|
|
361
|
-
for file in changes["deleted"]:
|
|
362
|
-
click.echo(f" D {file}")
|
|
363
|
-
|
|
364
|
-
if changes["renamed"]:
|
|
365
|
-
click.echo(f"\n🔄 Renamed files ({len(changes['renamed'])}):")
|
|
366
|
-
for file in changes["renamed"]:
|
|
367
|
-
click.echo(f" R {file}")
|
|
368
|
-
|
|
369
|
-
click.echo(f"\nTotal files with changes: {status_info['total_files']}")
|
|
370
|
-
|
|
371
|
-
except Exception as e:
|
|
372
|
-
click.echo(f"❌ Error: {e}")
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
@git_commit_cli.command()
|
|
376
|
-
@click.argument("message")
|
|
377
|
-
@click.option(
|
|
378
|
-
"--repo-path", type=click.Path(), help="Path to git repository (default: current directory)"
|
|
379
|
-
)
|
|
380
|
-
@click.option("--stage-all", is_flag=True, help="Stage all changes before committing")
|
|
381
|
-
def commit(message: str, repo_path: Optional[str], stage_all: bool):
|
|
382
|
-
"""Create a commit with a custom message"""
|
|
383
|
-
|
|
384
|
-
try:
|
|
385
|
-
workflow = GitCommitWorkflow(repo_path)
|
|
386
|
-
|
|
387
|
-
if stage_all:
|
|
388
|
-
if not workflow.stage_all_changes():
|
|
389
|
-
click.echo("❌ Failed to stage changes")
|
|
390
|
-
return
|
|
391
|
-
|
|
392
|
-
if workflow.create_commit(message):
|
|
393
|
-
click.echo(f"✅ Commit created: {message}")
|
|
394
|
-
else:
|
|
395
|
-
click.echo("❌ Failed to create commit")
|
|
396
|
-
|
|
397
|
-
except Exception as e:
|
|
398
|
-
click.echo(f"❌ Error: {e}")
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
@git_commit_cli.command()
|
|
402
|
-
@click.option("--model", type=str, help="Override AI model for testing")
|
|
403
|
-
def test_ai(model: Optional[str]):
|
|
404
|
-
"""Test the AI service for commit message generation"""
|
|
405
|
-
|
|
406
|
-
try:
|
|
407
|
-
from .ai_service import GitCommitAIService
|
|
408
|
-
|
|
409
|
-
ai_service = GitCommitAIService()
|
|
410
|
-
|
|
411
|
-
# Override model if specified
|
|
412
|
-
if model:
|
|
413
|
-
ai_service.model_name = model
|
|
414
|
-
|
|
415
|
-
click.echo(f"🧪 Testing AI service with model: {ai_service.model_name}")
|
|
416
|
-
click.echo(f"🌐 Ollama base URL: {ai_service.ollama_base_url}")
|
|
417
|
-
click.echo(f"🌡️ Temperature: {ai_service.temperature}")
|
|
418
|
-
|
|
419
|
-
# Test AI service
|
|
420
|
-
if ai_service.test_ai_service():
|
|
421
|
-
click.echo("✅ AI service test passed!")
|
|
422
|
-
else:
|
|
423
|
-
click.echo("❌ AI service test failed!")
|
|
424
|
-
|
|
425
|
-
except Exception as e:
|
|
426
|
-
click.echo(f"❌ Error testing AI service: {e}")
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
if __name__ == "__main__":
|
|
430
|
-
git_commit_cli()
|