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,