Upload
Browse files- .gitignore +1 -0
- .well-known/mcp.yaml +3 -0
- README.md +183 -4
- app.py +144 -0
- ask_agent.py +62 -0
- doc_generator.py +141 -0
- index.md +10 -0
- readme_generator.py +111 -0
- requirements.txt +11 -0
.gitignore
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
.env
|
.well-known/mcp.yaml
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
schema_version: 1
|
2 |
+
type: mcp_server
|
3 |
+
mcp_version: 0.1
|
README.md
CHANGED
@@ -1,14 +1,193 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: gradio
|
7 |
sdk_version: 5.33.1
|
8 |
app_file: app.py
|
9 |
pinned: false
|
10 |
license: mit
|
11 |
short_description: Automatic documentation generator for GitHub or zipped repos
|
|
|
|
|
|
|
|
|
12 |
---
|
13 |
|
14 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
---
|
2 |
+
title: Agents MCP Hackathon
|
3 |
+
emoji: π
|
4 |
+
colorFrom: indigo
|
5 |
+
colorTo: red
|
6 |
sdk: gradio
|
7 |
sdk_version: 5.33.1
|
8 |
app_file: app.py
|
9 |
pinned: false
|
10 |
license: mit
|
11 |
short_description: Automatic documentation generator for GitHub or zipped repos
|
12 |
+
tags:
|
13 |
+
- mcp-server-track
|
14 |
+
- gradio-app
|
15 |
+
- hackathon
|
16 |
---
|
17 |
|
18 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
19 |
+
|
20 |
+
# π€ AutoDocs β MCP Server for Automatic Code Documentation
|
21 |
+
|
22 |
+
Automatic documentation generator for GitHub or zipped repositories.
|
23 |
+
|
24 |
+
**AutoDocs** is a Gradio-based application that serves as an **MCP Server (Track 1)** for the Agents & MCP Hackathon.
|
25 |
+
It automatically generates documentation, README files, and requirements.txt for any Python code repository provided via GitHub URL or ZIP file.
|
26 |
+
|
27 |
+
---
|
28 |
+
|
29 |
+
## π Features
|
30 |
+
|
31 |
+
- π¦ Upload and process any ZIP file of a code repo.
|
32 |
+
- π Clone and process any GitHub repository.
|
33 |
+
- π Auto-generate:
|
34 |
+
- Docstrings (Google style)
|
35 |
+
- Typings
|
36 |
+
- Inline code comments
|
37 |
+
- `requirements.txt`
|
38 |
+
- `README.md` + `index.md`
|
39 |
+
- π§ Integrated AI agent to ask questions about the code.
|
40 |
+
|
41 |
+
---
|
42 |
+
|
43 |
+
## π οΈ MCP Server Information
|
44 |
+
|
45 |
+
β
This Space is an **MCP Server (Track 1)** compliant with the MCP protocol.
|
46 |
+
|
47 |
+
MCP Metadata (`.well-known/mcp.yaml`)
|
48 |
+
|
49 |
+
## π» Usage (as MCP Client)
|
50 |
+
|
51 |
+
This server can be queried via Claude Desktop, Cursor, or Tiny Agents MCP clients.
|
52 |
+
|
53 |
+
Example with Tiny Agents:
|
54 |
+
|
55 |
+
```bash
|
56 |
+
tiny-agents call --url https://huggingface.co/spaces/your-space-name
|
57 |
+
```
|
58 |
+
|
59 |
+
## Project Description
|
60 |
+
|
61 |
+
AutoDocs is a tool designed to automatically generate documentation, requirements files, and README files for Python projects. It leverages generative AI to add helpful comments and type annotations to your code, making it easier to understand and maintain. It can process a local repository, a GitHub repository via URL, or a zipped source code directory.
|
62 |
+
|
63 |
+
## Installation
|
64 |
+
|
65 |
+
1. **Clone the repository:**
|
66 |
+
|
67 |
+
```bash
|
68 |
+
git clone <repository_url>
|
69 |
+
cd <repository_name>
|
70 |
+
```
|
71 |
+
|
72 |
+
2. **Create a virtual environment (recommended):**
|
73 |
+
|
74 |
+
```bash
|
75 |
+
python3 -m venv venv
|
76 |
+
source venv/bin/activate # On Linux/macOS
|
77 |
+
venv\Scripts\activate # On Windows
|
78 |
+
```
|
79 |
+
|
80 |
+
3. **Install the dependencies:**
|
81 |
+
|
82 |
+
```bash
|
83 |
+
pip install -r requirements.txt
|
84 |
+
```
|
85 |
+
|
86 |
+
4. **Set up the environment variables:**
|
87 |
+
|
88 |
+
* Create a `.env` file in the root directory of the project.
|
89 |
+
* Add your Google Gemini API key to the `.env` file:
|
90 |
+
|
91 |
+
```
|
92 |
+
GOOGLE_API_KEY=<your_google_api_key>
|
93 |
+
```
|
94 |
+
|
95 |
+
**Note:** You will need a Google Gemini API key to use the documentation generation features. You can obtain one from the Google AI Studio.
|
96 |
+
|
97 |
+
## Usage
|
98 |
+
|
99 |
+
### Using the `app.py` module:
|
100 |
+
|
101 |
+
The `app.py` module contains the core logic for processing a repository and generating documentation.
|
102 |
+
|
103 |
+
```python
|
104 |
+
import gradio as gr
|
105 |
+
import os
|
106 |
+
import shutil
|
107 |
+
import tempfile
|
108 |
+
import zipfile
|
109 |
+
import subprocess
|
110 |
+
import uuid
|
111 |
+
|
112 |
+
from doc_generator import generate_documented_code, generate_requirements_txt
|
113 |
+
from readme_generator import generate_readme_from_zip
|
114 |
+
|
115 |
+
|
116 |
+
def process_repo(repo_path: str, zip_output_name: str = "AutoDocs") -> str:
|
117 |
+
"""Processes a repository to generate documentation, requirements, and a README.
|
118 |
+
|
119 |
+
Args:
|
120 |
+
repo_path: The path to the repository.
|
121 |
+
zip_output_name: The name of the output zip file (default: "AutoDocs").
|
122 |
+
|
123 |
+
Returns:
|
124 |
+
The path to the generated zip file.
|
125 |
+
"""
|
126 |
+
with tempfile.TemporaryDirectory() as temp_output_dir:
|
127 |
+
# Iterate through all Python files in the repository and generate documented code.
|
128 |
+
for root, _, files in os.walk(repo_path):
|
129 |
+
for file in files:
|
130 |
+
if file.endswith(".py"):
|
131 |
+
file_path = os.path.join(root, file)
|
132 |
+
generate_documented_code(file_path, file_path)
|
133 |
+
|
134 |
+
|
135 |
+
# Example Usage (not executable directly from this file, intended for integration):
|
136 |
+
# repo_path = "/path/to/your/repository"
|
137 |
+
# output_zip = process_repo(repo_path)
|
138 |
+
# print(f"Generated documentation zip file: {output_zip}")
|
139 |
+
```
|
140 |
+
|
141 |
+
### Using the FastAPI server (`mcp_server.py`):
|
142 |
+
|
143 |
+
The `mcp_server.py` module provides a FastAPI server with endpoints for generating documentation from a GitHub URL or a zip file upload.
|
144 |
+
|
145 |
+
1. **Run the FastAPI server:**
|
146 |
+
|
147 |
+
```bash
|
148 |
+
uvicorn mcp_server:app --reload
|
149 |
+
```
|
150 |
+
|
151 |
+
2. **Access the endpoints:**
|
152 |
+
|
153 |
+
* **Generate documentation from a GitHub URL:**
|
154 |
+
|
155 |
+
```
|
156 |
+
POST /generate_docs
|
157 |
+
Content-Type: multipart/form-data
|
158 |
+
|
159 |
+
github_url=<your_github_url>
|
160 |
+
```
|
161 |
+
|
162 |
+
* **Generate documentation from a zip file upload:**
|
163 |
+
|
164 |
+
```
|
165 |
+
POST /generate_docs
|
166 |
+
Content-Type: multipart/form-data
|
167 |
+
|
168 |
+
zip_file=@<path_to_your_zip_file>
|
169 |
+
```
|
170 |
+
|
171 |
+
* **MCP Manifest (/.well-known/mcp.yaml):**
|
172 |
+
|
173 |
+
```
|
174 |
+
GET /.well-known/mcp.yaml
|
175 |
+
```
|
176 |
+
This endpoint serves the MCP manifest file.
|
177 |
+
|
178 |
+
## Features
|
179 |
+
|
180 |
+
* **Automated Documentation Generation:** Uses generative AI to add comments and type annotations to Python code.
|
181 |
+
* **Requirements File Generation:** Automatically creates a `requirements.txt` file listing the project dependencies.
|
182 |
+
* **README Generation:** Generates a basic README file based on the project structure and code content.
|
183 |
+
* **GitHub URL Processing:** Can process repositories directly from GitHub URLs.
|
184 |
+
* **Zip File Upload:** Supports uploading zip files of source code for documentation generation.
|
185 |
+
* **MCP Manifest Serving:** Includes an endpoint to serve an MCP (Meta Control Protocol) manifest.
|
186 |
+
|
187 |
+
## Authors
|
188 |
+
|
189 |
+
Aguet Theau, Azdad Bilal.
|
190 |
+
|
191 |
+
## License
|
192 |
+
|
193 |
+
MIT License.
|
app.py
ADDED
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import os
|
3 |
+
import shutil
|
4 |
+
import tempfile
|
5 |
+
import zipfile
|
6 |
+
import subprocess
|
7 |
+
import uuid
|
8 |
+
|
9 |
+
from ask_agent import ask_agent
|
10 |
+
from doc_generator import generate_documented_code, generate_requirements_txt
|
11 |
+
from readme_generator import generate_readme_from_zip
|
12 |
+
|
13 |
+
last_processed_repo_path = ""
|
14 |
+
|
15 |
+
def process_repo(repo_path, zip_output_name="AutoDocs"):
|
16 |
+
with tempfile.TemporaryDirectory() as temp_output_dir:
|
17 |
+
# Document .py files
|
18 |
+
for root, _, files in os.walk(repo_path):
|
19 |
+
for file in files:
|
20 |
+
if file.endswith(".py"):
|
21 |
+
file_path = os.path.join(root, file)
|
22 |
+
generate_documented_code(file_path, file_path)
|
23 |
+
|
24 |
+
# requirements.txt
|
25 |
+
requirements_path = os.path.join(repo_path, "requirements.txt")
|
26 |
+
generate_requirements_txt(repo_path, requirements_path)
|
27 |
+
|
28 |
+
# Create a temporary .zip for README/index
|
29 |
+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_zip:
|
30 |
+
zip_path = tmp_zip.name
|
31 |
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
|
32 |
+
for root, _, files in os.walk(repo_path):
|
33 |
+
for file in files:
|
34 |
+
full_path = os.path.join(root, file)
|
35 |
+
rel_path = os.path.relpath(full_path, repo_path)
|
36 |
+
zipf.write(full_path, rel_path)
|
37 |
+
|
38 |
+
# README + index.md
|
39 |
+
readme_path, index_path = generate_readme_from_zip(zip_path, temp_output_dir)
|
40 |
+
|
41 |
+
# Copy the processed repo
|
42 |
+
for item in os.listdir(repo_path):
|
43 |
+
s = os.path.join(repo_path, item)
|
44 |
+
d = os.path.join(temp_output_dir, item)
|
45 |
+
if os.path.isdir(s):
|
46 |
+
shutil.copytree(s, d, dirs_exist_ok=True)
|
47 |
+
else:
|
48 |
+
shutil.copy2(s, d)
|
49 |
+
|
50 |
+
dest_readme = os.path.join(temp_output_dir, "README.md")
|
51 |
+
dest_index = os.path.join(temp_output_dir, "index.md")
|
52 |
+
|
53 |
+
if os.path.abspath(readme_path) != os.path.abspath(dest_readme):
|
54 |
+
shutil.copy2(readme_path, dest_readme)
|
55 |
+
if os.path.abspath(index_path) != os.path.abspath(dest_index):
|
56 |
+
shutil.copy2(index_path, dest_index)
|
57 |
+
|
58 |
+
# Output zip file with consistent name
|
59 |
+
output_zip_path = os.path.join(
|
60 |
+
tempfile.gettempdir(), f"{zip_output_name}.zip"
|
61 |
+
)
|
62 |
+
with zipfile.ZipFile(output_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
63 |
+
for root, _, files in os.walk(temp_output_dir):
|
64 |
+
for file in files:
|
65 |
+
full_path = os.path.join(root, file)
|
66 |
+
arcname = os.path.relpath(full_path, temp_output_dir)
|
67 |
+
zipf.write(full_path, arcname)
|
68 |
+
global last_processed_repo_path
|
69 |
+
last_processed_repo_path = output_zip_path
|
70 |
+
return output_zip_path
|
71 |
+
|
72 |
+
def process_zip_upload(uploaded_zip_file):
|
73 |
+
zip_path = uploaded_zip_file.name
|
74 |
+
zip_name = os.path.splitext(os.path.basename(zip_path))[0] # e.g., my_project.zip β my_project
|
75 |
+
|
76 |
+
with tempfile.TemporaryDirectory() as temp_input_dir:
|
77 |
+
input_zip_path = os.path.join(temp_input_dir, "input_repo.zip")
|
78 |
+
shutil.copy(zip_path, input_zip_path)
|
79 |
+
with zipfile.ZipFile(input_zip_path, "r") as zip_ref:
|
80 |
+
zip_ref.extractall(temp_input_dir)
|
81 |
+
|
82 |
+
extracted_dirs = [d for d in os.listdir(temp_input_dir) if os.path.isdir(os.path.join(temp_input_dir, d))]
|
83 |
+
repo_root = os.path.join(temp_input_dir, extracted_dirs[0]) if extracted_dirs else temp_input_dir
|
84 |
+
|
85 |
+
return process_repo(repo_root, zip_name)
|
86 |
+
|
87 |
+
def process_github_clone(github_url):
|
88 |
+
with tempfile.TemporaryDirectory() as clone_dir:
|
89 |
+
try:
|
90 |
+
subprocess.check_call(["git", "clone", github_url, clone_dir])
|
91 |
+
return process_repo(clone_dir)
|
92 |
+
except subprocess.CalledProcessError:
|
93 |
+
return "β Error cloning the GitHub repository. Please check the URL."
|
94 |
+
|
95 |
+
# Wrapper for process_zip_upload that also returns the path for the state
|
96 |
+
def process_zip_and_update_state(uploaded_zip_file):
|
97 |
+
zip_path = process_zip_upload(uploaded_zip_file)
|
98 |
+
return zip_path, zip_path # (output for gr.File, output for gr.State)
|
99 |
+
|
100 |
+
# Wrapper for process_github_clone as well
|
101 |
+
def process_git_and_update_state(github_url):
|
102 |
+
zip_path = process_github_clone(github_url)
|
103 |
+
return zip_path, zip_path
|
104 |
+
|
105 |
+
# Gradio user interface
|
106 |
+
with gr.Blocks() as demo:
|
107 |
+
gr.Markdown("# π€ AutoDocs β Smart Documentation Generator")
|
108 |
+
last_processed_repo_path_state = gr.State(value="")
|
109 |
+
with gr.Tab("π¦ Upload .zip"):
|
110 |
+
zip_file_input = gr.File(label="Drop your repo .zip file here", file_types=['.zip'])
|
111 |
+
generate_btn_zip = gr.Button("π Generate from ZIP")
|
112 |
+
output_zip_zip = gr.File(label="β¬οΈ Download your documented repo")
|
113 |
+
|
114 |
+
with gr.Tab("π GitHub URL"):
|
115 |
+
github_url_input = gr.Text(label="Link to GitHub repository", placeholder="https://github.com/user/repo.git")
|
116 |
+
generate_btn_git = gr.Button("π Generate from GitHub")
|
117 |
+
output_zip_git = gr.File(label="β¬οΈ Download your documented repo")
|
118 |
+
|
119 |
+
with gr.Tab("π§ Ask the agent about the repo"):
|
120 |
+
chatbot = gr.Chatbot()
|
121 |
+
user_input = gr.Textbox(placeholder="Ask your question here...")
|
122 |
+
send_btn = gr.Button("Send")
|
123 |
+
|
124 |
+
send_btn.click(
|
125 |
+
fn=ask_agent,
|
126 |
+
inputs=[chatbot, user_input, last_processed_repo_path_state],
|
127 |
+
outputs=[chatbot, user_input]
|
128 |
+
)
|
129 |
+
|
130 |
+
generate_btn_zip.click(
|
131 |
+
fn=process_zip_and_update_state,
|
132 |
+
inputs=[zip_file_input],
|
133 |
+
outputs=[output_zip_zip, last_processed_repo_path_state]
|
134 |
+
)
|
135 |
+
|
136 |
+
generate_btn_git.click(
|
137 |
+
fn=process_git_and_update_state,
|
138 |
+
inputs=[github_url_input],
|
139 |
+
outputs=[output_zip_git, last_processed_repo_path_state]
|
140 |
+
)
|
141 |
+
|
142 |
+
if __name__ == "__main__":
|
143 |
+
demo.queue()
|
144 |
+
demo.launch()
|
ask_agent.py
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import tempfile
|
3 |
+
import zipfile
|
4 |
+
import google.generativeai as genai
|
5 |
+
from dotenv import load_dotenv
|
6 |
+
load_dotenv()
|
7 |
+
|
8 |
+
API_KEY = os.getenv("GOOGLE_API_KEY")
|
9 |
+
genai.configure(api_key=API_KEY)
|
10 |
+
model = genai.GenerativeModel("models/gemini-2.0-flash")
|
11 |
+
chat_session = model.start_chat(history=[])
|
12 |
+
|
13 |
+
def ask_agent(history, message, last_processed_repo_path):
|
14 |
+
|
15 |
+
if not last_processed_repo_path or not os.path.exists(last_processed_repo_path):
|
16 |
+
return history, "π No repository has been processed yet. Please generate documentation first."
|
17 |
+
|
18 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
19 |
+
with zipfile.ZipFile(last_processed_repo_path, 'r') as zip_ref:
|
20 |
+
zip_ref.extractall(tmpdir)
|
21 |
+
|
22 |
+
# Extensions for docs and code to consider
|
23 |
+
extensions_docs = [".md", ".txt"]
|
24 |
+
extensions_code = [".py", ".js", ".java", ".ts", ".cpp", ".c", ".cs", ".go", ".rb", ".swift", ".php"]
|
25 |
+
|
26 |
+
all_files = []
|
27 |
+
for root, _, files in os.walk(tmpdir):
|
28 |
+
for file in files:
|
29 |
+
ext = os.path.splitext(file)[1].lower()
|
30 |
+
if ext in extensions_docs or ext in extensions_code:
|
31 |
+
all_files.append(os.path.join(root, file))
|
32 |
+
|
33 |
+
if not all_files:
|
34 |
+
return history, "π No documentation or code files found in the generated zip."
|
35 |
+
|
36 |
+
# Read and concatenate content
|
37 |
+
docs_and_code_content = ""
|
38 |
+
for file_path in all_files:
|
39 |
+
try:
|
40 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
41 |
+
file_content = f.read()
|
42 |
+
rel_path = os.path.relpath(file_path, tmpdir)
|
43 |
+
docs_and_code_content += f"\n\n===== File: {rel_path} =====\n\n"
|
44 |
+
docs_and_code_content += file_content
|
45 |
+
except Exception as e:
|
46 |
+
docs_and_code_content += f"\n\n===== Error reading file {file_path}: {str(e)} =====\n\n"
|
47 |
+
|
48 |
+
prompt = (
|
49 |
+
f"Here is the content of the project (documentation and code):\n\n{docs_and_code_content}\n\n"
|
50 |
+
f"Question: {message}\n\nPlease respond clearly and precisely."
|
51 |
+
)
|
52 |
+
|
53 |
+
try:
|
54 |
+
response = chat_session.send_message(prompt)
|
55 |
+
answer = response.text
|
56 |
+
except Exception as e:
|
57 |
+
answer = f"β Error when calling Gemini: {str(e)}"
|
58 |
+
|
59 |
+
history = history or []
|
60 |
+
history.append((message, answer))
|
61 |
+
|
62 |
+
return history, ""
|
doc_generator.py
ADDED
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import google.generativeai as genai
|
2 |
+
import re
|
3 |
+
import os
|
4 |
+
import ast
|
5 |
+
from dotenv import load_dotenv
|
6 |
+
import sys
|
7 |
+
import importlib.util
|
8 |
+
|
9 |
+
load_dotenv()
|
10 |
+
|
11 |
+
API_KEY = os.getenv("GOOGLE_API_KEY")
|
12 |
+
if API_KEY is None:
|
13 |
+
raise ValueError("β οΈ The API key MY_API_KEY is missing! Check the Secrets in Hugging Face.")
|
14 |
+
genai.configure(api_key=API_KEY)
|
15 |
+
model = genai.GenerativeModel("models/gemini-2.0-flash")
|
16 |
+
|
17 |
+
PROMPT = """You are an expert programming assistant.
|
18 |
+
For the following code, perform the following actions:
|
19 |
+
- The code must remain exactly the same
|
20 |
+
- Add clear comments for each important step.
|
21 |
+
- Rename variables if it makes the code easier to understand.
|
22 |
+
- Add type annotations if the language supports it.
|
23 |
+
- For each function, add a Google-style docstring (or equivalent format depending on the language).
|
24 |
+
|
25 |
+
Respond only with the updated code, no explanation.
|
26 |
+
Here is the code:
|
27 |
+
|
28 |
+
{code}
|
29 |
+
"""
|
30 |
+
|
31 |
+
def generate_documented_code(input_path: str, output_path: str) -> str:
|
32 |
+
"""
|
33 |
+
Generate a documented version of the code from the given input file and save it to the output file.
|
34 |
+
|
35 |
+
Args:
|
36 |
+
input_path (str): Path to the original code file.
|
37 |
+
output_path (str): Path where the documented code will be saved.
|
38 |
+
|
39 |
+
Returns:
|
40 |
+
str: The updated and documented code.
|
41 |
+
"""
|
42 |
+
with open(input_path, "r", encoding="utf-8") as f:
|
43 |
+
original_code = f.read()
|
44 |
+
|
45 |
+
prompt = PROMPT.format(code=original_code)
|
46 |
+
response = model.generate_content(prompt)
|
47 |
+
updated_code = response.text.strip()
|
48 |
+
|
49 |
+
# Clean up Markdown blocks if present
|
50 |
+
lines = updated_code.splitlines()
|
51 |
+
if len(lines) > 2:
|
52 |
+
lines = lines[1:-1] # remove the first and last lines
|
53 |
+
updated_code = "\n".join(lines)
|
54 |
+
else:
|
55 |
+
# if less than 3 lines, clear everything or keep as is depending on needs
|
56 |
+
updated_code = ""
|
57 |
+
|
58 |
+
with open(output_path, "w", encoding="utf-8") as output_file:
|
59 |
+
output_file.write(updated_code)
|
60 |
+
|
61 |
+
return updated_code
|
62 |
+
|
63 |
+
|
64 |
+
def extract_imports_from_file(file_path):
|
65 |
+
"""
|
66 |
+
Extract imported modules from a Python file to generate requirements.txt.
|
67 |
+
|
68 |
+
Args:
|
69 |
+
file_path (str): Path to the Python file.
|
70 |
+
|
71 |
+
Returns:
|
72 |
+
set: A set of imported module names.
|
73 |
+
"""
|
74 |
+
try:
|
75 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
76 |
+
tree = ast.parse(f.read())
|
77 |
+
except SyntaxError:
|
78 |
+
return set()
|
79 |
+
|
80 |
+
imports = set()
|
81 |
+
for node in ast.walk(tree):
|
82 |
+
if isinstance(node, ast.Import):
|
83 |
+
for alias in node.names:
|
84 |
+
imports.add(alias.name.split('.')[0])
|
85 |
+
elif isinstance(node, ast.ImportFrom):
|
86 |
+
if node.module and not node.module.startswith("."):
|
87 |
+
imports.add(node.module.split('.')[0])
|
88 |
+
return imports
|
89 |
+
|
90 |
+
|
91 |
+
def is_std_lib(module_name):
|
92 |
+
"""
|
93 |
+
Check if a module is part of the Python standard library.
|
94 |
+
|
95 |
+
Args:
|
96 |
+
module_name (str): The name of the module.
|
97 |
+
|
98 |
+
Returns:
|
99 |
+
bool: True if the module is part of the standard library, False otherwise.
|
100 |
+
"""
|
101 |
+
if module_name in sys.builtin_module_names:
|
102 |
+
return True
|
103 |
+
spec = importlib.util.find_spec(module_name)
|
104 |
+
return spec is not None and "site-packages" not in (spec.origin or "")
|
105 |
+
|
106 |
+
|
107 |
+
def generate_requirements_txt(base_path, output_path):
|
108 |
+
"""
|
109 |
+
Generate a requirements.txt file based on external imports found in Python files.
|
110 |
+
|
111 |
+
Args:
|
112 |
+
base_path (str): Root directory of the codebase.
|
113 |
+
output_path (str): Path to save the generated requirements.txt file.
|
114 |
+
"""
|
115 |
+
all_imports = set()
|
116 |
+
local_modules = set()
|
117 |
+
|
118 |
+
# Get names of internal modules (i.e., .py files in the repo)
|
119 |
+
for root, _, files in os.walk(base_path):
|
120 |
+
for file in files:
|
121 |
+
if file.endswith(".py"):
|
122 |
+
module_name = os.path.splitext(file)[0]
|
123 |
+
local_modules.add(module_name)
|
124 |
+
|
125 |
+
# Extract all imports used in the project
|
126 |
+
for root, _, files in os.walk(base_path):
|
127 |
+
for file in files:
|
128 |
+
if file.endswith(".py"):
|
129 |
+
file_path = os.path.join(root, file)
|
130 |
+
all_imports.update(extract_imports_from_file(file_path))
|
131 |
+
|
132 |
+
# Remove internal modules and standard library modules
|
133 |
+
external_imports = sorted([
|
134 |
+
imp for imp in all_imports
|
135 |
+
if imp not in local_modules and not is_std_lib(imp)
|
136 |
+
])
|
137 |
+
|
138 |
+
# Write the requirements.txt file
|
139 |
+
with open(output_path, "w", encoding="utf-8") as f:
|
140 |
+
for package in external_imports:
|
141 |
+
f.write(f"{package}\n")
|
index.md
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
π repo/
|
2 |
+
βββ .well-known/
|
3 |
+
β βββ mcp.yaml
|
4 |
+
βββ app.py β Gradio + MCP server
|
5 |
+
βββ doc_generator.py
|
6 |
+
βββ mcp_server.py
|
7 |
+
βββ readme_generator.py
|
8 |
+
βββ requirements.txt
|
9 |
+
βββ README.md
|
10 |
+
βββ index.md
|
readme_generator.py
ADDED
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import zipfile
|
3 |
+
import tempfile
|
4 |
+
import google.generativeai as genai
|
5 |
+
from dotenv import load_dotenv
|
6 |
+
from doc_generator import generate_requirements_txt
|
7 |
+
|
8 |
+
load_dotenv()
|
9 |
+
genai.configure(api_key=os.getenv("GOOGLE_API_KEY"))
|
10 |
+
model = genai.GenerativeModel("models/gemini-2.0-flash")
|
11 |
+
|
12 |
+
ANNOTATIONS = {
|
13 |
+
"app.py": "β Gradio + MCP server",
|
14 |
+
"README.md": "β With demo + tag \"mcp-server-track\"",
|
15 |
+
"demo_video.mp4": "β Link embedded in README"
|
16 |
+
}
|
17 |
+
|
18 |
+
PROMPT = """You are an expert software project documentation assistant.
|
19 |
+
|
20 |
+
You will write a clear, complete, and well-structured `README.md` file for a source code repository with the following files and content excerpts:
|
21 |
+
|
22 |
+
{file_summaries}
|
23 |
+
|
24 |
+
The README must contain:
|
25 |
+
1. A title
|
26 |
+
2. A short project description
|
27 |
+
3. An "Installation" section
|
28 |
+
4. A "Usage" section
|
29 |
+
5. A "Features" section
|
30 |
+
6. An "Authors" section (write "To be completed" if not detected)
|
31 |
+
7. A "License" section (write "To be completed" if not detected)
|
32 |
+
|
33 |
+
Respond only with the README.md content, without markdown ``` tags.
|
34 |
+
"""
|
35 |
+
|
36 |
+
def summarize_files(dir_path, max_files=20, max_chars=5000):
|
37 |
+
summaries = []
|
38 |
+
for root, _, files in os.walk(dir_path):
|
39 |
+
for file in files:
|
40 |
+
if file.endswith((".py", ".js", ".ts", ".java", ".md", ".json", ".txt")):
|
41 |
+
try:
|
42 |
+
with open(os.path.join(root, file), "r", encoding="utf-8") as f:
|
43 |
+
content = f.read()
|
44 |
+
rel_path = os.path.relpath(os.path.join(root, file), dir_path)
|
45 |
+
summaries.append(f"### {rel_path}\n```\n{content[:1000]}\n```")
|
46 |
+
if len("".join(summaries)) > max_chars:
|
47 |
+
break
|
48 |
+
except Exception:
|
49 |
+
continue
|
50 |
+
if len(summaries) >= max_files:
|
51 |
+
break
|
52 |
+
return "\n\n".join(summaries)
|
53 |
+
|
54 |
+
def generate_readme_from_zip(zip_file_path: str, output_dir: str) -> (str, str):
|
55 |
+
with tempfile.TemporaryDirectory() as tempdir:
|
56 |
+
with zipfile.ZipFile(zip_file_path, "r") as zip_ref:
|
57 |
+
zip_ref.extractall(tempdir)
|
58 |
+
|
59 |
+
file_summaries = summarize_files(tempdir)
|
60 |
+
prompt = PROMPT.format(file_summaries=file_summaries)
|
61 |
+
response = model.generate_content(prompt)
|
62 |
+
readme_content = response.text.strip()
|
63 |
+
|
64 |
+
readme_path = os.path.join(output_dir, "README.md")
|
65 |
+
index_path = os.path.join(output_dir, "index.md")
|
66 |
+
os.makedirs(output_dir, exist_ok=True)
|
67 |
+
# Clean markdown code blocks if they exist
|
68 |
+
lines = readme_content.splitlines()
|
69 |
+
if len(lines) > 2:
|
70 |
+
lines = lines[1:-1] # remove the first and last lines
|
71 |
+
readme_content = "\n".join(lines)
|
72 |
+
else:
|
73 |
+
# if less than 3 lines, empty or keep as needed
|
74 |
+
readme_content = ""
|
75 |
+
|
76 |
+
with open(readme_path, "w", encoding="utf-8") as f:
|
77 |
+
f.write(readme_content)
|
78 |
+
|
79 |
+
# β
Generate index from tempdir (correct location of extracted files)
|
80 |
+
write_index_file(tempdir, index_path)
|
81 |
+
|
82 |
+
return readme_path, index_path
|
83 |
+
|
84 |
+
def generate_tree_structure(path: str, prefix: str = "") -> str:
|
85 |
+
entries = sorted(os.listdir(path))
|
86 |
+
lines = []
|
87 |
+
dir_name = os.path.basename(os.path.abspath(path))
|
88 |
+
lines.append(f"π repo/")
|
89 |
+
|
90 |
+
for idx, entry in enumerate(entries):
|
91 |
+
full_path = os.path.join(path, entry)
|
92 |
+
connector = "βββ "
|
93 |
+
comment = f" {ANNOTATIONS.get(entry, '')}".rstrip()
|
94 |
+
|
95 |
+
lines.append(prefix + connector + (entry + "/" if os.path.isdir(full_path) else entry) + comment)
|
96 |
+
|
97 |
+
if os.path.isdir(full_path):
|
98 |
+
extension_prefix = "β "
|
99 |
+
subtree = generate_tree_structure(full_path, prefix + extension_prefix)
|
100 |
+
lines.extend(subtree.splitlines()[1:]) # skip repeated dir name
|
101 |
+
|
102 |
+
lines.extend(["βββ README.md",
|
103 |
+
"βββ index.md"])
|
104 |
+
|
105 |
+
return "\n".join(lines)
|
106 |
+
|
107 |
+
|
108 |
+
def write_index_file(project_path: str, output_path: str):
|
109 |
+
structure = generate_tree_structure(project_path)
|
110 |
+
with open(output_path, "w", encoding="utf-8") as f:
|
111 |
+
f.write(structure)
|
requirements.txt
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio>=4.0.0
|
2 |
+
openai
|
3 |
+
tqdm
|
4 |
+
requests
|
5 |
+
gitpython
|
6 |
+
python-dotenv
|
7 |
+
google.generativeai
|
8 |
+
dotenv
|
9 |
+
jinja2
|
10 |
+
python-multipart
|
11 |
+
stdlib-list
|