aga-ai-github-assistant 1.2.0__tar.gz
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.
- aga_ai_github_assistant-1.2.0/PKG-INFO +20 -0
- aga_ai_github_assistant-1.2.0/aga/__init__.py +0 -0
- aga_ai_github_assistant-1.2.0/aga/core/__init__.py +0 -0
- aga_ai_github_assistant-1.2.0/aga/core/ai_engine.py +56 -0
- aga_ai_github_assistant-1.2.0/aga/core/github_client.py +104 -0
- aga_ai_github_assistant-1.2.0/aga/modules/__init__.py +0 -0
- aga_ai_github_assistant-1.2.0/aga/modules/analyzer.py +57 -0
- aga_ai_github_assistant-1.2.0/aga/modules/readme_gen.py +27 -0
- aga_ai_github_assistant-1.2.0/aga/modules/uploader.py +72 -0
- aga_ai_github_assistant-1.2.0/aga/utils/__init__.py +0 -0
- aga_ai_github_assistant-1.2.0/aga_ai_github_assistant.egg-info/PKG-INFO +20 -0
- aga_ai_github_assistant-1.2.0/aga_ai_github_assistant.egg-info/SOURCES.txt +18 -0
- aga_ai_github_assistant-1.2.0/aga_ai_github_assistant.egg-info/dependency_links.txt +1 -0
- aga_ai_github_assistant-1.2.0/aga_ai_github_assistant.egg-info/entry_points.txt +2 -0
- aga_ai_github_assistant-1.2.0/aga_ai_github_assistant.egg-info/requires.txt +13 -0
- aga_ai_github_assistant-1.2.0/aga_ai_github_assistant.egg-info/top_level.txt +3 -0
- aga_ai_github_assistant-1.2.0/app/main.py +426 -0
- aga_ai_github_assistant-1.2.0/cli/main.py +56 -0
- aga_ai_github_assistant-1.2.0/pyproject.toml +33 -0
- aga_ai_github_assistant-1.2.0/setup.cfg +4 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aga-ai-github-assistant
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: AI-powered GitHub assistant with file deployment, repo analysis, and README generation.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: gradio>=4.0.0
|
|
9
|
+
Requires-Dist: fastapi
|
|
10
|
+
Requires-Dist: uvicorn
|
|
11
|
+
Requires-Dist: langchain
|
|
12
|
+
Requires-Dist: langchain-groq
|
|
13
|
+
Requires-Dist: langchain-core
|
|
14
|
+
Requires-Dist: PyGithub
|
|
15
|
+
Requires-Dist: python-dotenv
|
|
16
|
+
Requires-Dist: httpx
|
|
17
|
+
Requires-Dist: fpdf2
|
|
18
|
+
Requires-Dist: tabulate
|
|
19
|
+
Requires-Dist: tenacity
|
|
20
|
+
Requires-Dist: click
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from langchain_groq import ChatGroq
|
|
3
|
+
from langchain_core.prompts import PromptTemplate
|
|
4
|
+
from langchain_core.output_parsers import StrOutputParser
|
|
5
|
+
|
|
6
|
+
class AIEngine:
|
|
7
|
+
"""
|
|
8
|
+
Core AI logic using Groq Llama 3.3.
|
|
9
|
+
Handles Features 3 (Commits), 4 (README), and 5 (Analysis).
|
|
10
|
+
"""
|
|
11
|
+
def __init__(self, api_key: str, model: str = "llama-3.3-70b-versatile"):
|
|
12
|
+
self.llm = ChatGroq(
|
|
13
|
+
groq_api_key=api_key,
|
|
14
|
+
model_name=model,
|
|
15
|
+
temperature=0.2
|
|
16
|
+
)
|
|
17
|
+
self.parser = StrOutputParser()
|
|
18
|
+
|
|
19
|
+
def generate_commit_message(self, file_summaries: str, style: str = "conventional") -> str:
|
|
20
|
+
template = """You are an expert developer. Based on the following file summaries,
|
|
21
|
+
write a {style} git commit message.
|
|
22
|
+
|
|
23
|
+
Summaries:
|
|
24
|
+
{file_summaries}
|
|
25
|
+
|
|
26
|
+
Commit message (one line, max 72 chars):"""
|
|
27
|
+
|
|
28
|
+
prompt = PromptTemplate.from_template(template)
|
|
29
|
+
chain = prompt | self.llm | self.parser
|
|
30
|
+
return chain.invoke({"file_summaries": file_summaries, "style": style}).strip()
|
|
31
|
+
|
|
32
|
+
def generate_readme(self, project_data: str, style: str = "professional") -> str:
|
|
33
|
+
template = """You are a documentation specialist. Create a {style} README.md for this project.
|
|
34
|
+
Include Title, Features, Tech Stack, and Installation.
|
|
35
|
+
|
|
36
|
+
Project context:
|
|
37
|
+
{project_data}
|
|
38
|
+
|
|
39
|
+
README.md content:"""
|
|
40
|
+
|
|
41
|
+
prompt = PromptTemplate.from_template(template)
|
|
42
|
+
chain = prompt | self.llm | self.parser
|
|
43
|
+
return chain.invoke({"project_data": project_data, "style": style})
|
|
44
|
+
|
|
45
|
+
def analyze_repository(self, repo_content: str) -> str:
|
|
46
|
+
template = """Perform a deep architectural and security analysis of the following project.
|
|
47
|
+
Provide a Health Score (0-100) and specific improvement areas.
|
|
48
|
+
|
|
49
|
+
Project Content:
|
|
50
|
+
{repo_content}
|
|
51
|
+
|
|
52
|
+
Full Report:"""
|
|
53
|
+
|
|
54
|
+
prompt = PromptTemplate.from_template(template)
|
|
55
|
+
chain = prompt | self.llm | self.parser
|
|
56
|
+
return chain.invoke({"repo_content": repo_content})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import html
|
|
2
|
+
import logging
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
from github import Github, Repository, AuthenticatedUser
|
|
5
|
+
from github.GithubException import GithubException
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _sanitize_path(path: str) -> str:
|
|
9
|
+
parts = [p for p in path.replace("\\", "/").split("/") if p and p != ".."]
|
|
10
|
+
return "/".join(parts)
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
class GitHubClient:
|
|
15
|
+
"""
|
|
16
|
+
Professional wrapper for GitHub API operations.
|
|
17
|
+
Handles Feature 1: Repository Management.
|
|
18
|
+
"""
|
|
19
|
+
def __init__(self, token: str):
|
|
20
|
+
self.client = Github(token)
|
|
21
|
+
try:
|
|
22
|
+
self.user: AuthenticatedUser = self.client.get_user()
|
|
23
|
+
logger.info(f"Authenticated as {self.user.login}")
|
|
24
|
+
except Exception as e:
|
|
25
|
+
logger.error(f"GitHub Authentication failed: {e}")
|
|
26
|
+
raise
|
|
27
|
+
|
|
28
|
+
def create_repository(
|
|
29
|
+
self,
|
|
30
|
+
name: str,
|
|
31
|
+
description: str = "",
|
|
32
|
+
private: bool = True,
|
|
33
|
+
auto_init: bool = True
|
|
34
|
+
) -> Repository.Repository:
|
|
35
|
+
"""Create a new repository for the authenticated user."""
|
|
36
|
+
try:
|
|
37
|
+
repo = self.user.create_repo(
|
|
38
|
+
name=name,
|
|
39
|
+
description=description,
|
|
40
|
+
private=private,
|
|
41
|
+
auto_init=auto_init
|
|
42
|
+
)
|
|
43
|
+
return repo
|
|
44
|
+
except GithubException as e:
|
|
45
|
+
logger.error(f"Failed to create repository {name}: {e}")
|
|
46
|
+
raise
|
|
47
|
+
|
|
48
|
+
def list_repositories(self) -> List[Repository.Repository]:
|
|
49
|
+
"""List all repositories for the authenticated user."""
|
|
50
|
+
return list(self.user.get_repos())
|
|
51
|
+
|
|
52
|
+
def get_repo_details(self, repo_name: str) -> Repository.Repository:
|
|
53
|
+
"""Get details for a specific repository (e.g. 'username/repo')."""
|
|
54
|
+
try:
|
|
55
|
+
return self.client.get_repo(repo_name)
|
|
56
|
+
except GithubException as e:
|
|
57
|
+
logger.error(f"Repository {repo_name} not found: {e}")
|
|
58
|
+
raise
|
|
59
|
+
|
|
60
|
+
def get_contents(self, repo_name: str, path: str = "") -> List:
|
|
61
|
+
"""Fetch contents of a repository at a specific path."""
|
|
62
|
+
safe_path = _sanitize_path(path)
|
|
63
|
+
try:
|
|
64
|
+
repo = self.client.get_repo(repo_name)
|
|
65
|
+
contents = repo.get_contents(safe_path)
|
|
66
|
+
if not isinstance(contents, list):
|
|
67
|
+
contents = [contents]
|
|
68
|
+
return contents
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.error(f"Error fetching contents for {repo_name} at {safe_path}: {e}")
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
def push_file(self, repo_name: str, path: str, content: str, commit_message: str, branch: str = "main"):
|
|
74
|
+
"""Push a file to a specific path in a repository (create or update)."""
|
|
75
|
+
safe_path = _sanitize_path(path)
|
|
76
|
+
try:
|
|
77
|
+
repo = self.client.get_repo(repo_name)
|
|
78
|
+
try:
|
|
79
|
+
contents = repo.get_contents(safe_path, ref=branch)
|
|
80
|
+
|
|
81
|
+
if isinstance(contents, list):
|
|
82
|
+
return f"Error: '{html.escape(safe_path)}' is a directory. Please specify a filename (e.g. {html.escape(safe_path.rstrip('/'))}/myfile.txt)"
|
|
83
|
+
|
|
84
|
+
repo.update_file(safe_path, commit_message, content, contents.sha, branch=branch)
|
|
85
|
+
return f"Successfully Updated: {html.escape(safe_path)}"
|
|
86
|
+
except GithubException as e:
|
|
87
|
+
if e.status == 404:
|
|
88
|
+
repo.create_file(safe_path, commit_message, content, branch=branch)
|
|
89
|
+
return f"Successfully Created: {html.escape(safe_path)}"
|
|
90
|
+
else:
|
|
91
|
+
return f"GitHub Error ({e.status}): {html.escape(e.data.get('message', str(e)))}"
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error(f"Push failed: {repo_name}/{safe_path}: {e}")
|
|
94
|
+
return f"System Error: {html.escape(str(e))}"
|
|
95
|
+
|
|
96
|
+
def delete_repository(self, full_name: str):
|
|
97
|
+
"""Delete a repository. Note: Requires 'delete_repo' scope on token."""
|
|
98
|
+
try:
|
|
99
|
+
repo = self.client.get_repo(full_name)
|
|
100
|
+
repo.delete()
|
|
101
|
+
logger.info(f"Deleted repository {full_name}")
|
|
102
|
+
except GithubException as e:
|
|
103
|
+
logger.error(f"Failed to delete repository {full_name}: {e}")
|
|
104
|
+
raise
|
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
from aga.core.github_client import GitHubClient
|
|
3
|
+
from aga.core.ai_engine import AIEngine
|
|
4
|
+
|
|
5
|
+
class RepoAnalyzer:
|
|
6
|
+
"""
|
|
7
|
+
Feature 5: Repository Analyzer.
|
|
8
|
+
Calculates health scores and provides reasoning.
|
|
9
|
+
"""
|
|
10
|
+
def __init__(self, github_client: GitHubClient, ai_engine: AIEngine):
|
|
11
|
+
self.gh = github_client
|
|
12
|
+
self.ai = ai_engine
|
|
13
|
+
|
|
14
|
+
def analyze(self, repo_url: str) -> Dict:
|
|
15
|
+
"""Performed deep analysis on a repository."""
|
|
16
|
+
# Extracts 'owner/repo' from URL
|
|
17
|
+
repo_name = repo_url.replace("https://github.com/", "").strip("/")
|
|
18
|
+
repo = self.gh.get_repo_details(repo_name)
|
|
19
|
+
|
|
20
|
+
# 1. Quantitative Metrics
|
|
21
|
+
metrics = {
|
|
22
|
+
"stars": repo.stargazers_count,
|
|
23
|
+
"forks": repo.forks_count,
|
|
24
|
+
"open_issues": repo.open_issues_count,
|
|
25
|
+
"has_readme": bool(repo.get_contents("README.md") if self._check_file(repo, "README.md") else False),
|
|
26
|
+
"has_license": bool(repo.license),
|
|
27
|
+
"last_updated": repo.updated_at.strftime("%Y-%m-%d")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# 2. Qualitative AI Analysis
|
|
31
|
+
# We fetch a summary of the root directory to feed the AI
|
|
32
|
+
contents = repo.get_contents("")
|
|
33
|
+
structure = "\n".join([c.path for c in contents])
|
|
34
|
+
|
|
35
|
+
ai_report = self.ai.analyze_repository(f"Repo: {repo_name}\nFiles:\n{structure}\nMetrics: {metrics}")
|
|
36
|
+
|
|
37
|
+
# 3. Final Scoring Logic (Mock logic for specific scores)
|
|
38
|
+
scores = {
|
|
39
|
+
"health": 85 if metrics["has_readme"] and metrics["has_license"] else 50,
|
|
40
|
+
"security": 70, # Future: Scan for secrets or vulnerable dependencies
|
|
41
|
+
"maintainability": 80 if metrics["open_issues"] < 10 else 60,
|
|
42
|
+
"documentation": 95 if metrics["has_readme"] else 20
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
"name": repo_name,
|
|
47
|
+
"metrics": metrics,
|
|
48
|
+
"scores": scores,
|
|
49
|
+
"report": ai_report
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
def _check_file(self, repo, path):
|
|
53
|
+
try:
|
|
54
|
+
repo.get_contents(path)
|
|
55
|
+
return True
|
|
56
|
+
except:
|
|
57
|
+
return False
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from aga.core.ai_engine import AIEngine
|
|
3
|
+
|
|
4
|
+
class ReadmeGenerator:
|
|
5
|
+
"""
|
|
6
|
+
Feature 4: AI README Generator.
|
|
7
|
+
Supports multiple presentation styles.
|
|
8
|
+
"""
|
|
9
|
+
STYLES = {
|
|
10
|
+
"professional": "Formal, comprehensive, suitable for enterprise tools.",
|
|
11
|
+
"open_source": "Community-focused, includes contribution guidelines and badges.",
|
|
12
|
+
"startup": "Aggressive, feature-heavy, focused on unique selling points.",
|
|
13
|
+
"hackathon": "Quick, punchy, focused on the 'how it works' and demo video.",
|
|
14
|
+
"minimal": "Clean, focused strictly on installation and usage."
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def __init__(self, ai_engine: AIEngine):
|
|
18
|
+
self.ai = ai_engine
|
|
19
|
+
|
|
20
|
+
def generate(self, project_summary: str, style: str = "professional") -> str:
|
|
21
|
+
if style not in self.STYLES:
|
|
22
|
+
style = "professional"
|
|
23
|
+
|
|
24
|
+
custom_instructions = self.STYLES[style]
|
|
25
|
+
prompt_data = f"Project Summary: {project_summary}\nStyle Instructions: {custom_instructions}"
|
|
26
|
+
|
|
27
|
+
return self.ai.generate_readme(prompt_data, style=style)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import magic
|
|
3
|
+
from typing import List, Dict, Optional
|
|
4
|
+
|
|
5
|
+
class ProjectScanner:
|
|
6
|
+
"""
|
|
7
|
+
Feature 2: Advanced Project Upload & Scanning.
|
|
8
|
+
Detects project types, languages, and frameworks.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Signature files for framework detection
|
|
12
|
+
FRAMEWORKS = {
|
|
13
|
+
"requirements.txt": "Python (Pip)",
|
|
14
|
+
"package.json": "JavaScript/TypeScript (Node.js)",
|
|
15
|
+
"pom.xml": "Java (Maven)",
|
|
16
|
+
"build.gradle": "Java (Gradle)",
|
|
17
|
+
"go.mod": "Go",
|
|
18
|
+
"Cargo.toml": "Rust",
|
|
19
|
+
"composer.json": "PHP",
|
|
20
|
+
"Gemfile": "Ruby",
|
|
21
|
+
"manage.py": "Django",
|
|
22
|
+
"app.py": "Flask/FastAPI",
|
|
23
|
+
"next.config.js": "Next.js",
|
|
24
|
+
"tailwind.config.js": "Tailwind CSS",
|
|
25
|
+
"docker-compose.yml": "Docker Compose",
|
|
26
|
+
"Dockerfile": "Docker"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
EXTENSION_MAP = {
|
|
30
|
+
".py": "Python",
|
|
31
|
+
".js": "JavaScript",
|
|
32
|
+
".ts": "TypeScript",
|
|
33
|
+
".java": "Java",
|
|
34
|
+
".cpp": "C++",
|
|
35
|
+
".go": "Go",
|
|
36
|
+
".rs": "Rust",
|
|
37
|
+
".rb": "Ruby",
|
|
38
|
+
".php": "PHP"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def scan_path(self, folder_path: str) -> Dict:
|
|
42
|
+
"""Analyze a local directory for project metadata."""
|
|
43
|
+
stats = {
|
|
44
|
+
"languages": set(),
|
|
45
|
+
"frameworks": set(),
|
|
46
|
+
"file_count": 0,
|
|
47
|
+
"structure": []
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for root, dirs, files in os.walk(folder_path):
|
|
51
|
+
# Exclude common ignores
|
|
52
|
+
dirs[:] = [d for d in dirs if d not in ['.git', 'node_modules', '__pycache__', 'venv']]
|
|
53
|
+
|
|
54
|
+
for file in files:
|
|
55
|
+
stats["file_count"] += 1
|
|
56
|
+
ext = os.path.splitext(file)[1]
|
|
57
|
+
|
|
58
|
+
if ext in self.EXTENSION_MAP:
|
|
59
|
+
stats["languages"].add(self.EXTENSION_MAP[ext])
|
|
60
|
+
|
|
61
|
+
if file in self.FRAMEWORKS:
|
|
62
|
+
stats["frameworks"].add(self.FRAMEWORKS[file])
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
"languages": list(stats["languages"]),
|
|
66
|
+
"frameworks": list(stats["frameworks"]),
|
|
67
|
+
"file_count": stats["file_count"]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
def detect_mime(self, file_path: str) -> str:
|
|
71
|
+
"""Use libmagic to detect true file type."""
|
|
72
|
+
return magic.from_file(file_path, mime=True)
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aga-ai-github-assistant
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: AI-powered GitHub assistant with file deployment, repo analysis, and README generation.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: gradio>=4.0.0
|
|
9
|
+
Requires-Dist: fastapi
|
|
10
|
+
Requires-Dist: uvicorn
|
|
11
|
+
Requires-Dist: langchain
|
|
12
|
+
Requires-Dist: langchain-groq
|
|
13
|
+
Requires-Dist: langchain-core
|
|
14
|
+
Requires-Dist: PyGithub
|
|
15
|
+
Requires-Dist: python-dotenv
|
|
16
|
+
Requires-Dist: httpx
|
|
17
|
+
Requires-Dist: fpdf2
|
|
18
|
+
Requires-Dist: tabulate
|
|
19
|
+
Requires-Dist: tenacity
|
|
20
|
+
Requires-Dist: click
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
aga/__init__.py
|
|
3
|
+
aga/core/__init__.py
|
|
4
|
+
aga/core/ai_engine.py
|
|
5
|
+
aga/core/github_client.py
|
|
6
|
+
aga/modules/__init__.py
|
|
7
|
+
aga/modules/analyzer.py
|
|
8
|
+
aga/modules/readme_gen.py
|
|
9
|
+
aga/modules/uploader.py
|
|
10
|
+
aga/utils/__init__.py
|
|
11
|
+
aga_ai_github_assistant.egg-info/PKG-INFO
|
|
12
|
+
aga_ai_github_assistant.egg-info/SOURCES.txt
|
|
13
|
+
aga_ai_github_assistant.egg-info/dependency_links.txt
|
|
14
|
+
aga_ai_github_assistant.egg-info/entry_points.txt
|
|
15
|
+
aga_ai_github_assistant.egg-info/requires.txt
|
|
16
|
+
aga_ai_github_assistant.egg-info/top_level.txt
|
|
17
|
+
app/main.py
|
|
18
|
+
cli/main.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import html
|
|
2
|
+
import gradio as gr
|
|
3
|
+
import os
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
def sanitize_path(path: str) -> str:
|
|
7
|
+
parts = [p for p in path.replace("\\", "/").split("/") if p and p != ".."]
|
|
8
|
+
return "/".join(parts)
|
|
9
|
+
|
|
10
|
+
from aga.core.github_client import GitHubClient
|
|
11
|
+
from aga.core.ai_engine import AIEngine
|
|
12
|
+
from aga.modules.uploader import ProjectScanner
|
|
13
|
+
from aga.modules.analyzer import RepoAnalyzer
|
|
14
|
+
from aga.modules.readme_gen import ReadmeGenerator
|
|
15
|
+
|
|
16
|
+
load_dotenv()
|
|
17
|
+
|
|
18
|
+
# --- Backend Logic Helpers ---
|
|
19
|
+
def authenticate(token, groq_key):
|
|
20
|
+
try:
|
|
21
|
+
gh = GitHubClient(token)
|
|
22
|
+
ai = AIEngine(groq_key)
|
|
23
|
+
return "Authentication Successful!", gh, ai
|
|
24
|
+
except Exception as e:
|
|
25
|
+
return f"Error: {str(e)}", None, None
|
|
26
|
+
|
|
27
|
+
def manage_repos(token):
|
|
28
|
+
try:
|
|
29
|
+
gh = GitHubClient(token)
|
|
30
|
+
repos = gh.list_repositories()
|
|
31
|
+
names = [r.full_name for r in repos]
|
|
32
|
+
# Added a specific format for the table
|
|
33
|
+
data = [[r.full_name, "Private" if r.private else "Public", r.stargazers_count, r.updated_at.strftime("%Y-%m-%d")] for r in repos]
|
|
34
|
+
return data, gr.update(choices=names)
|
|
35
|
+
except Exception as e:
|
|
36
|
+
return [["Error", str(e), "", ""]], gr.update(choices=[])
|
|
37
|
+
|
|
38
|
+
def get_repo_folders(token, repo_full_name, current_path=""):
|
|
39
|
+
if not repo_full_name:
|
|
40
|
+
return [], current_path, gr.update(choices=[])
|
|
41
|
+
try:
|
|
42
|
+
gh = GitHubClient(token)
|
|
43
|
+
contents = gh.get_contents(repo_full_name, current_path)
|
|
44
|
+
|
|
45
|
+
table_data = []
|
|
46
|
+
file_list = []
|
|
47
|
+
if current_path:
|
|
48
|
+
table_data.append([".. (Go Back)", "Parent Folder", "Enter"])
|
|
49
|
+
|
|
50
|
+
for c in contents:
|
|
51
|
+
item_type = "DIR" if c.type == "dir" else "FILE"
|
|
52
|
+
table_data.append([f"[{item_type}] {c.path.split('/')[-1]}", "Folder" if c.type == "dir" else "File", "Set as Target"])
|
|
53
|
+
if c.type == "file":
|
|
54
|
+
file_list.append(c.path)
|
|
55
|
+
|
|
56
|
+
return table_data, current_path, gr.update(choices=file_list)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
print(f"Explorer Error: {e}")
|
|
59
|
+
return [], current_path, gr.update(choices=[])
|
|
60
|
+
|
|
61
|
+
def fetch_file_content(token, repo_full_name, file_path):
|
|
62
|
+
if not repo_full_name or not file_path:
|
|
63
|
+
return "", "No file selected"
|
|
64
|
+
try:
|
|
65
|
+
gh = GitHubClient(token)
|
|
66
|
+
repo = gh.client.get_repo(repo_full_name)
|
|
67
|
+
content_file = repo.get_contents(file_path)
|
|
68
|
+
decoded_content = content_file.decoded_content.decode("utf-8", errors="ignore")
|
|
69
|
+
|
|
70
|
+
details = f"""
|
|
71
|
+
**File Information:**
|
|
72
|
+
- **Name:** {html.escape(content_file.name)}
|
|
73
|
+
- **Size:** {content_file.size / 1024:.2f} KB
|
|
74
|
+
- **SHA:** `{html.escape(content_file.sha)}`
|
|
75
|
+
- [View on GitHub]({html.escape(content_file.html_url)})
|
|
76
|
+
"""
|
|
77
|
+
return decoded_content, details
|
|
78
|
+
except Exception as e:
|
|
79
|
+
return f"Error fetching file: {str(e)}", f"Error: {str(e)}"
|
|
80
|
+
|
|
81
|
+
def navigate_explorer(token, repo_full_name, evt: gr.SelectData, current_path):
|
|
82
|
+
# evt.index[0] is row, evt.index[1] is column
|
|
83
|
+
row_idx = evt.index[0]
|
|
84
|
+
col_idx = evt.index[1]
|
|
85
|
+
|
|
86
|
+
# We need the data from the table to know what was clicked
|
|
87
|
+
# Since we can't easily get the data here without passing it,
|
|
88
|
+
# we'll use a hack or just re-fetch for now? No, better to have the data passed.
|
|
89
|
+
# Actually, navigate_explorer needs the table data.
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
def handle_explorer_click(token, repo_full_name, current_path, table_data, evt: gr.SelectData):
|
|
93
|
+
# table_data can be a pandas DataFrame or a list
|
|
94
|
+
if table_data is None or repo_full_name is None:
|
|
95
|
+
return table_data, current_path, ""
|
|
96
|
+
|
|
97
|
+
# Convert to list if it's a DataFrame
|
|
98
|
+
import pandas as pd
|
|
99
|
+
if isinstance(table_data, pd.DataFrame):
|
|
100
|
+
data_list = table_data.values.tolist()
|
|
101
|
+
else:
|
|
102
|
+
data_list = table_data
|
|
103
|
+
|
|
104
|
+
if not data_list:
|
|
105
|
+
return table_data, current_path, ""
|
|
106
|
+
|
|
107
|
+
row = data_list[evt.index[0]]
|
|
108
|
+
item_name_with_type = str(row[0])
|
|
109
|
+
item_type = str(row[1])
|
|
110
|
+
|
|
111
|
+
clean_name = item_name_with_type.replace("[DIR] ", "").replace("[FILE] ", "").strip()
|
|
112
|
+
|
|
113
|
+
# Logic for Navigation
|
|
114
|
+
if item_name_with_type == ".. (Go Back)":
|
|
115
|
+
new_path = "/".join(current_path.strip("/").split("/")[:-1])
|
|
116
|
+
new_data, path_state, _ = get_repo_folders(token, repo_full_name, new_path)
|
|
117
|
+
return new_data, path_state, new_path
|
|
118
|
+
|
|
119
|
+
if item_type == "Folder" and evt.index[1] == 0: # Clicked folder name
|
|
120
|
+
new_path = f"{current_path}/{clean_name}".strip("/")
|
|
121
|
+
new_data, path_state, _ = get_repo_folders(token, repo_full_name, new_path)
|
|
122
|
+
return new_data, path_state, new_path
|
|
123
|
+
|
|
124
|
+
# Logic for Selecting Path
|
|
125
|
+
if evt.index[1] == 2: # Clicked "Action" column
|
|
126
|
+
selected_path = f"{current_path}/{clean_name}".strip("/")
|
|
127
|
+
return table_data, current_path, selected_path
|
|
128
|
+
|
|
129
|
+
return table_data, current_path, current_path
|
|
130
|
+
|
|
131
|
+
def handle_file_upload(files, current_path):
|
|
132
|
+
if not files:
|
|
133
|
+
return "", current_path
|
|
134
|
+
|
|
135
|
+
# Store the content of the first file
|
|
136
|
+
file_info = files[0]
|
|
137
|
+
file_path = file_info.name
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
141
|
+
content = f.read()
|
|
142
|
+
except Exception as e:
|
|
143
|
+
return f"Error reading file: {str(e)}", current_path
|
|
144
|
+
|
|
145
|
+
filename = os.path.basename(file_path)
|
|
146
|
+
|
|
147
|
+
# Smart path logic:
|
|
148
|
+
# If current_path is empty, just use filename
|
|
149
|
+
# If current_path is a folder, append filename
|
|
150
|
+
if not current_path:
|
|
151
|
+
new_path = filename
|
|
152
|
+
elif current_path.endswith(filename):
|
|
153
|
+
new_path = current_path # Already has it
|
|
154
|
+
else:
|
|
155
|
+
# Check if it's likely a directory (doesn't have an extension or is a known folder)
|
|
156
|
+
new_path = f"{current_path.rstrip('/')}/{filename}"
|
|
157
|
+
|
|
158
|
+
return content, new_path
|
|
159
|
+
|
|
160
|
+
def handle_push(token, manual_repo, selected_repo, path, content, commit):
|
|
161
|
+
target_repo = manual_repo if manual_repo else selected_repo
|
|
162
|
+
if not target_repo or not path or not content:
|
|
163
|
+
return "<div style='color: #ffa500; font-weight: bold;'>Warning: Missing required fields (Repo, Path, or Content)</div>"
|
|
164
|
+
|
|
165
|
+
safe_path = sanitize_path(path)
|
|
166
|
+
try:
|
|
167
|
+
gh = GitHubClient(token)
|
|
168
|
+
msg = commit if commit else f"Deployed via AGA Platform: {safe_path}"
|
|
169
|
+
result = gh.push_file(target_repo, safe_path, content, msg)
|
|
170
|
+
color = "#00ff00" if "Successfully" in result else "#ff4b4b"
|
|
171
|
+
return f"<div style='color: {color}; font-weight: bold; border: 1px solid {color}; padding: 10px; border-radius: 5px;'>{html.escape(result)}</div>"
|
|
172
|
+
except Exception as e:
|
|
173
|
+
return f"<div style='color: #ff4b4b; font-weight: bold;'>Error: {html.escape(str(e))}</div>"
|
|
174
|
+
|
|
175
|
+
def update_repo_visibility(mode):
|
|
176
|
+
if mode == "Select Existing":
|
|
177
|
+
return gr.update(visible=True), gr.update(visible=False)
|
|
178
|
+
return gr.update(visible=False), gr.update(visible=True)
|
|
179
|
+
|
|
180
|
+
def run_analysis(token, groq_key, url, model):
|
|
181
|
+
if not token or not groq_key:
|
|
182
|
+
return "Missing Credentials", "Please provide BOTH GitHub Token and Groq API Key in the Authentication tab."
|
|
183
|
+
if not url:
|
|
184
|
+
return "Missing URL", "Please enter a valid GitHub repository URL."
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
gh = GitHubClient(token)
|
|
188
|
+
ai = AIEngine(groq_key, model=model)
|
|
189
|
+
analyzer = RepoAnalyzer(gh, ai)
|
|
190
|
+
res = analyzer.analyze(url)
|
|
191
|
+
|
|
192
|
+
score_md = f"### Health Score: {res['scores']['health']} | Security: {res['scores']['security']} | Docs: {res['scores']['documentation']}"
|
|
193
|
+
return score_md, res['report']
|
|
194
|
+
except Exception as e:
|
|
195
|
+
error_msg = str(e)
|
|
196
|
+
if "401" in error_msg or "invalid_api_key" in error_msg:
|
|
197
|
+
return "Authentication Failed", "Your Groq API Key is invalid or expired. Please check the Authentication tab. \n\nGet a new key at: https://console.groq.com/"
|
|
198
|
+
return f"Analysis Failed", error_msg
|
|
199
|
+
|
|
200
|
+
def generate_doc(groq_key, context, style):
|
|
201
|
+
if not groq_key:
|
|
202
|
+
return "Error: Groq API Key is missing. Please provide it in the Authentication tab."
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
ai = AIEngine(groq_key)
|
|
206
|
+
gen = ReadmeGenerator(ai)
|
|
207
|
+
return gen.generate(context, style)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
error_msg = str(e)
|
|
210
|
+
if "401" in error_msg or "invalid_api_key" in error_msg:
|
|
211
|
+
return "Invalid Groq Key. Please update it in the Authentication tab."
|
|
212
|
+
return f"Error: {error_msg}"
|
|
213
|
+
|
|
214
|
+
def handle_repo_creation(token, name, desc, private):
|
|
215
|
+
if not token or not name:
|
|
216
|
+
return "Please provide a repository name and ensure you are authenticated."
|
|
217
|
+
try:
|
|
218
|
+
gh = GitHubClient(token)
|
|
219
|
+
repo = gh.create_repository(name, desc, private)
|
|
220
|
+
return f"Successfully created: **[{repo.full_name}]({repo.html_url})**"
|
|
221
|
+
except Exception as e:
|
|
222
|
+
return f"Error: {str(e)}"
|
|
223
|
+
|
|
224
|
+
# --- UI Design ---
|
|
225
|
+
with gr.Blocks(theme=gr.themes.Soft(), title="AGA - AI GitHub Assistant") as demo:
|
|
226
|
+
# State management
|
|
227
|
+
gh_session = gr.State()
|
|
228
|
+
ai_session = gr.State()
|
|
229
|
+
current_explorer_path = gr.State("") # Track current directory in explorer
|
|
230
|
+
|
|
231
|
+
gr.Markdown("# AI GitHub Assistant (AGA)", elem_id="main-title")
|
|
232
|
+
gr.Markdown("Transforming your GitHub workflow with professional-grade AI Intelligence.")
|
|
233
|
+
|
|
234
|
+
with gr.Tab("Authentication"):
|
|
235
|
+
with gr.Row():
|
|
236
|
+
token_input = gr.Textbox(label="GitHub Personal Access Token", type="password",
|
|
237
|
+
value=os.getenv("GITHUB_TOKEN", ""), placeholder="ghp_...")
|
|
238
|
+
groq_input = gr.Textbox(label="Groq API Key", type="password",
|
|
239
|
+
value=os.getenv("GROQ_API_KEY", ""), placeholder="gsk_...")
|
|
240
|
+
auth_btn = gr.Button("Initialize Platform", variant="primary")
|
|
241
|
+
auth_status = gr.Markdown("Status: Pending Authentication")
|
|
242
|
+
|
|
243
|
+
with gr.Tab("Repo Manager"):
|
|
244
|
+
gr.Markdown("### Repository Control Center")
|
|
245
|
+
with gr.Row():
|
|
246
|
+
refresh_btn = gr.Button("Sync with GitHub", variant="secondary")
|
|
247
|
+
|
|
248
|
+
repo_table = gr.Dataframe(
|
|
249
|
+
headers=["Full Name", "Visibility", "Stars", "Last Updated"],
|
|
250
|
+
interactive=False,
|
|
251
|
+
label="Your Active Repositories (Select a row to explore)",
|
|
252
|
+
datatype=["str", "str", "number", "str"]
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
gr.Markdown("---")
|
|
256
|
+
gr.Markdown("### AI File Deployment Engine")
|
|
257
|
+
|
|
258
|
+
with gr.Group():
|
|
259
|
+
with gr.Row():
|
|
260
|
+
with gr.Column(scale=1):
|
|
261
|
+
gr.Markdown("#### 1. Target Repository")
|
|
262
|
+
repo_mode = gr.Radio(["Select Existing", "Manual Entry"], label="Input Method", value="Select Existing")
|
|
263
|
+
repo_select = gr.Dropdown(label="Selected Repo", choices=[], visible=True, interactive=True)
|
|
264
|
+
manual_repo_name = gr.Textbox(label="Repo Identifier (owner/repo)", placeholder="e.g. username/repo", visible=False)
|
|
265
|
+
|
|
266
|
+
with gr.Column(scale=1):
|
|
267
|
+
gr.Markdown("#### 2. Navigation & Path")
|
|
268
|
+
explorer_table = gr.Dataframe(
|
|
269
|
+
headers=["Name", "Type", "Action"],
|
|
270
|
+
interactive=False,
|
|
271
|
+
label="File Explorer",
|
|
272
|
+
wrap=True
|
|
273
|
+
)
|
|
274
|
+
file_viewer_dropdown = gr.Dropdown(label="Quick View File Details", choices=[], interactive=True)
|
|
275
|
+
file_details_box = gr.Markdown("Select a file above to view metadata.")
|
|
276
|
+
target_path = gr.Textbox(label="Final Push Path", placeholder="e.g. folder/file.py")
|
|
277
|
+
|
|
278
|
+
with gr.Column(scale=1):
|
|
279
|
+
gr.Markdown("#### 3. Deployment Settings")
|
|
280
|
+
file_uploader = gr.File(label="Upload File", file_count="multiple")
|
|
281
|
+
commit_msg = gr.Textbox(label="Commit Message", placeholder="Feat: Add new module via AGA")
|
|
282
|
+
push_btn = gr.Button("Deploy Code to GitHub", variant="primary", size="lg")
|
|
283
|
+
|
|
284
|
+
with gr.Row():
|
|
285
|
+
with gr.Column():
|
|
286
|
+
file_content = gr.Code(label="Code / File Content", language="python", lines=15)
|
|
287
|
+
|
|
288
|
+
push_status = gr.HTML("<div style='text-align: center; padding: 10px; border-radius: 5px; background: #2d2d2d; border: 1px solid #444;'>Status: Waiting for deployment...</div>")
|
|
289
|
+
|
|
290
|
+
with gr.Tab("Repo Creator"):
|
|
291
|
+
gr.Markdown("### Create New Repository")
|
|
292
|
+
with gr.Row():
|
|
293
|
+
new_repo_name = gr.Textbox(label="Repository Name", placeholder="my-awesome-project")
|
|
294
|
+
is_private = gr.Checkbox(label="Private Repository", value=True)
|
|
295
|
+
new_repo_desc = gr.Textbox(label="Description (Optional)")
|
|
296
|
+
create_btn = gr.Button("Create Repository on GitHub", variant="primary")
|
|
297
|
+
create_status = gr.Markdown("")
|
|
298
|
+
|
|
299
|
+
with gr.Tab("Deep Analyzer"):
|
|
300
|
+
gr.Markdown("### Repository Health & Architecture Scan")
|
|
301
|
+
with gr.Row():
|
|
302
|
+
repo_url = gr.Textbox(label="Enter GitHub Repository URL", placeholder="https://github.com/username/repo", scale=4)
|
|
303
|
+
model_choice = gr.Dropdown(
|
|
304
|
+
choices=["llama-3.3-70b-versatile", "llama-3.1-70b-versatile", "mixtral-8x7b-32768"],
|
|
305
|
+
label="AI Model",
|
|
306
|
+
value="llama-3.3-70b-versatile",
|
|
307
|
+
scale=1
|
|
308
|
+
)
|
|
309
|
+
analyze_btn = gr.Button("Start Deep Analysis", variant="primary")
|
|
310
|
+
score_display = gr.Markdown("### Status: Waiting for input")
|
|
311
|
+
report_display = gr.Textbox(label="Detailed AI Architectural Report", lines=20)
|
|
312
|
+
|
|
313
|
+
with gr.Tab("Smart README"):
|
|
314
|
+
gr.Markdown("### AI-Powered Documentation")
|
|
315
|
+
proj_context = gr.Textbox(label="Project Details / File Structure", placeholder="Paste tree structure or summary here...", lines=8)
|
|
316
|
+
style_choice = gr.Dropdown(choices=["professional", "open_source", "startup", "hackathon", "minimal"],
|
|
317
|
+
label="Readme Style Profile", value="professional")
|
|
318
|
+
gen_btn = gr.Button("Generate Professional README", variant="primary")
|
|
319
|
+
readme_output = gr.Markdown("---")
|
|
320
|
+
|
|
321
|
+
gr.Markdown("---")
|
|
322
|
+
gr.Markdown("AGA Platform v1.2 | Powered by Llama 3.3 Intelligence", elem_id="footer")
|
|
323
|
+
|
|
324
|
+
# --- Event Handlers ---
|
|
325
|
+
auth_btn.click(
|
|
326
|
+
authenticate,
|
|
327
|
+
inputs=[token_input, groq_input],
|
|
328
|
+
outputs=[auth_status, gh_session, ai_session]
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
refresh_btn.click(
|
|
332
|
+
manage_repos,
|
|
333
|
+
inputs=[token_input],
|
|
334
|
+
outputs=[repo_table, repo_select]
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Explorer Logic
|
|
338
|
+
repo_mode.change(
|
|
339
|
+
update_repo_visibility,
|
|
340
|
+
inputs=[repo_mode],
|
|
341
|
+
outputs=[repo_select, manual_repo_name]
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Click a repo in the list to trigger exploration
|
|
345
|
+
def select_repo_from_table(evt: gr.SelectData, token, table_data):
|
|
346
|
+
import pandas as pd
|
|
347
|
+
if isinstance(table_data, pd.DataFrame):
|
|
348
|
+
data_list = table_data.values.tolist()
|
|
349
|
+
else:
|
|
350
|
+
data_list = table_data
|
|
351
|
+
|
|
352
|
+
repo_name = data_list[evt.index[0]][0] # Always col 0 for full_name
|
|
353
|
+
# Trigger folder fetch
|
|
354
|
+
data, path, file_choices = get_repo_folders(token, repo_name, "")
|
|
355
|
+
return repo_name, data, path, file_choices
|
|
356
|
+
|
|
357
|
+
repo_table.select(
|
|
358
|
+
select_repo_from_table,
|
|
359
|
+
inputs=[token_input, repo_table],
|
|
360
|
+
outputs=[repo_select, explorer_table, current_explorer_path, file_viewer_dropdown]
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
repo_select.change(
|
|
364
|
+
get_repo_folders,
|
|
365
|
+
inputs=[token_input, repo_select, current_explorer_path],
|
|
366
|
+
outputs=[explorer_table, current_explorer_path, file_viewer_dropdown]
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
manual_repo_name.submit(
|
|
370
|
+
get_repo_folders,
|
|
371
|
+
inputs=[token_input, manual_repo_name, current_explorer_path],
|
|
372
|
+
outputs=[explorer_table, current_explorer_path, file_viewer_dropdown]
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def handle_explorer_click_with_dropdown(token, repo_full_name, current_path, table_data, evt: gr.SelectData):
|
|
376
|
+
# Call the old handler logic but return the dropdown update too
|
|
377
|
+
table, path, target = handle_explorer_click(token, repo_full_name, current_path, table_data, evt)
|
|
378
|
+
# Refresh dropdown for new path
|
|
379
|
+
_, _, dropdown_update = get_repo_folders(token, repo_full_name, path)
|
|
380
|
+
return table, path, target, dropdown_update
|
|
381
|
+
|
|
382
|
+
explorer_table.select(
|
|
383
|
+
handle_explorer_click_with_dropdown,
|
|
384
|
+
inputs=[token_input, repo_select, current_explorer_path, explorer_table],
|
|
385
|
+
outputs=[explorer_table, current_explorer_path, target_path, file_viewer_dropdown]
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
file_viewer_dropdown.change(
|
|
389
|
+
fetch_file_content,
|
|
390
|
+
inputs=[token_input, repo_select, file_viewer_dropdown],
|
|
391
|
+
outputs=[file_content, file_details_box]
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
file_uploader.change(
|
|
395
|
+
handle_file_upload,
|
|
396
|
+
inputs=[file_uploader, target_path],
|
|
397
|
+
outputs=[file_content, target_path]
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
push_btn.click(
|
|
401
|
+
handle_push,
|
|
402
|
+
inputs=[token_input, manual_repo_name, repo_select, target_path, file_content, commit_msg],
|
|
403
|
+
outputs=[push_status]
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
analyze_btn.click(
|
|
407
|
+
run_analysis,
|
|
408
|
+
inputs=[token_input, groq_input, repo_url, model_choice],
|
|
409
|
+
outputs=[score_display, report_display]
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
gen_btn.click(
|
|
413
|
+
generate_doc,
|
|
414
|
+
inputs=[groq_input, proj_context, style_choice],
|
|
415
|
+
outputs=[readme_output]
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
create_btn.click(
|
|
419
|
+
handle_repo_creation,
|
|
420
|
+
inputs=[token_input, new_repo_name, new_repo_desc, is_private],
|
|
421
|
+
outputs=[create_status]
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
if __name__ == "__main__":
|
|
425
|
+
demo.launch(server_port=int(os.getenv("PORT", 7860)) ,share=True)
|
|
426
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import os
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
from aga.core.github_client import GitHubClient
|
|
5
|
+
from aga.core.ai_engine import AIEngine
|
|
6
|
+
from aga.modules.analyzer import RepoAnalyzer
|
|
7
|
+
|
|
8
|
+
load_dotenv()
|
|
9
|
+
|
|
10
|
+
@click.group()
|
|
11
|
+
def cli():
|
|
12
|
+
"""AGA - AI GitHub Assistant CLI Tool"""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
@cli.command()
|
|
16
|
+
@click.argument('name')
|
|
17
|
+
@click.option('--description', default="Created via AGA CLI", help="Repo description")
|
|
18
|
+
@click.option('--public', is_flag=True, help="Make repository public")
|
|
19
|
+
def create_repo(name, description, public):
|
|
20
|
+
"""Create a new GitHub repository."""
|
|
21
|
+
token = os.getenv("GITHUB_TOKEN")
|
|
22
|
+
if not token:
|
|
23
|
+
click.echo("Error: GITHUB_TOKEN not found in .env")
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
client = GitHubClient(token)
|
|
27
|
+
repo = client.create_repository(name, description, private=not public)
|
|
28
|
+
click.echo(f"🚀 Repository created successfully: {repo.html_url}")
|
|
29
|
+
|
|
30
|
+
@cli.command()
|
|
31
|
+
@click.argument('url')
|
|
32
|
+
def analyze(url):
|
|
33
|
+
"""Deeply analyze a GitHub repository."""
|
|
34
|
+
token = os.getenv("GITHUB_TOKEN")
|
|
35
|
+
groq_key = os.getenv("GROQ_API_KEY")
|
|
36
|
+
|
|
37
|
+
if not token or not groq_key:
|
|
38
|
+
click.echo("Error: GITHUB_TOKEN or GROQ_API_KEY not found in .env")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
client = GitHubClient(token)
|
|
42
|
+
ai = AIEngine(groq_key)
|
|
43
|
+
analyzer = RepoAnalyzer(client, ai)
|
|
44
|
+
|
|
45
|
+
click.echo(f"🔍 Analyzing {url}...")
|
|
46
|
+
result = analyzer.analyze(url)
|
|
47
|
+
|
|
48
|
+
click.echo("\n--- Health Scores ---")
|
|
49
|
+
for category, score in result['scores'].items():
|
|
50
|
+
click.echo(f"{category.capitalize()}: {score}/100")
|
|
51
|
+
|
|
52
|
+
click.echo("\n--- AI Analysis Summary ---")
|
|
53
|
+
click.echo(result['report'][:500] + "...")
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
cli()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aga-ai-github-assistant"
|
|
7
|
+
version = "1.2.0"
|
|
8
|
+
description = "AI-powered GitHub assistant with file deployment, repo analysis, and README generation."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
dependencies = [
|
|
13
|
+
"gradio>=4.0.0",
|
|
14
|
+
"fastapi",
|
|
15
|
+
"uvicorn",
|
|
16
|
+
"langchain",
|
|
17
|
+
"langchain-groq",
|
|
18
|
+
"langchain-core",
|
|
19
|
+
"PyGithub",
|
|
20
|
+
"python-dotenv",
|
|
21
|
+
"httpx",
|
|
22
|
+
"fpdf2",
|
|
23
|
+
"tabulate",
|
|
24
|
+
"tenacity",
|
|
25
|
+
"click",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
aga = "cli.main:main"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["."]
|
|
33
|
+
include = ["aga*", "app*", "cli*"]
|