diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index c900964..e1049ae 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -84,7 +84,7 @@ graph LR
D -->|9. return to UI| F
F -->|10. publish| N
N -->|PATCH publish| D
- D -->|update visibility| PG
+ D -->|update visibility| PGV
D -.->|OAuth| GH
A -.->|AI inference| G
D ---|pooled conns| PB
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index f1dde2e..1030253 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -54,5 +54,42 @@ services:
ports:
- "6432:5432"
+ fastapi-parser:
+ build:
+ context: ./services/parser
+ dockerfile: docker/Dockerfile
+ restart: always
+ container_name: pydocai-parser
+ env_file:
+ - ./services/core/env/.env
+ environment:
+ DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@pgbouncer:5432/${DB_NAME}
+ INTERNAL_API_KEY: pydocai-internal-key
+ ports:
+ - "8002:8002"
+ depends_on:
+ - pgbouncer
+
+ fastapi-ai:
+ build:
+ context: ./services/ai
+ dockerfile: docker/Dockerfile
+ restart: always
+ container_name: pydocai-ai
+ env_file:
+ - ./services/core/env/.env
+ environment:
+ DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@pgbouncer:5432/${DB_NAME}
+ DJANGO_INTERNAL_URL: http://backend:8000/api/internal
+ INTERNAL_API_KEY: pydocai-internal-key
+ EMBEDDING_MODEL: all-MiniLM-L6-v2
+ volumes:
+ - model_cache:/root/.cache/huggingface
+ ports:
+ - "8003:8003"
+ depends_on:
+ - pgbouncer
+
volumes:
static_volume:
+ model_cache:
diff --git a/frondend/api/sitemap.js b/frondend/api/sitemap.js
index d430d17..87ae9ac 100644
--- a/frondend/api/sitemap.js
+++ b/frondend/api/sitemap.js
@@ -20,6 +20,7 @@ export default function handler(req, res) {
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, 'utf-8')
res.setHeader('Content-Type', 'text/xml')
+ res.setHeader('X-Robots-Tag', 'noindex')
res.setHeader('Cache-Control', 'public, max-age=3600')
return res.status(200).send(content)
}
diff --git a/frondend/index.html b/frondend/index.html
index 5b0c039..3a8fc12 100644
--- a/frondend/index.html
+++ b/frondend/index.html
@@ -18,14 +18,14 @@
-
+
-
+
-
+
@@ -33,13 +33,13 @@
-
+
-
PyDocAI — AI-Powered Python Documentation Generator
+ PyDocAI by Denjo — AI-Powered Python Documentation Generator
diff --git a/frondend/public/robots.txt b/frondend/public/robots.txt
index 1f28da7..f814e77 100644
--- a/frondend/public/robots.txt
+++ b/frondend/public/robots.txt
@@ -11,4 +11,4 @@ Disallow: /profile
Disallow: /feedback
Disallow: /admin/
-Sitemap: https://pydocai.vercel.app/api/sitemap
+Sitemap: https://pydocai.vercel.app/sitemap.xml
diff --git a/frondend/src/pages/Home.jsx b/frondend/src/pages/Home.jsx
index abe4752..bb012c9 100644
--- a/frondend/src/pages/Home.jsx
+++ b/frondend/src/pages/Home.jsx
@@ -64,7 +64,7 @@ const jsonLd = {
operatingSystem: 'Web',
description: 'AI-powered documentation generator for Python, Django, and any programming language. Upload code or connect a Git repo to generate comprehensive documentation automatically.',
url: siteUrl,
- author: { '@type': 'Organization', name: 'PyDocAI' },
+ author: { '@type': 'Person', name: 'Denjo (Dennis Joseph)', url: 'https://dennis-r.vercel.app/' },
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
}
@@ -76,13 +76,13 @@ export default function Home() {
return (
- PyDocAI — AI-Powered Documentation Generator for Any Language
-
+ PyDocAI by Denjo — AI-Powered Documentation Generator for Any Language
+
-
+
-
+
diff --git a/frondend/vercel.json b/frondend/vercel.json
index 1323cda..8ea68ac 100644
--- a/frondend/vercel.json
+++ b/frondend/vercel.json
@@ -1,5 +1,16 @@
{
+ "redirects": [
+ {
+ "source": "/index.html",
+ "destination": "/",
+ "permanent": true
+ }
+ ],
"rewrites": [
+ {
+ "source": "/api/:path*",
+ "destination": "https://pydocai.duckdns.org/api/:path*"
+ },
{
"source": "/(.*)",
"destination": "/index.html"
diff --git a/services/ai/api/routes/generate.py b/services/ai/api/routes/generate.py
index 3e53aa8..cdebc3c 100644
--- a/services/ai/api/routes/generate.py
+++ b/services/ai/api/routes/generate.py
@@ -7,16 +7,16 @@
from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends
from sqlalchemy.orm import Session
-from ...schemas.requests import GenerateRequest
-from ...schemas.responses import GenerateResponse
-from ...services.groq import call_groq
-from ...services.markdown import sanitize_markdown
-from ...services.generation import generate_file_docs, generate_project_summary, mock_docs, retrieve_project_context
-from ...services.docs_builder import build_api_docs
-from ...common.django_client import get_project, get_project_files, send_ai_docs, update_project
-from ...rag import embed_and_store_chunks
-from ..deps import get_db, verify_internal_key
-from ...config.config import settings
+from schemas.requests import GenerateRequest
+from schemas.responses import GenerateResponse
+from services.groq import call_groq
+from services.markdown import sanitize_markdown
+from services.generation import generate_file_docs, generate_project_summary, mock_docs, retrieve_project_context
+from services.docs_builder import build_api_docs
+from common.django_client import get_project, get_project_files, send_ai_docs, update_project
+from rag import embed_and_store_chunks
+from api.deps import get_db, verify_internal_key
+from config.config import settings
router = APIRouter()
logger = logging.getLogger("ai.generate")
@@ -56,7 +56,9 @@ def generate_docs(req: GenerateRequest, background_tasks: BackgroundTasks, db: S
update_project(req.project_id, {"status": "processing"})
- files_data = get_project_files(req.project_id)
+ files_data = req.files_data
+ if not files_data:
+ files_data = get_project_files(req.project_id)
if not files_data:
raise HTTPException(400, "No parsed files found. Run parser first.")
@@ -94,7 +96,6 @@ def generate_docs(req: GenerateRequest, background_tasks: BackgroundTasks, db: S
fh.write(content)
summary = generate_project_summary(temp_dir, project.get("name"))
- update_project(req.project_id, {"project_info": summary})
if req.use_ai and settings.GROQ_API_KEY:
try:
@@ -129,6 +130,7 @@ def generate_docs(req: GenerateRequest, background_tasks: BackgroundTasks, db: S
has_root_req = any(f.get("file_path") == "requirements.txt" for f in files_data)
has_root_manage = any(f.get("file_path") == "manage.py" for f in files_data)
cd_dir = "." if (has_root_req or has_root_manage) else (repo_dir or ".")
+ pm_cmd = "pip install -r requirements.txt"
project_context = _build_project_context(
project, summary, fw_name, fw_block, tree_trunc,
@@ -191,6 +193,8 @@ def generate_docs(req: GenerateRequest, background_tasks: BackgroundTasks, db: S
send_ai_docs(req.project_id, {
"generated_docs": generated,
"readme_docs": readme,
+ "api_docs": api_docs,
+ "project_info": summary,
"status": "done",
})
else:
@@ -209,6 +213,7 @@ def generate_docs(req: GenerateRequest, background_tasks: BackgroundTasks, db: S
send_ai_docs(req.project_id, {
"generated_docs": sanitize_markdown(fallback),
"readme_docs": sanitize_markdown(fallback),
+ "project_info": summary,
"status": "done",
})
diff --git a/services/ai/models/__init__.py b/services/ai/models/__init__.py
index aafb569..6eb0869 100644
--- a/services/ai/models/__init__.py
+++ b/services/ai/models/__init__.py
@@ -1,3 +1,5 @@
+from .project import Project
+from .project_file import ProjectFile
from .code_embedding import CodeEmbedding
-__all__ = ["CodeEmbedding"]
+__all__ = ["Project", "ProjectFile", "CodeEmbedding"]
diff --git a/services/ai/schemas/requests.py b/services/ai/schemas/requests.py
index c4a7e76..31693cd 100644
--- a/services/ai/schemas/requests.py
+++ b/services/ai/schemas/requests.py
@@ -6,3 +6,4 @@ class GenerateRequest(BaseModel):
project_id: str
file_path: Optional[str] = None
use_ai: bool = True
+ files_data: Optional[list] = None
diff --git a/services/core/apps/parser/tasks.py b/services/core/apps/parser/tasks.py
index ef42f94..f9d99fc 100644
--- a/services/core/apps/parser/tasks.py
+++ b/services/core/apps/parser/tasks.py
@@ -43,6 +43,7 @@ def parse_folder_task(self, project_id, py_files, zip_base64=None, user_descript
zf = zipfile.ZipFile(io.BytesIO(zip_data))
parsed_count = 0
+ files_data = []
for file_path in py_files:
if should_exclude(file_path):
continue
@@ -50,12 +51,21 @@ def parse_folder_task(self, project_id, py_files, zip_base64=None, user_descript
content = zf.read(file_path).decode("utf-8", errors="ignore")
files = {"file": (file_path.split("/")[-1], content.encode("utf-8"), "text/x-python")}
data = {"project_id": str(project_id), "name": project.name, "file_path": file_path}
- _call_fastapi("POST", f"{PARSER_URL}/api/parser/file/", files=files, data=data)
+ resp = _call_fastapi("POST", f"{PARSER_URL}/api/parser/file/", files=files, data=data)
+ files_data.append({
+ "file_path": resp["file_path"],
+ "file_name": resp["file_name"],
+ "content": resp["content"],
+ "parsed_data": resp["parsed"],
+ })
parsed_count += 1
except Exception as e:
logger.warning(f"Error sending {file_path} to parser: {e}")
- ai_resp = _call_fastapi("POST", f"{AI_URL}/api/ai/generate/", json={"project_id": str(project_id)})
+ ai_resp = _call_fastapi("POST", f"{AI_URL}/api/ai/generate/", json={
+ "project_id": str(project_id),
+ "files_data": files_data,
+ })
project.refresh_from_db()
@@ -88,9 +98,18 @@ def parse_and_generate_docs_task(self, project_id, source_code, file_name, file_
files = {"file": (file_name, source_code.encode("utf-8"), "text/x-python")}
data = {"project_id": str(project_id), "name": project.name}
- _call_fastapi("POST", f"{PARSER_URL}/api/parser/file/", files=files, data=data)
-
- ai_resp = _call_fastapi("POST", f"{AI_URL}/api/ai/generate/", json={"project_id": str(project_id)})
+ resp = _call_fastapi("POST", f"{PARSER_URL}/api/parser/file/", files=files, data=data)
+
+ files_data = [{
+ "file_path": resp["file_path"],
+ "file_name": resp["file_name"],
+ "content": resp["content"],
+ "parsed_data": resp["parsed"],
+ }]
+ ai_resp = _call_fastapi("POST", f"{AI_URL}/api/ai/generate/", json={
+ "project_id": str(project_id),
+ "files_data": files_data,
+ })
project.refresh_from_db()
diff --git a/services/core/apps/projects/serializers/project.py b/services/core/apps/projects/serializers/project.py
index b423d32..0baedbc 100644
--- a/services/core/apps/projects/serializers/project.py
+++ b/services/core/apps/projects/serializers/project.py
@@ -35,7 +35,12 @@ class ProjectListSerializer(serializers.ModelSerializer):
file_count = serializers.SerializerMethodField()
def get_file_count(self, obj):
- return getattr(obj, 'file_count', obj.files.count())
+ count = getattr(obj, 'file_count', obj.files.count())
+ if count:
+ return count
+ if isinstance(obj.project_info, dict):
+ return obj.project_info.get('file_count', 0)
+ return 0
class Meta:
model = Project
diff --git a/services/core/config/settings/production.py b/services/core/config/settings/production.py
index 7696cbf..5e40cb1 100644
--- a/services/core/config/settings/production.py
+++ b/services/core/config/settings/production.py
@@ -31,7 +31,7 @@
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD')
# ── PRODUCTION SECURITY ─────────────────────────────────────
-SECURE_SSL_REDIRECT = True
+SECURE_SSL_REDIRECT = False
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
@@ -50,7 +50,7 @@
'rest_framework.throttling.UserRateThrottle',
]
REST_FRAMEWORK['DEFAULT_THROTTLE_RATES'] = {
- 'anon': '100/day',
+ 'anon': '10000/day',
'user': '1000/hour',
'comment_create': '20/hour',
'publish': '10/hour',
diff --git a/services/parser/api/routes/file.py b/services/parser/api/routes/file.py
index fc6da3e..86fa291 100644
--- a/services/parser/api/routes/file.py
+++ b/services/parser/api/routes/file.py
@@ -3,7 +3,6 @@
from ast_parser import parse_python_file
from validators import validate_python_code
from framework_detector import detect_framework
-from common.django_client import get_project, update_project, create_project_file
from api.deps import verify_internal_key
router = APIRouter()
@@ -32,31 +31,16 @@ async def analyze_file(
parsed = parse_python_file(source_code)
- project = get_project(project_id)
- if not project:
- raise HTTPException(404, "Project not found")
-
imports = [imp.get("display", str(imp)) if isinstance(imp, dict) else str(imp) for imp in parsed.get("imports", [])]
fw_info = detect_framework(imports, [file_path or file.filename], [source_code])
final_path = file_path or file.filename
- create_project_file(project_id, {
- "file_path": final_path,
- "file_name": file.filename.split("/")[-1],
- "file_size": len(source_code),
- "content": source_code,
- "parsed_data": parsed,
- "generated_docs": "",
- })
-
- update_project(project_id, {
- "framework_info": fw_info,
- "parsed_data": parsed,
- "status": "processing",
- })
return {
"project_id": project_id,
+ "file_name": file.filename.split("/")[-1],
+ "file_path": final_path,
+ "content": source_code,
"parsed": parsed,
"file_count": 1,
"framework": fw_info,