aga-ai-github-assistant 1.2.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.
aga/__init__.py ADDED
File without changes
aga/core/__init__.py ADDED
File without changes
aga/core/ai_engine.py ADDED
@@ -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)
aga/utils/__init__.py ADDED
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,16 @@
1
+ aga/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ aga/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ aga/core/ai_engine.py,sha256=EAECw_KQ8kcXT_hSWqpsu76ScMSw2h76IhyYPc4wQkE,2175
4
+ aga/core/github_client.py,sha256=hDjuMGI7MFCOs9SfMM_Xq_enXXIESAqoioB26ypeQC4,4308
5
+ aga/modules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ aga/modules/analyzer.py,sha256=Mr2b3I3FUUqlSYjU4Ujsv24SPiHU6a3ImQnb6DL9SbA,2171
7
+ aga/modules/readme_gen.py,sha256=GtDxtDPPuut4JXIx1xi-Zn6fSK0C2zLPOmH-7Q1496o,1114
8
+ aga/modules/uploader.py,sha256=G30FVjKKyqeJlnEZl0bMwnU49vMxTmmJx-1px9Zc63k,2308
9
+ aga/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ app/main.py,sha256=1quVapqRRsYsWi1qx1IZYJ_zFeq_1lOiel94IDUiFW0,18160
11
+ cli/main.py,sha256=YAqna4S3cGX2G1QsgL8ssNY-7jugPIAPzI337_hCelI,1752
12
+ aga_ai_github_assistant-1.2.0.dist-info/METADATA,sha256=-Lz8QKYtQixU-5ixwYpMeCQi1Lr4pbYtQ-P_vhTMfOc,584
13
+ aga_ai_github_assistant-1.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
14
+ aga_ai_github_assistant-1.2.0.dist-info/entry_points.txt,sha256=_GtHAIZQ5FUcFUX8qBab3CyEyAuUpn6MIWuJEshHHp4,38
15
+ aga_ai_github_assistant-1.2.0.dist-info/top_level.txt,sha256=CF-PPNNIqU18aow8zUJ8EsG3A_lNrkKVWNGIbe7ixvU,12
16
+ aga_ai_github_assistant-1.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aga = cli.main:main
@@ -0,0 +1,3 @@
1
+ aga
2
+ app
3
+ cli
app/main.py ADDED
@@ -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
+
cli/main.py ADDED
@@ -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()