Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
1 change: 1 addition & 0 deletions frondend/api/sitemap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
10 changes: 5 additions & 5 deletions frondend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,28 @@
<meta name="theme-color" content="#4c8df3" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="PyDocAI — Automatically generate professional documentation for your Django projects using AI. Upload files, paste code, or connect a Git repo." />
<meta name="keywords" content="django documentation generator, AI docs, Python documentation, PyDocAI, automated documentation, code to docs" />
<meta name="keywords" content="django documentation generator, AI docs, Python documentation, PyDocAI, denjo, automated documentation, code to docs, Dennis Joseph" />
<meta name="robots" content="index, follow" />
<meta name="author" content="PyDocAI" />
<meta name="author" content="Denjo (Dennis Joseph)" />
<meta name="google-site-verification" content="TAw09gaOVi718Q7HiVZ0uWyrUrU2dGn5JJSh5szsgss" />
<link rel="canonical" href="https://pydocai.vercel.app/" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://pydocai.vercel.app/" />
<meta property="og:title" content="PyDocAI — AI-Powered Python Documentation Generator" />
<meta property="og:title" content="PyDocAI by Denjo — AI-Powered Python Documentation Generator" />
<meta property="og:description" content="Generate beautiful, comprehensive documentation for your Django projects in seconds." />
<meta property="og:image" content="https://pydocai.vercel.app/pydocai-og.svg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:site_name" content="PyDocAI" />
<meta property="og:locale" content="en_US" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="PyDocAI — AI-Powered Python Documentation Generator" />
<meta name="twitter:title" content="PyDocAI by Denjo — AI-Powered Python Documentation Generator" />
<meta name="twitter:description" content="Generate beautiful, comprehensive documentation for your Django projects using AI." />
<meta name="twitter:image" content="https://pydocai.vercel.app/pydocai-og.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800&family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<title>PyDocAI — AI-Powered Python Documentation Generator</title>
<title>PyDocAI by Denjo — AI-Powered Python Documentation Generator</title>
</head>
<body>
<div id="root"></div>
Expand Down
2 changes: 1 addition & 1 deletion frondend/public/robots.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ Disallow: /profile
Disallow: /feedback
Disallow: /admin/

Sitemap: https://pydocai.vercel.app/api/sitemap
Sitemap: https://pydocai.vercel.app/sitemap.xml
10 changes: 5 additions & 5 deletions frondend/src/pages/Home.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
}

Expand All @@ -76,13 +76,13 @@ export default function Home() {
return (
<div className="relative z-10 flex flex-col bg-bg-primary">
<Helmet>
<title>PyDocAI — AI-Powered Documentation Generator for Any Language</title>
<meta name="description" content="Upload your Django project or paste code from any language — AI generates beautiful, comprehensive docs. Free universal documentation generator." />
<title>PyDocAI by Denjo — AI-Powered Documentation Generator for Any Language</title>
<meta name="description" content="Upload your Django project or paste code from any language — AI generates beautiful, comprehensive docs. Created by Denjo (Dennis Joseph). Free universal documentation generator." />
<link rel="canonical" href={siteUrl} />
<meta property="og:title" content="PyDocAI — AI-Powered Documentation Generator for Any Language" />
<meta property="og:title" content="PyDocAI by Denjo — AI-Powered Documentation Generator for Any Language" />
<meta property="og:description" content="Upload your Django project or paste code from any language — AI generates beautiful, comprehensive docs." />
<meta property="og:url" content={siteUrl} />
<meta name="twitter:title" content="PyDocAI — AI-Powered Documentation Generator for Any Language" />
<meta name="twitter:title" content="PyDocAI by Denjo — AI-Powered Documentation Generator for Any Language" />
<meta name="twitter:description" content="Upload your Django project or paste code from any language — AI generates beautiful, comprehensive docs." />
<script type="application/ld+json">{JSON.stringify(jsonLd)}</script>
</Helmet>
Expand Down
11 changes: 11 additions & 0 deletions frondend/vercel.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
29 changes: 17 additions & 12 deletions services/ai/api/routes/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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.")

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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",
})

Expand Down
4 changes: 3 additions & 1 deletion services/ai/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from .project import Project
from .project_file import ProjectFile
from .code_embedding import CodeEmbedding

__all__ = ["CodeEmbedding"]
__all__ = ["Project", "ProjectFile", "CodeEmbedding"]
1 change: 1 addition & 0 deletions services/ai/schemas/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ class GenerateRequest(BaseModel):
project_id: str
file_path: Optional[str] = None
use_ai: bool = True
files_data: Optional[list] = None
29 changes: 24 additions & 5 deletions services/core/apps/parser/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,29 @@ 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
try:
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()

Expand Down Expand Up @@ -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()

Expand Down
7 changes: 6 additions & 1 deletion services/core/apps/projects/serializers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions services/core/config/settings/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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',
Expand Down
22 changes: 3 additions & 19 deletions services/parser/api/routes/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down
Loading