Spaces:
Sleeping
Sleeping
Initial deployment of AI REST API Generator
Browse files- .gitignore +19 -0
- Dockerfile +20 -20
- LICENSE +15 -0
- README.md +207 -20
- additional_function.py +30 -0
- app.py +412 -0
- generator.py +420 -0
- pipeline.py +161 -0
- prompt.py +456 -0
- requirements.txt +9 -3
- settings_generator.py +117 -0
- spec_schema.json +118 -0
- spec_validator.py +314 -0
.gitignore
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
*.pyo
|
| 5 |
+
*.pyd
|
| 6 |
+
venv/
|
| 7 |
+
.env
|
| 8 |
+
|
| 9 |
+
# Streamlit
|
| 10 |
+
.streamlit/
|
| 11 |
+
|
| 12 |
+
# Django
|
| 13 |
+
db.sqlite3
|
| 14 |
+
media/
|
| 15 |
+
staticfiles/
|
| 16 |
+
|
| 17 |
+
# Runs / Generated projects
|
| 18 |
+
runs/
|
| 19 |
+
projects/
|
Dockerfile
CHANGED
|
@@ -1,20 +1,20 @@
|
|
| 1 |
-
FROM python:3.
|
| 2 |
-
|
| 3 |
-
WORKDIR /app
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
COPY
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
RUN
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
git \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
# Copy project files
|
| 11 |
+
COPY . /app
|
| 12 |
+
|
| 13 |
+
# Install Python dependencies
|
| 14 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 15 |
+
|
| 16 |
+
# Expose Streamlit port
|
| 17 |
+
EXPOSE 8501
|
| 18 |
+
|
| 19 |
+
# Run Streamlit
|
| 20 |
+
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Harshad Hole
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
|
README.md
CHANGED
|
@@ -1,20 +1,207 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
# 🤖 AI REST API Generator
|
| 3 |
+
|
| 4 |
+
**AI REST API Generator** is a developer-focused tool that automatically generates **Django REST Framework backends** using AI or manual configuration.
|
| 5 |
+
It converts high-level requirements into a fully structured, runnable Django project with CRUD APIs, serializers, views, URLs, and settings.
|
| 6 |
+
|
| 7 |
+
🚧 **Status:** Under active development
|
| 8 |
+
📧 **Contact:** [harshadhole04@gmail.com](mailto:harshadhole04@gmail.com)
|
| 9 |
+
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
## ✨ Features
|
| 13 |
+
|
| 14 |
+
* ⚡ **AI-Powered Backend Generation**
|
| 15 |
+
|
| 16 |
+
* Generate Django REST APIs from natural language prompts
|
| 17 |
+
* Automatically infers models, fields, and relationships
|
| 18 |
+
|
| 19 |
+
* 🧱 **Manual Model Builder**
|
| 20 |
+
|
| 21 |
+
* Define models, fields, relationships (One-to-One, One-to-Many)
|
| 22 |
+
* Full control without AI if preferred
|
| 23 |
+
|
| 24 |
+
* 🧩 **Schema-Driven Architecture**
|
| 25 |
+
|
| 26 |
+
* JSON Schema validation before code generation
|
| 27 |
+
* Auto-fixes common schema issues safely
|
| 28 |
+
|
| 29 |
+
* 📦 **Complete Django Project Output**
|
| 30 |
+
|
| 31 |
+
* Models, serializers, views, URLs
|
| 32 |
+
* JWT authentication support
|
| 33 |
+
* Database configuration (SQLite / PostgreSQL)
|
| 34 |
+
* Static & media configuration
|
| 35 |
+
|
| 36 |
+
* 📥 **One-Click Download**
|
| 37 |
+
|
| 38 |
+
* Generated backend is zipped and ready to run
|
| 39 |
+
|
| 40 |
+
* 🧠 **Pluggable LLM Support**
|
| 41 |
+
|
| 42 |
+
* OpenAI
|
| 43 |
+
* Groq
|
| 44 |
+
* Easy to extend for other providers
|
| 45 |
+
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
## 🏗️ Architecture Overview
|
| 49 |
+
|
| 50 |
+
```
|
| 51 |
+
Streamlit UI
|
| 52 |
+
↓
|
| 53 |
+
Pipeline Orchestrator
|
| 54 |
+
↓
|
| 55 |
+
LLM (Model Prompt / Spec Generation)
|
| 56 |
+
↓
|
| 57 |
+
Schema Validation (JSON Schema)
|
| 58 |
+
↓
|
| 59 |
+
Django Code Generator
|
| 60 |
+
↓
|
| 61 |
+
Ready-to-run Django REST Project
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
---
|
| 65 |
+
|
| 66 |
+
## 🚀 How It Works
|
| 67 |
+
|
| 68 |
+
1. **Input**
|
| 69 |
+
|
| 70 |
+
* Project name & description
|
| 71 |
+
* Choose AI-generated models or manual models
|
| 72 |
+
* Select database and authentication
|
| 73 |
+
* Provide LLM provider and API key
|
| 74 |
+
|
| 75 |
+
2. **Validation**
|
| 76 |
+
|
| 77 |
+
* Generated spec is validated against a strict JSON schema
|
| 78 |
+
* Automatic normalization and safe fixes applied
|
| 79 |
+
|
| 80 |
+
3. **Generation**
|
| 81 |
+
|
| 82 |
+
* Django project is created in an isolated run directory
|
| 83 |
+
* Apps, models, serializers, views, URLs, settings generated
|
| 84 |
+
|
| 85 |
+
4. **Output**
|
| 86 |
+
|
| 87 |
+
* Project is zipped
|
| 88 |
+
* Download instantly from the UI
|
| 89 |
+
|
| 90 |
+
---
|
| 91 |
+
|
| 92 |
+
## 🖥️ Running Locally
|
| 93 |
+
|
| 94 |
+
### 1️⃣ Clone the Repository
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
git clone https://github.com/harshadSH/ai-rest-api-generator.git
|
| 98 |
+
cd ai-rest-api-generator
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
### 2️⃣ Create Virtual Environment
|
| 102 |
+
|
| 103 |
+
```bash
|
| 104 |
+
python -m venv venv
|
| 105 |
+
source venv/bin/activate # Windows: venv\Scripts\activate
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
### 3️⃣ Install Dependencies
|
| 109 |
+
|
| 110 |
+
```bash
|
| 111 |
+
pip install -r requirements.txt
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
### 4️⃣ Run the Streamlit App
|
| 115 |
+
|
| 116 |
+
```bash
|
| 117 |
+
streamlit run app.py
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
---
|
| 121 |
+
|
| 122 |
+
## 📂 Generated Project Structure
|
| 123 |
+
|
| 124 |
+
```
|
| 125 |
+
project_name/
|
| 126 |
+
├── manage.py
|
| 127 |
+
├── project_name/
|
| 128 |
+
│ ├── settings.py
|
| 129 |
+
│ ├── urls.py
|
| 130 |
+
│ └── wsgi.py
|
| 131 |
+
├── core/
|
| 132 |
+
│ ├── models.py
|
| 133 |
+
│ ├── serializers.py
|
| 134 |
+
│ ├── views.py
|
| 135 |
+
│ ├── urls.py
|
| 136 |
+
│ └── admin.py
|
| 137 |
+
├── requirements.txt
|
| 138 |
+
└── README.md
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
---
|
| 142 |
+
|
| 143 |
+
## 🛡️ Authentication
|
| 144 |
+
|
| 145 |
+
* Supports **JWT Authentication**
|
| 146 |
+
* All CRUD endpoints can be protected
|
| 147 |
+
* Easily extendable to session-based auth
|
| 148 |
+
|
| 149 |
+
---
|
| 150 |
+
|
| 151 |
+
## 🧪 Validation & Reliability
|
| 152 |
+
|
| 153 |
+
* Strict JSON Schema enforcement
|
| 154 |
+
* Deterministic naming conventions
|
| 155 |
+
* Auto-correction of:
|
| 156 |
+
|
| 157 |
+
* Database engine names
|
| 158 |
+
* Model and field naming
|
| 159 |
+
* Missing required fields
|
| 160 |
+
* Prevents invalid or unsafe Django code generation
|
| 161 |
+
|
| 162 |
+
---
|
| 163 |
+
|
| 164 |
+
## 🌍 Deployment
|
| 165 |
+
|
| 166 |
+
This project is suitable for:
|
| 167 |
+
|
| 168 |
+
* Local development
|
| 169 |
+
* Docker-based deployment
|
| 170 |
+
* Cloud hosting (AWS, Azure, GCP, Railway, Render)
|
| 171 |
+
|
| 172 |
+
⚠️ Note: Production hardening (rate limiting, monitoring, secrets management) should be added as per deployment needs.
|
| 173 |
+
|
| 174 |
+
---
|
| 175 |
+
|
| 176 |
+
## 📜 License
|
| 177 |
+
|
| 178 |
+
This project is licensed under the **MIT License**.
|
| 179 |
+
You are free to use, modify, and distribute it with attribution.
|
| 180 |
+
|
| 181 |
+
---
|
| 182 |
+
|
| 183 |
+
## 👨💻 Author
|
| 184 |
+
|
| 185 |
+
**Harshad Hole**
|
| 186 |
+
Bachelor of Engineering – Artificial Intelligence & Data Science
|
| 187 |
+
📧 Email: **[harshadhole04@gmail.com](mailto:harshadhole04@gmail.com)**
|
| 188 |
+
|
| 189 |
+
---
|
| 190 |
+
|
| 191 |
+
## ⭐ Why This Project Matters
|
| 192 |
+
|
| 193 |
+
* Solves repetitive backend boilerplate
|
| 194 |
+
* Demonstrates real-world use of LLMs in software engineering
|
| 195 |
+
* Combines AI + validation + deterministic code generation
|
| 196 |
+
* Strong portfolio-grade project for AI / Backend / Full-Stack roles
|
| 197 |
+
|
| 198 |
+
---
|
| 199 |
+
|
| 200 |
+
If you want, I can also:
|
| 201 |
+
|
| 202 |
+
* Write a **“Project Motivation”** section
|
| 203 |
+
* Optimize README for **Hugging Face Spaces**
|
| 204 |
+
* Add badges (Python, Django, MIT, AI)
|
| 205 |
+
* Review it from a recruiter’s perspective
|
| 206 |
+
|
| 207 |
+
Just tell me 👍
|
additional_function.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_groq import ChatGroq
|
| 2 |
+
from langchain_openai import ChatOpenAI
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
def get_llm(provider: str, api_key: str):
|
| 6 |
+
if not api_key or not api_key.strip():
|
| 7 |
+
raise ValueError("API key is missing or empty")
|
| 8 |
+
|
| 9 |
+
provider = provider.lower()
|
| 10 |
+
|
| 11 |
+
if provider == "groq":
|
| 12 |
+
# ✅ Set env var as fallback (Groq SDK expects this sometimes)
|
| 13 |
+
os.environ["GROQ_API_KEY"] = api_key
|
| 14 |
+
|
| 15 |
+
return ChatGroq(
|
| 16 |
+
model="openai/gpt-oss-120b",
|
| 17 |
+
api_key=api_key,
|
| 18 |
+
temperature=0
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
elif provider == "openai":
|
| 22 |
+
return ChatOpenAI(
|
| 23 |
+
model="gpt-4o-mini",
|
| 24 |
+
api_key=api_key,
|
| 25 |
+
temperature=0
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
else:
|
| 29 |
+
raise ValueError(f"Unsupported LLM provider: {provider}")
|
| 30 |
+
|
app.py
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import uuid
|
| 3 |
+
import os
|
| 4 |
+
import json
|
| 5 |
+
import shutil
|
| 6 |
+
import zipfile
|
| 7 |
+
from typing import Dict, List
|
| 8 |
+
|
| 9 |
+
from spec_validator import validate_and_clean_spec
|
| 10 |
+
from pipeline import run_pipeline
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# =====================================================
|
| 15 |
+
# CONFIG
|
| 16 |
+
# =====================================================
|
| 17 |
+
BASE_RUN_DIR = "runs"
|
| 18 |
+
SCHEMA_PATH = "spec_schema.json"
|
| 19 |
+
|
| 20 |
+
st.set_page_config(
|
| 21 |
+
page_title="AI REST API Generator",
|
| 22 |
+
layout="wide",
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
# =====================================================
|
| 26 |
+
# HELPERS
|
| 27 |
+
# =====================================================
|
| 28 |
+
|
| 29 |
+
def create_run_dir():
|
| 30 |
+
run_id = uuid.uuid4().hex[:8]
|
| 31 |
+
run_dir = os.path.join(BASE_RUN_DIR, f"run_{run_id}")
|
| 32 |
+
os.makedirs(run_dir, exist_ok=True)
|
| 33 |
+
return run_id, run_dir
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def zip_folder(folder_path: str) -> str:
|
| 37 |
+
zip_path = f"{folder_path}.zip"
|
| 38 |
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
|
| 39 |
+
for root, _, files in os.walk(folder_path):
|
| 40 |
+
for file in files:
|
| 41 |
+
full_path = os.path.join(root, file)
|
| 42 |
+
arcname = os.path.relpath(full_path, folder_path)
|
| 43 |
+
zipf.write(full_path, arcname)
|
| 44 |
+
return zip_path
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def load_schema():
|
| 48 |
+
with open(SCHEMA_PATH, "r", encoding="utf-8") as f:
|
| 49 |
+
return json.load(f)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# =====================================================
|
| 53 |
+
# SESSION STATE INIT
|
| 54 |
+
# =====================================================
|
| 55 |
+
|
| 56 |
+
if "spec" not in st.session_state:
|
| 57 |
+
st.session_state.spec = {
|
| 58 |
+
"project_name": "",
|
| 59 |
+
"description": "",
|
| 60 |
+
"database": {},
|
| 61 |
+
"auth": {"type": "jwt"},
|
| 62 |
+
"api_config": {"base_url": "/api/"},
|
| 63 |
+
"apps": {"core": {"models": {}, "apis": {}}},
|
| 64 |
+
"use_ai_models": True,
|
| 65 |
+
"llm": {}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
if "models_ui" not in st.session_state:
|
| 69 |
+
st.session_state.models_ui = []
|
| 70 |
+
|
| 71 |
+
if "logs" not in st.session_state:
|
| 72 |
+
st.session_state.logs = []
|
| 73 |
+
|
| 74 |
+
log_box = st.empty()
|
| 75 |
+
|
| 76 |
+
def log(msg: str):
|
| 77 |
+
st.session_state.logs.append(msg)
|
| 78 |
+
log_box.write("\n".join(st.session_state.logs))
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# =====================================================
|
| 82 |
+
# UI HEADER
|
| 83 |
+
# =====================================================
|
| 84 |
+
|
| 85 |
+
st.title("🤖 AI REST API Generator")
|
| 86 |
+
st.markdown(
|
| 87 |
+
"""
|
| 88 |
+
**Build Django REST APIs using AI or manual configuration.**
|
| 89 |
+
🚧 *This website is under active development.*
|
| 90 |
+
"""
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
st.divider()
|
| 94 |
+
|
| 95 |
+
# =====================================================
|
| 96 |
+
# SECTION 1 — LLM CONFIGURATION
|
| 97 |
+
# =====================================================
|
| 98 |
+
st.header("🔑 LLM Configuration")
|
| 99 |
+
|
| 100 |
+
llm_provider = st.selectbox(
|
| 101 |
+
"Select LLM Provider",
|
| 102 |
+
["Groq", "OpenAI"]
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
api_key = st.text_input(
|
| 106 |
+
f"{llm_provider} API Key",
|
| 107 |
+
type="password",
|
| 108 |
+
help="Your API key is used only for this session and never stored."
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
st.session_state.spec["llm"] = {
|
| 112 |
+
"provider": llm_provider.lower()
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# =====================================================
|
| 119 |
+
# SECTION 2 — PROJECT BASICS
|
| 120 |
+
# =====================================================
|
| 121 |
+
st.header("📦 Project Information")
|
| 122 |
+
|
| 123 |
+
col1, col2 = st.columns(2)
|
| 124 |
+
|
| 125 |
+
with col1:
|
| 126 |
+
st.session_state.spec["project_name"] = st.text_input(
|
| 127 |
+
"Project Name",
|
| 128 |
+
placeholder="my_backend_project"
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
with col2:
|
| 132 |
+
st.session_state.spec["description"] = st.text_area(
|
| 133 |
+
"Project Description",
|
| 134 |
+
placeholder="Short description of your backend"
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
# =====================================================
|
| 138 |
+
# SECTION 3 — DATABASE CONFIGURATION
|
| 139 |
+
# =====================================================
|
| 140 |
+
st.header("🗄 Database Configuration")
|
| 141 |
+
|
| 142 |
+
db_engine = st.selectbox(
|
| 143 |
+
"Database Engine",
|
| 144 |
+
["sqlite", "postgresql", "mysql"]
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
if db_engine == "sqlite":
|
| 148 |
+
st.session_state.spec["database"] = {
|
| 149 |
+
"engine": "sqlite",
|
| 150 |
+
"name": "db.sqlite3"
|
| 151 |
+
}
|
| 152 |
+
else:
|
| 153 |
+
col1, col2 = st.columns(2)
|
| 154 |
+
with col1:
|
| 155 |
+
db_name = st.text_input("Database Name")
|
| 156 |
+
db_user = st.text_input("Database User")
|
| 157 |
+
db_password = st.text_input("Database Password", type="password")
|
| 158 |
+
with col2:
|
| 159 |
+
db_host = st.text_input("Database Host", value="localhost")
|
| 160 |
+
db_port = st.number_input("Database Port", value=5432)
|
| 161 |
+
|
| 162 |
+
st.session_state.spec["database"] = {
|
| 163 |
+
"engine": db_engine,
|
| 164 |
+
"name": db_name,
|
| 165 |
+
"user": db_user,
|
| 166 |
+
"password": db_password,
|
| 167 |
+
"host": db_host,
|
| 168 |
+
"port": db_port
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
# =====================================================
|
| 172 |
+
# SECTION 4 — MODEL MODE
|
| 173 |
+
# =====================================================
|
| 174 |
+
st.header("🧱 Model Generation")
|
| 175 |
+
|
| 176 |
+
use_ai = st.radio(
|
| 177 |
+
"How do you want to create models?",
|
| 178 |
+
["AI Generated Models", "Manual Model Builder"]
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
st.session_state.spec["use_ai_models"] = (use_ai == "AI Generated Models")
|
| 182 |
+
|
| 183 |
+
# =====================================================
|
| 184 |
+
# SECTION 5 — MANUAL MODEL BUILDER
|
| 185 |
+
# =====================================================
|
| 186 |
+
if not st.session_state.spec["use_ai_models"]:
|
| 187 |
+
st.subheader("🛠 Manual Model Builder")
|
| 188 |
+
|
| 189 |
+
if st.button("➕ Add Model"):
|
| 190 |
+
st.session_state.models_ui.append({
|
| 191 |
+
"name": "",
|
| 192 |
+
"fields": []
|
| 193 |
+
})
|
| 194 |
+
|
| 195 |
+
model_names = []
|
| 196 |
+
|
| 197 |
+
for mi, model in enumerate(st.session_state.models_ui):
|
| 198 |
+
with st.expander(f"Model {mi + 1}", expanded=True):
|
| 199 |
+
model["name"] = st.text_input(
|
| 200 |
+
"Model Name",
|
| 201 |
+
model["name"],
|
| 202 |
+
key=f"model_{mi}"
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
if model["name"]:
|
| 206 |
+
model_names.append(model["name"])
|
| 207 |
+
|
| 208 |
+
if st.button("➕ Add Column", key=f"add_col_{mi}"):
|
| 209 |
+
model["fields"].append({
|
| 210 |
+
"name": "",
|
| 211 |
+
"type": "CharField",
|
| 212 |
+
"primary_key": False,
|
| 213 |
+
"unique": False,
|
| 214 |
+
"null": False,
|
| 215 |
+
"relation": None,
|
| 216 |
+
"on_delete": "CASCADE"
|
| 217 |
+
})
|
| 218 |
+
|
| 219 |
+
for fi, field in enumerate(model["fields"]):
|
| 220 |
+
cols = st.columns(7)
|
| 221 |
+
|
| 222 |
+
field["name"] = cols[0].text_input(
|
| 223 |
+
"Column",
|
| 224 |
+
field["name"],
|
| 225 |
+
key=f"fname_{mi}_{fi}"
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
field["type"] = cols[1].selectbox(
|
| 229 |
+
"Type",
|
| 230 |
+
[
|
| 231 |
+
"CharField",
|
| 232 |
+
"IntegerField",
|
| 233 |
+
"UUIDField",
|
| 234 |
+
"BooleanField",
|
| 235 |
+
"DateField",
|
| 236 |
+
"OneToOne",
|
| 237 |
+
"OneToMany"
|
| 238 |
+
],
|
| 239 |
+
key=f"ftype_{mi}_{fi}"
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
field["primary_key"] = cols[2].checkbox(
|
| 243 |
+
"PK",
|
| 244 |
+
key=f"fpk_{mi}_{fi}"
|
| 245 |
+
)
|
| 246 |
+
field["unique"] = cols[3].checkbox(
|
| 247 |
+
"Unique",
|
| 248 |
+
key=f"funq_{mi}_{fi}"
|
| 249 |
+
)
|
| 250 |
+
field["null"] = cols[4].checkbox(
|
| 251 |
+
"Null",
|
| 252 |
+
key=f"fnull_{mi}_{fi}"
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
if field["type"] in ["OneToOne", "OneToMany"]:
|
| 256 |
+
field["relation"] = cols[5].selectbox(
|
| 257 |
+
"Reference Model",
|
| 258 |
+
model_names,
|
| 259 |
+
key=f"frel_{mi}_{fi}"
|
| 260 |
+
)
|
| 261 |
+
field["on_delete"] = cols[6].selectbox(
|
| 262 |
+
"On Delete",
|
| 263 |
+
["CASCADE", "SET_NULL", "PROTECT"],
|
| 264 |
+
key=f"fdel_{mi}_{fi}"
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
# Convert UI → spec
|
| 268 |
+
models_spec = {}
|
| 269 |
+
apis_spec = {}
|
| 270 |
+
|
| 271 |
+
for model in st.session_state.models_ui:
|
| 272 |
+
if not model["name"]:
|
| 273 |
+
continue
|
| 274 |
+
|
| 275 |
+
fields_spec = {}
|
| 276 |
+
|
| 277 |
+
for f in model["fields"]:
|
| 278 |
+
if not f["name"]:
|
| 279 |
+
continue
|
| 280 |
+
|
| 281 |
+
if f["type"] == "OneToOne":
|
| 282 |
+
fields_spec[f["name"]] = {
|
| 283 |
+
"type": "OneToOneField",
|
| 284 |
+
"to": f["relation"],
|
| 285 |
+
"on_delete": f["on_delete"],
|
| 286 |
+
"null": f["null"],
|
| 287 |
+
"unique": True
|
| 288 |
+
}
|
| 289 |
+
elif f["type"] == "OneToMany":
|
| 290 |
+
fields_spec[f["name"]] = {
|
| 291 |
+
"type": "ForeignKey",
|
| 292 |
+
"to": f["relation"],
|
| 293 |
+
"on_delete": f["on_delete"],
|
| 294 |
+
"null": f["null"]
|
| 295 |
+
}
|
| 296 |
+
else:
|
| 297 |
+
fields_spec[f["name"]] = {
|
| 298 |
+
"type": f["type"],
|
| 299 |
+
"primary_key": f["primary_key"],
|
| 300 |
+
"unique": f["unique"],
|
| 301 |
+
"null": f["null"]
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
models_spec[model["name"]] = {"fields": fields_spec}
|
| 305 |
+
apis_spec[model["name"]] = ["list", "create", "retrieve", "update", "delete"]
|
| 306 |
+
|
| 307 |
+
st.session_state.spec["apps"]["core"]["models"] = models_spec
|
| 308 |
+
st.session_state.spec["apps"]["core"]["apis"] = apis_spec
|
| 309 |
+
|
| 310 |
+
# =====================================================
|
| 311 |
+
# SECTION 6 — GENERATION
|
| 312 |
+
# =====================================================
|
| 313 |
+
st.header("🚀 Generate Project")
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
# UI elements (define once, above button)
|
| 318 |
+
progress_bar = st.progress(0)
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def ui_log(msg):
|
| 322 |
+
st.session_state.logs.append(msg)
|
| 323 |
+
log_box.write("\n".join(st.session_state.logs))
|
| 324 |
+
|
| 325 |
+
def ui_progress(value):
|
| 326 |
+
progress_bar.progress(value)
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
if st.button("Generate Backend"):
|
| 330 |
+
# ----------------------------
|
| 331 |
+
# 1️⃣ Basic validation
|
| 332 |
+
# ----------------------------
|
| 333 |
+
if not api_key:
|
| 334 |
+
st.error("API key is required to generate the project.")
|
| 335 |
+
st.stop()
|
| 336 |
+
|
| 337 |
+
st.session_state.logs.clear()
|
| 338 |
+
progress_bar.progress(0)
|
| 339 |
+
|
| 340 |
+
# ----------------------------
|
| 341 |
+
# 2️⃣ Load schema & validate UI spec
|
| 342 |
+
# ----------------------------
|
| 343 |
+
schema = load_schema()
|
| 344 |
+
|
| 345 |
+
valid, cleaned_spec, errors, warnings = validate_and_clean_spec(
|
| 346 |
+
st.session_state.spec,
|
| 347 |
+
schema
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
if not valid:
|
| 351 |
+
st.error("Specification validation failed.")
|
| 352 |
+
st.json(errors)
|
| 353 |
+
st.stop()
|
| 354 |
+
|
| 355 |
+
# ----------------------------
|
| 356 |
+
# 3️⃣ Create isolated run directory
|
| 357 |
+
# ----------------------------
|
| 358 |
+
run_id, run_dir = create_run_dir()
|
| 359 |
+
ui_log(f"🆔 Run ID: {run_id}")
|
| 360 |
+
ui_log("🚀 Starting generation pipeline...")
|
| 361 |
+
|
| 362 |
+
# ----------------------------
|
| 363 |
+
# 4️⃣ Run backend pipeline
|
| 364 |
+
# ----------------------------
|
| 365 |
+
try:
|
| 366 |
+
run_pipeline(
|
| 367 |
+
spec=cleaned_spec,
|
| 368 |
+
run_dir=run_dir,
|
| 369 |
+
llm_provider=llm_provider.lower(),
|
| 370 |
+
api_key=api_key,
|
| 371 |
+
log_callback=ui_log,
|
| 372 |
+
progress_callback=ui_progress
|
| 373 |
+
)
|
| 374 |
+
|
| 375 |
+
ui_log("📦 Zipping project...")
|
| 376 |
+
zip_path = zip_folder(run_dir)
|
| 377 |
+
|
| 378 |
+
ui_log("✅ Project ready for download")
|
| 379 |
+
|
| 380 |
+
with open(zip_path, "rb") as f:
|
| 381 |
+
st.download_button(
|
| 382 |
+
"⬇ Download Generated Project",
|
| 383 |
+
f,
|
| 384 |
+
file_name=f"{cleaned_spec['project_name']}.zip"
|
| 385 |
+
)
|
| 386 |
+
|
| 387 |
+
# ----------------------------
|
| 388 |
+
# 5️⃣ Error handling
|
| 389 |
+
# ----------------------------
|
| 390 |
+
except Exception as e:
|
| 391 |
+
ui_log("❌ Pipeline failed")
|
| 392 |
+
ui_log(str(e))
|
| 393 |
+
st.error(f"Generation failed at step: {e}")
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
# ----------------------------
|
| 397 |
+
# 6️⃣ Cleanup (safe)
|
| 398 |
+
# ----------------------------
|
| 399 |
+
finally:
|
| 400 |
+
shutil.rmtree(run_dir, ignore_errors=True)
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
# =====================================================
|
| 404 |
+
# FOOTER
|
| 405 |
+
# =====================================================
|
| 406 |
+
st.divider()
|
| 407 |
+
st.markdown(
|
| 408 |
+
"""
|
| 409 |
+
📧 **Contact:** harshadhole04@gmail.com
|
| 410 |
+
© AI REST API Generator — Under Development
|
| 411 |
+
"""
|
| 412 |
+
)
|
generator.py
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import subprocess
|
| 4 |
+
import ast
|
| 5 |
+
|
| 6 |
+
# Import strict prompts
|
| 7 |
+
from prompt import *
|
| 8 |
+
|
| 9 |
+
from langchain_openai import ChatOpenAI
|
| 10 |
+
from langchain.prompts import ChatPromptTemplate
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def add_app_to_installed_apps(settings_path: str, app_name: str):
|
| 14 |
+
with open(settings_path, "r", encoding="utf-8") as f:
|
| 15 |
+
lines = f.readlines()
|
| 16 |
+
|
| 17 |
+
in_installed_apps = False
|
| 18 |
+
already_added = False
|
| 19 |
+
new_lines = []
|
| 20 |
+
|
| 21 |
+
for line in lines:
|
| 22 |
+
if line.strip().startswith("INSTALLED_APPS"):
|
| 23 |
+
in_installed_apps = True
|
| 24 |
+
|
| 25 |
+
if in_installed_apps and f'"{app_name}"' in line:
|
| 26 |
+
already_added = True
|
| 27 |
+
|
| 28 |
+
if in_installed_apps and line.strip() == "]":
|
| 29 |
+
if not already_added:
|
| 30 |
+
new_lines.append(f' "{app_name}",\n')
|
| 31 |
+
in_installed_apps = False
|
| 32 |
+
|
| 33 |
+
new_lines.append(line)
|
| 34 |
+
|
| 35 |
+
if not already_added:
|
| 36 |
+
with open(settings_path, "w", encoding="utf-8") as f:
|
| 37 |
+
f.writelines(new_lines)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# ============================================================
|
| 41 |
+
# HELPER FUNCTIONS
|
| 42 |
+
# ============================================================
|
| 43 |
+
|
| 44 |
+
def run_cmd(command, cwd=None):
|
| 45 |
+
"""Runs terminal commands safely."""
|
| 46 |
+
result = subprocess.run(
|
| 47 |
+
command.split(),
|
| 48 |
+
cwd=cwd,
|
| 49 |
+
stdout=subprocess.PIPE,
|
| 50 |
+
stderr=subprocess.PIPE,
|
| 51 |
+
text=True
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
if result.returncode != 0:
|
| 56 |
+
print("❌ Command failed:", command)
|
| 57 |
+
print(result.stderr)
|
| 58 |
+
raise Exception(result.stderr)
|
| 59 |
+
return result.stdout
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def write_file(path, content):
|
| 63 |
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
| 64 |
+
with open(path, "w", encoding="utf8") as f:
|
| 65 |
+
f.write(content)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def is_valid_python(code: str) -> bool:
|
| 69 |
+
try:
|
| 70 |
+
ast.parse(code)
|
| 71 |
+
return True
|
| 72 |
+
except:
|
| 73 |
+
return False
|
| 74 |
+
|
| 75 |
+
# ===================README FILE GENERATOR ===================
|
| 76 |
+
|
| 77 |
+
def generate_readme_with_llm(spec_json, project_path, llm):
|
| 78 |
+
chain = ChatPromptTemplate.from_messages([
|
| 79 |
+
("system", README_PROMPT),
|
| 80 |
+
("user", "{json_input}")
|
| 81 |
+
])
|
| 82 |
+
|
| 83 |
+
runnable = chain | llm
|
| 84 |
+
result = runnable.invoke({
|
| 85 |
+
"json_input": json.dumps(spec_json, indent=2)
|
| 86 |
+
})
|
| 87 |
+
|
| 88 |
+
readme_content = result.content.strip()
|
| 89 |
+
|
| 90 |
+
write_file(os.path.join(project_path, "README.md"), readme_content)
|
| 91 |
+
print("📘 README.md generated successfully.")
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
# ============================================================
|
| 95 |
+
# CUSTOM FILE VALIDATORS
|
| 96 |
+
# ============================================================
|
| 97 |
+
|
| 98 |
+
def is_valid_serializer(code):
|
| 99 |
+
"""Rejects dynamic serializer patterns."""
|
| 100 |
+
forbidden = ["globals(", "type(", "for ", "_create", "json_input"]
|
| 101 |
+
if any(f in code for f in forbidden):
|
| 102 |
+
return False
|
| 103 |
+
if "class " not in code:
|
| 104 |
+
return False
|
| 105 |
+
if "Serializer" not in code:
|
| 106 |
+
return False
|
| 107 |
+
return is_valid_python(code)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def is_valid_views(code):
|
| 111 |
+
"""Reject dynamic views or helper functions."""
|
| 112 |
+
forbidden = ["globals(", "type(", "_create", "for ", "json_input"]
|
| 113 |
+
if any(f in code for f in forbidden):
|
| 114 |
+
return False
|
| 115 |
+
if "APIView" not in code:
|
| 116 |
+
return False
|
| 117 |
+
return is_valid_python(code)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def is_valid_urls(code):
|
| 121 |
+
forbidden = ["globals(", "type(", "_create", "json_input"]
|
| 122 |
+
if any(f in code for f in forbidden):
|
| 123 |
+
return False
|
| 124 |
+
|
| 125 |
+
if "urlpatterns" not in code:
|
| 126 |
+
return False
|
| 127 |
+
if "path(" not in code:
|
| 128 |
+
return False
|
| 129 |
+
if "<int:pk>" not in code:
|
| 130 |
+
return False
|
| 131 |
+
|
| 132 |
+
# No leading slash allowed
|
| 133 |
+
if 'path("/' in code or "path('/" in code:
|
| 134 |
+
return False
|
| 135 |
+
|
| 136 |
+
return is_valid_python(code)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
# ============================================================
|
| 140 |
+
# GENERATION LOGIC (AUTO-FIX)
|
| 141 |
+
# ============================================================
|
| 142 |
+
|
| 143 |
+
def generate_code_with_fix(
|
| 144 |
+
prompt,
|
| 145 |
+
json_slice,
|
| 146 |
+
*,
|
| 147 |
+
llm,
|
| 148 |
+
validator=None,
|
| 149 |
+
retries=5,
|
| 150 |
+
):
|
| 151 |
+
|
| 152 |
+
for attempt in range(retries):
|
| 153 |
+
print(f"🧠 Generating (attempt {attempt+1})...")
|
| 154 |
+
|
| 155 |
+
chain = ChatPromptTemplate.from_messages([
|
| 156 |
+
("system", prompt),
|
| 157 |
+
("user", "{json_input}")
|
| 158 |
+
])
|
| 159 |
+
|
| 160 |
+
runnable = chain | llm
|
| 161 |
+
result = runnable.invoke({
|
| 162 |
+
"json_input": json.dumps(json_slice, indent=2)
|
| 163 |
+
})
|
| 164 |
+
|
| 165 |
+
code = result.content.strip()
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
# Use custom validator if provided
|
| 169 |
+
if validator:
|
| 170 |
+
if validator(code):
|
| 171 |
+
print("✅ File valid.")
|
| 172 |
+
return code
|
| 173 |
+
else:
|
| 174 |
+
# Syntax-only validation
|
| 175 |
+
if is_valid_python(code):
|
| 176 |
+
print("✅ File valid.")
|
| 177 |
+
return code
|
| 178 |
+
|
| 179 |
+
print("❌ Invalid file. Regenerating...")
|
| 180 |
+
|
| 181 |
+
raise Exception("❌ Could not generate a valid file after retries.")
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def generate_urls_with_fix(prompt, json_slice, *, llm, retries=5):
|
| 185 |
+
return generate_code_with_fix(prompt, json_slice, validator=is_valid_urls,llm=llm, retries=retries)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
# ============================================================
|
| 189 |
+
# MAIN PROJECT GENERATOR
|
| 190 |
+
# ============================================================
|
| 191 |
+
|
| 192 |
+
import os
|
| 193 |
+
import sys
|
| 194 |
+
|
| 195 |
+
def generate_full_project(spec_json, output_dir, llm, project_name):
|
| 196 |
+
print("generating project")
|
| 197 |
+
|
| 198 |
+
print("creating path")
|
| 199 |
+
|
| 200 |
+
project_path = os.path.join(output_dir, project_name)
|
| 201 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 202 |
+
|
| 203 |
+
PYTHON = sys.executable # ✅ always correct python
|
| 204 |
+
|
| 205 |
+
# -----------------------------
|
| 206 |
+
# 1) CREATE DJANGO PROJECT
|
| 207 |
+
# -----------------------------
|
| 208 |
+
try:
|
| 209 |
+
print("🚀 Creating Django project...")
|
| 210 |
+
run_cmd(f"{PYTHON} -m django startproject {project_name}", cwd=output_dir)
|
| 211 |
+
except Exception as e:
|
| 212 |
+
raise RuntimeError(f"Failed to create Django project '{project_name}'") from e
|
| 213 |
+
|
| 214 |
+
# -----------------------------
|
| 215 |
+
# 2) CREATE APPS
|
| 216 |
+
# -----------------------------
|
| 217 |
+
try:
|
| 218 |
+
for app in spec_json.get("apps", {}):
|
| 219 |
+
print(f"📦 Creating app: {app}")
|
| 220 |
+
run_cmd(f"{PYTHON} manage.py startapp {app}", cwd=project_path)
|
| 221 |
+
|
| 222 |
+
settings_path = os.path.join(
|
| 223 |
+
project_path,
|
| 224 |
+
project_name,
|
| 225 |
+
"settings.py"
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
add_app_to_installed_apps(settings_path, app)
|
| 229 |
+
|
| 230 |
+
except Exception as e:
|
| 231 |
+
raise RuntimeError("Failed while creating Django apps") from e
|
| 232 |
+
|
| 233 |
+
# -----------------------------
|
| 234 |
+
# 3) GENERATE FILES FOR EACH APP
|
| 235 |
+
# -----------------------------
|
| 236 |
+
for app_name, app_spec in spec_json.get("apps", {}).items():
|
| 237 |
+
try:
|
| 238 |
+
print(f"📝 Generating code for app: {app_name}")
|
| 239 |
+
app_dir = os.path.join(project_path, app_name)
|
| 240 |
+
|
| 241 |
+
if not os.path.exists(app_dir):
|
| 242 |
+
raise FileNotFoundError(f"App directory not found: {app_dir}")
|
| 243 |
+
|
| 244 |
+
models_slice = {"models": app_spec.get("models", {})}
|
| 245 |
+
|
| 246 |
+
serializer_slice = {
|
| 247 |
+
"model_names": sorted(app_spec.get("models", {}).keys())
|
| 248 |
+
}
|
| 249 |
+
admin_slice = {"model_names": list(app_spec.get("models", {}).keys())}
|
| 250 |
+
|
| 251 |
+
views_slice = {
|
| 252 |
+
"model_names": list(app_spec.get("models", {}).keys()),
|
| 253 |
+
"apis": app_spec.get("apis", {})
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
urls_slice = {
|
| 257 |
+
"model_names": list(app_spec.get("models", {}).keys()),
|
| 258 |
+
"apis": app_spec.get("apis", {}),
|
| 259 |
+
"base_url": spec_json.get("api_config", {}).get("base_url", "/api/")
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
models_code = generate_code_with_fix(
|
| 263 |
+
MODELS_PROMPT, models_slice, validator=is_valid_python, llm=llm
|
| 264 |
+
)
|
| 265 |
+
write_file(os.path.join(app_dir, "models.py"), models_code)
|
| 266 |
+
|
| 267 |
+
serializers_code = generate_code_with_fix(
|
| 268 |
+
SERIALIZERS_PROMPT, serializer_slice, validator=is_valid_serializer, llm=llm
|
| 269 |
+
)
|
| 270 |
+
write_file(os.path.join(app_dir, "serializers.py"), serializers_code)
|
| 271 |
+
|
| 272 |
+
views_code = generate_code_with_fix(
|
| 273 |
+
VIEWS_PROMPT, views_slice, validator=is_valid_views, llm=llm
|
| 274 |
+
)
|
| 275 |
+
write_file(os.path.join(app_dir, "views.py"), views_code)
|
| 276 |
+
|
| 277 |
+
urls_code = generate_urls_with_fix(
|
| 278 |
+
URLS_PROMPT, urls_slice, llm=llm
|
| 279 |
+
)
|
| 280 |
+
write_file(os.path.join(app_dir, "urls.py"), urls_code)
|
| 281 |
+
|
| 282 |
+
admin_code = generate_code_with_fix(
|
| 283 |
+
ADMIN_PROMPT, admin_slice, validator=is_valid_python, llm=llm
|
| 284 |
+
)
|
| 285 |
+
write_file(os.path.join(app_dir, "admin.py"), admin_code)
|
| 286 |
+
|
| 287 |
+
except Exception as e:
|
| 288 |
+
raise RuntimeError(f"Code generation failed for app '{app_name}'") from e
|
| 289 |
+
|
| 290 |
+
# -----------------------------
|
| 291 |
+
# 4) GENERATE REQUIREMENTS
|
| 292 |
+
# -----------------------------
|
| 293 |
+
try:
|
| 294 |
+
requirements_slice = {
|
| 295 |
+
"auth": spec_json.get("auth", {}),
|
| 296 |
+
"database": spec_json.get("database", {}),
|
| 297 |
+
"deployment": spec_json.get("deployment", {})
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
requirements_code = generate_code_with_fix(
|
| 301 |
+
REQUIREMENTS_PROMPT, requirements_slice, llm=llm
|
| 302 |
+
)
|
| 303 |
+
write_file(os.path.join(project_path, "requirements.txt"), requirements_code)
|
| 304 |
+
|
| 305 |
+
except Exception as e:
|
| 306 |
+
raise RuntimeError("Failed to generate requirements.txt") from e
|
| 307 |
+
|
| 308 |
+
# -----------------------------
|
| 309 |
+
# 5) PROJECT URLS
|
| 310 |
+
# -----------------------------
|
| 311 |
+
try:
|
| 312 |
+
project_urls_slice = {
|
| 313 |
+
"project_name": project_name,
|
| 314 |
+
"apps": list(spec_json.get("apps", {}).keys()),
|
| 315 |
+
"base_url": spec_json.get("api_config", {}).get("base_url", "/api/")
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
project_urls_code = generate_code_with_fix(
|
| 319 |
+
PROJECT_URLS_PROMPT,
|
| 320 |
+
project_urls_slice,
|
| 321 |
+
validator=is_valid_python, llm=llm
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
write_file(
|
| 325 |
+
os.path.join(project_path, project_name, "urls.py"),
|
| 326 |
+
project_urls_code
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
except Exception as e:
|
| 330 |
+
raise RuntimeError("Failed to generate project urls.py") from e
|
| 331 |
+
|
| 332 |
+
# -----------------------------
|
| 333 |
+
# 6) README (OPTIONAL)
|
| 334 |
+
# -----------------------------
|
| 335 |
+
try:
|
| 336 |
+
generate_readme_with_llm(spec_json, project_path, llm=llm)
|
| 337 |
+
except Exception as e:
|
| 338 |
+
print("⚠ README generation failed (non-blocking):", e)
|
| 339 |
+
|
| 340 |
+
print("\n🎉 DONE! Project created at:", project_path)
|
| 341 |
+
return project_path
|
| 342 |
+
|
| 343 |
+
def load_json_spec(path="json_output/spec.json"):
|
| 344 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 345 |
+
return json.load(f)
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
#=============================Prompt_With_Model_Specification================
|
| 349 |
+
from langchain.prompts import ChatPromptTemplate
|
| 350 |
+
|
| 351 |
+
def generate_django_model_prompt(project_name: str, description: str, llm):
|
| 352 |
+
"""
|
| 353 |
+
Uses LLM to generate a high-quality, structured prompt
|
| 354 |
+
for Django model development with clear specifications.
|
| 355 |
+
"""
|
| 356 |
+
|
| 357 |
+
prompt = ChatPromptTemplate.from_messages([
|
| 358 |
+
("system", """
|
| 359 |
+
You are a senior Django backend architect with production experience.
|
| 360 |
+
|
| 361 |
+
Your task is to generate a SINGLE, CLEAN, PLAIN-TEXT PROMPT
|
| 362 |
+
that will later be used to generate Django models.py.
|
| 363 |
+
|
| 364 |
+
STRICT RULES:
|
| 365 |
+
- Output ONLY plain text.
|
| 366 |
+
- Do NOT use markdown, bullet points, headings, or code blocks.
|
| 367 |
+
- Do NOT include explanations or commentary.
|
| 368 |
+
- Do NOT generate Django code.
|
| 369 |
+
- Generate ONLY the final prompt text.
|
| 370 |
+
|
| 371 |
+
MODEL DESIGN RULES:
|
| 372 |
+
- Follow Django ORM best practices.
|
| 373 |
+
- Use deterministic, production-ready model structures.
|
| 374 |
+
- Infer missing models or fields if required to complete the system.
|
| 375 |
+
- Never invent unnecessary models.
|
| 376 |
+
- Every model MUST have a clear real-world purpose.
|
| 377 |
+
|
| 378 |
+
FIELD RULES:
|
| 379 |
+
- Infer sensible fields when the user does not specify all required fields.
|
| 380 |
+
- Use correct Django field types.
|
| 381 |
+
- Add timestamps (created_at, updated_at) when appropriate.
|
| 382 |
+
- Use UUID primary keys where suitable.
|
| 383 |
+
- Do NOT use dynamic patterns or metaprogramming.
|
| 384 |
+
- Ensure field names are snake_case.
|
| 385 |
+
- Ensure model names are PascalCase.
|
| 386 |
+
|
| 387 |
+
RELATIONSHIP RULES:
|
| 388 |
+
- Infer relationships only when logically required.
|
| 389 |
+
- Use ForeignKey for one-to-many relationships.
|
| 390 |
+
- Use OneToOneField only when explicitly required.
|
| 391 |
+
- Avoid ManyToMany unless clearly necessary.
|
| 392 |
+
|
| 393 |
+
META RULES:
|
| 394 |
+
- Include Meta options such as db_table and ordering.
|
| 395 |
+
- Include __str__ methods for all models.
|
| 396 |
+
- Ensure compatibility with Django REST Framework serializers and views.
|
| 397 |
+
|
| 398 |
+
OUTPUT QUALITY RULE:
|
| 399 |
+
The generated prompt must be precise, minimal, and implementation-ready,
|
| 400 |
+
so that another LLM can generate models.py without making assumptions.
|
| 401 |
+
"""),
|
| 402 |
+
("user", """
|
| 403 |
+
Project Name: {project_name}
|
| 404 |
+
|
| 405 |
+
User Requirements:
|
| 406 |
+
{description}
|
| 407 |
+
|
| 408 |
+
Generate a single, concise, implementation-ready PROMPT
|
| 409 |
+
that instructs an LLM to generate Django models.py.
|
| 410 |
+
""")
|
| 411 |
+
])
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
chain = prompt | llm
|
| 415 |
+
result = chain.invoke({
|
| 416 |
+
"project_name": project_name,
|
| 417 |
+
"description": description
|
| 418 |
+
})
|
| 419 |
+
|
| 420 |
+
return result.content.strip()
|
pipeline.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# pipeline.py
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
from additional_function import get_llm
|
| 6 |
+
from generator import generate_full_project, generate_django_model_prompt
|
| 7 |
+
from spec_validator import generate_valid_json_spec
|
| 8 |
+
from settings_generator import update_settings_py
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def run_pipeline(
|
| 12 |
+
spec: dict,
|
| 13 |
+
run_dir: str,
|
| 14 |
+
llm_provider: str,
|
| 15 |
+
api_key: str,
|
| 16 |
+
log_callback=None,
|
| 17 |
+
progress_callback=None,
|
| 18 |
+
retries: int = 3,
|
| 19 |
+
):
|
| 20 |
+
"""
|
| 21 |
+
Orchestrates full backend generation pipeline.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def log(msg: str):
|
| 25 |
+
if log_callback:
|
| 26 |
+
log_callback(msg)
|
| 27 |
+
|
| 28 |
+
def progress(val: int):
|
| 29 |
+
if progress_callback:
|
| 30 |
+
progress_callback(val)
|
| 31 |
+
|
| 32 |
+
# -------------------------------------------------
|
| 33 |
+
# STEP 0 — Init
|
| 34 |
+
# -------------------------------------------------
|
| 35 |
+
progress(0)
|
| 36 |
+
log("🚀 Starting generation pipeline")
|
| 37 |
+
|
| 38 |
+
# -------------------------------------------------
|
| 39 |
+
# STEP 1 — Create LLM
|
| 40 |
+
# -------------------------------------------------
|
| 41 |
+
try:
|
| 42 |
+
progress(10)
|
| 43 |
+
log("🔑 Initializing LLM")
|
| 44 |
+
|
| 45 |
+
llm = get_llm(llm_provider, api_key)
|
| 46 |
+
|
| 47 |
+
log("✅ LLM initialized successfully")
|
| 48 |
+
|
| 49 |
+
except Exception as e:
|
| 50 |
+
raise RuntimeError("LLM initialization failed") from e
|
| 51 |
+
|
| 52 |
+
# -------------------------------------------------
|
| 53 |
+
# STEP 2 — Build user prompt
|
| 54 |
+
# -------------------------------------------------
|
| 55 |
+
try:
|
| 56 |
+
progress(25)
|
| 57 |
+
log("🧠 Preparing user prompt")
|
| 58 |
+
|
| 59 |
+
if spec.get("use_ai_models", True):
|
| 60 |
+
log("Using AI to generate models")
|
| 61 |
+
|
| 62 |
+
user_prompt = generate_django_model_prompt(
|
| 63 |
+
project_name=spec.get("project_name"),
|
| 64 |
+
description=spec.get("description"),
|
| 65 |
+
llm=llm
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
else:
|
| 69 |
+
log("Using manually defined models")
|
| 70 |
+
|
| 71 |
+
model_summary = []
|
| 72 |
+
for model_name, model_def in spec["apps"]["core"]["models"].items():
|
| 73 |
+
fields = ", ".join(model_def["fields"].keys())
|
| 74 |
+
model_summary.append(f"{model_name}({fields})")
|
| 75 |
+
|
| 76 |
+
user_prompt = f"""
|
| 77 |
+
Project Name: {spec.get("project_name")}
|
| 78 |
+
Description: {spec.get("description")}
|
| 79 |
+
|
| 80 |
+
Models:
|
| 81 |
+
{', '.join(model_summary)}
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
log("✅ User prompt prepared")
|
| 85 |
+
|
| 86 |
+
except Exception as e:
|
| 87 |
+
raise RuntimeError("User prompt generation failed") from e
|
| 88 |
+
|
| 89 |
+
# -------------------------------------------------
|
| 90 |
+
# STEP 3 — Generate JSON Spec
|
| 91 |
+
# -------------------------------------------------
|
| 92 |
+
try:
|
| 93 |
+
progress(45)
|
| 94 |
+
log("📄 Generating JSON specification")
|
| 95 |
+
|
| 96 |
+
cleaned_spec = generate_valid_json_spec(
|
| 97 |
+
user_prompt=user_prompt,
|
| 98 |
+
llm=llm,
|
| 99 |
+
retries=retries
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
log("✅ JSON spec generated successfully")
|
| 103 |
+
|
| 104 |
+
except Exception as e:
|
| 105 |
+
raise RuntimeError("JSON spec generation failed") from e
|
| 106 |
+
|
| 107 |
+
# -------------------------------------------------
|
| 108 |
+
# STEP 4 — Generate Django Project
|
| 109 |
+
# -------------------------------------------------
|
| 110 |
+
try:
|
| 111 |
+
progress(75)
|
| 112 |
+
project_name = spec.get("project_name")
|
| 113 |
+
log("🛠 Generating Django project")
|
| 114 |
+
|
| 115 |
+
generate_full_project(
|
| 116 |
+
spec_json=cleaned_spec,
|
| 117 |
+
output_dir=run_dir,
|
| 118 |
+
llm=llm,
|
| 119 |
+
project_name =project_name
|
| 120 |
+
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
log("✅ Django project created")
|
| 124 |
+
|
| 125 |
+
except Exception as e:
|
| 126 |
+
log("❌ INTERNAL ERROR during Django project generation:")
|
| 127 |
+
log(repr(e)) # <-- THIS IS CRITICAL
|
| 128 |
+
raise RuntimeError("Django project generation failed") from e
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
# -------------------------------------------------
|
| 132 |
+
# STEP 5 — Update settings.py
|
| 133 |
+
# -------------------------------------------------
|
| 134 |
+
try:
|
| 135 |
+
progress(90)
|
| 136 |
+
log("⚙ Updating Django settings")
|
| 137 |
+
|
| 138 |
+
settings_path = os.path.join(
|
| 139 |
+
run_dir,
|
| 140 |
+
cleaned_spec["project_name"],
|
| 141 |
+
cleaned_spec["project_name"],
|
| 142 |
+
"settings.py"
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
update_settings_py(
|
| 146 |
+
settings_path=settings_path,
|
| 147 |
+
db_spec=cleaned_spec.get("database", {})
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
log("✅ settings.py updated")
|
| 151 |
+
|
| 152 |
+
except Exception as e:
|
| 153 |
+
raise RuntimeError("Django settings update failed") from e
|
| 154 |
+
|
| 155 |
+
# -------------------------------------------------
|
| 156 |
+
# DONE
|
| 157 |
+
# -------------------------------------------------
|
| 158 |
+
progress(100)
|
| 159 |
+
log("🎉 Generation completed successfully")
|
| 160 |
+
|
| 161 |
+
return run_dir
|
prompt.py
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
SYSTEM_PROMPT = """You are an expert backend architect.
|
| 2 |
+
|
| 3 |
+
Your task is to convert the user’s requirements into a STRICT JSON specification
|
| 4 |
+
that EXACTLY matches the provided schema for a minimal Django REST API backend.
|
| 5 |
+
|
| 6 |
+
ABSOLUTE RULES:
|
| 7 |
+
- Output ONLY a valid JSON object.
|
| 8 |
+
- Do NOT include markdown, comments, explanations, or extra text.
|
| 9 |
+
- The JSON MUST match the schema EXACTLY.
|
| 10 |
+
- Missing required keys are NOT allowed.
|
| 11 |
+
- Extra keys NOT defined in the schema are NOT allowed.
|
| 12 |
+
- Populate ALL required fields.
|
| 13 |
+
|
| 14 |
+
NAMING RULES:
|
| 15 |
+
- project_name MUST be lowercase with underscores only.
|
| 16 |
+
- Model names MUST be PascalCase.
|
| 17 |
+
- Field names MUST be snake_case.
|
| 18 |
+
- Names must be deterministic and meaningful.
|
| 19 |
+
|
| 20 |
+
DATABASE RULES:
|
| 21 |
+
- If database is not specified, use:
|
| 22 |
+
{{
|
| 23 |
+
"engine": "sqlite",
|
| 24 |
+
"name": "db.sqlite3"
|
| 25 |
+
}}
|
| 26 |
+
|
| 27 |
+
AUTH RULES:
|
| 28 |
+
- If the user mentions authentication, login, JWT, token, or security → auth.type = "jwt".
|
| 29 |
+
- Otherwise → auth.type = "session".
|
| 30 |
+
|
| 31 |
+
MODEL RULES:
|
| 32 |
+
- ALWAYS generate at least one model.
|
| 33 |
+
- Each model MUST have at least one field.
|
| 34 |
+
- Every model MUST appear in BOTH core.models and core.apis.
|
| 35 |
+
- Use ONLY the allowed Django field types from the schema.
|
| 36 |
+
- Do NOT add field options not defined in the schema.
|
| 37 |
+
|
| 38 |
+
FIELD INFERENCE RULES:
|
| 39 |
+
- name, title → CharField
|
| 40 |
+
- email → EmailField
|
| 41 |
+
- description, content → TextField
|
| 42 |
+
- age, count → IntegerField
|
| 43 |
+
- created_at, updated_at → DateTimeField
|
| 44 |
+
- relation fields → ForeignKey
|
| 45 |
+
|
| 46 |
+
RELATIONSHIP RULES:
|
| 47 |
+
- Use ForeignKey ONLY if a relationship is explicitly required.
|
| 48 |
+
- ForeignKey fields MUST include:
|
| 49 |
+
{{
|
| 50 |
+
"type": "ForeignKey",
|
| 51 |
+
"to": "ModelName"
|
| 52 |
+
}}
|
| 53 |
+
|
| 54 |
+
API RULES:
|
| 55 |
+
- Each model MUST define all CRUD actions:
|
| 56 |
+
["list", "create", "retrieve", "update", "delete"].
|
| 57 |
+
- APIs MUST be placed under core.apis.
|
| 58 |
+
|
| 59 |
+
API CONFIG RULES:
|
| 60 |
+
- api_config.base_url MUST always be "/api/".
|
| 61 |
+
|
| 62 |
+
OUTPUT RULES:
|
| 63 |
+
- Output ONLY the final JSON object.
|
| 64 |
+
- The JSON must be directly parsable.
|
| 65 |
+
- No trailing commas.
|
| 66 |
+
- No formatting or styling.
|
| 67 |
+
|
| 68 |
+
FORBIDDEN:
|
| 69 |
+
- Do NOT generate overview, dependencies, code, examples, instructions, endpoints, or explanations.
|
| 70 |
+
- Output ONLY fields defined in the schema.
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
Follow the schema exactly and generate a clean, minimal, production-ready JSON specification.
|
| 74 |
+
|
| 75 |
+
"""
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
README_PROMPT = """
|
| 79 |
+
You are a senior backend engineer.
|
| 80 |
+
|
| 81 |
+
Generate a professional README.md for a Django REST API project.
|
| 82 |
+
|
| 83 |
+
The README must include:
|
| 84 |
+
1. Project overview
|
| 85 |
+
2. Tech stack
|
| 86 |
+
3. Setup instructions (virtualenv, install requirements)
|
| 87 |
+
4. Environment variables (if any)
|
| 88 |
+
5. Database setup and migrations
|
| 89 |
+
6. How to run the server
|
| 90 |
+
7. API usage instructions
|
| 91 |
+
8. Example API endpoints (CRUD)
|
| 92 |
+
9. Authentication instructions (if enabled)
|
| 93 |
+
10. Folder structure overview
|
| 94 |
+
|
| 95 |
+
Rules:
|
| 96 |
+
- Use Markdown
|
| 97 |
+
- Be concise but complete
|
| 98 |
+
- Assume the project is auto-generated
|
| 99 |
+
"""
|
| 100 |
+
|
| 101 |
+
MODELS_PROMPT = """
|
| 102 |
+
You are an expert Django developer.
|
| 103 |
+
|
| 104 |
+
CRITICAL RULES:
|
| 105 |
+
- Output ONLY valid Python code for models.py.
|
| 106 |
+
- NO markdown, NO comments, NO explanation.
|
| 107 |
+
- DO NOT reference json_input inside the output.
|
| 108 |
+
- DO NOT create helper functions, loops, or dynamic code.
|
| 109 |
+
- Must begin with EXACTLY:
|
| 110 |
+
|
| 111 |
+
from django.db import models
|
| 112 |
+
|
| 113 |
+
json_input["models"] has this structure:
|
| 114 |
+
{{
|
| 115 |
+
"Doctor": {{ "fields": {{...}} }},
|
| 116 |
+
"Patient": {{ "fields": {{...}} }}
|
| 117 |
+
}}
|
| 118 |
+
|
| 119 |
+
For each model:
|
| 120 |
+
Generate ONE Django model class.
|
| 121 |
+
|
| 122 |
+
FIELD RULES:
|
| 123 |
+
- CharField -> models.CharField(max_length=<value>)
|
| 124 |
+
- EmailField -> models.EmailField(max_length=<value>)
|
| 125 |
+
- TextField -> models.TextField()
|
| 126 |
+
- IntegerField -> models.IntegerField()
|
| 127 |
+
- FloatField -> models.FloatField()
|
| 128 |
+
- BooleanField -> models.BooleanField()
|
| 129 |
+
- DateField -> models.DateField()
|
| 130 |
+
- DateTimeField -> models.DateTimeField()
|
| 131 |
+
|
| 132 |
+
FOREIGN KEY RULE:
|
| 133 |
+
- type = "ForeignKey"
|
| 134 |
+
- Output:
|
| 135 |
+
models.ForeignKey("<to>", on_delete=models.CASCADE)
|
| 136 |
+
|
| 137 |
+
ATTRIBUTE RULES:
|
| 138 |
+
- required = false -> blank=True, null=True
|
| 139 |
+
- unique = true -> unique=True
|
| 140 |
+
- max_length MUST be included for CharField and EmailField
|
| 141 |
+
|
| 142 |
+
DO NOT add:
|
| 143 |
+
- __str__
|
| 144 |
+
- Meta class
|
| 145 |
+
- extra imports
|
| 146 |
+
|
| 147 |
+
Output ONLY the classes.
|
| 148 |
+
"""
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
SERIALIZERS_PROMPT = """
|
| 154 |
+
You are an expert Django REST Framework developer.
|
| 155 |
+
|
| 156 |
+
CRITICAL RULES:
|
| 157 |
+
- Output ONLY valid Python code for serializers.py.
|
| 158 |
+
- NO markdown, NO comments, NO explanation.
|
| 159 |
+
- DO NOT reference json_input inside the output.
|
| 160 |
+
- DO NOT generate loops, conditionals, dictionaries, globals(), or dynamic class creation.
|
| 161 |
+
- DO NOT import unused models.
|
| 162 |
+
|
| 163 |
+
INPUT FORMAT:
|
| 164 |
+
json_input = {{
|
| 165 |
+
"model_names": ["Post", "User"]
|
| 166 |
+
}}
|
| 167 |
+
|
| 168 |
+
REQUIRED IMPORTS:
|
| 169 |
+
from rest_framework import serializers
|
| 170 |
+
from .models import Post, User # use EXACT names from model_names
|
| 171 |
+
|
| 172 |
+
FOR EACH model name in model_names:
|
| 173 |
+
Generate EXACTLY this structure:
|
| 174 |
+
|
| 175 |
+
class ModelNameSerializer(serializers.ModelSerializer):
|
| 176 |
+
class Meta:
|
| 177 |
+
model = ModelName
|
| 178 |
+
fields = "__all__"
|
| 179 |
+
|
| 180 |
+
STRICT OUTPUT RULES:
|
| 181 |
+
- Output ONLY the serializer classes.
|
| 182 |
+
- One serializer per model.
|
| 183 |
+
- Order serializers in the same order as model_names.
|
| 184 |
+
"""
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
VIEWS_PROMPT = """
|
| 190 |
+
You are an expert Django REST Framework developer.
|
| 191 |
+
|
| 192 |
+
CRITICAL RULES:
|
| 193 |
+
- Output ONLY valid Python code for views.py.
|
| 194 |
+
- NO markdown, NO comments, NO explanation.
|
| 195 |
+
- DO NOT reference json_input in output.
|
| 196 |
+
- DO NOT generate loops, helper functions, globals(), or dynamic code.
|
| 197 |
+
|
| 198 |
+
Required imports ONLY:
|
| 199 |
+
from rest_framework import generics
|
| 200 |
+
from .models import *
|
| 201 |
+
from .serializers import *
|
| 202 |
+
|
| 203 |
+
json_input contains:
|
| 204 |
+
- model_names (["Doctor","Patient"])
|
| 205 |
+
- apis such as:
|
| 206 |
+
"Doctor": ["list","create","retrieve","update","delete"]
|
| 207 |
+
|
| 208 |
+
For each model:
|
| 209 |
+
|
| 210 |
+
IF it has "list" or "create" in apis:
|
| 211 |
+
Generate:
|
| 212 |
+
|
| 213 |
+
class ModelNameListCreateAPIView(generics.ListCreateAPIView):
|
| 214 |
+
queryset = ModelName.objects.all()
|
| 215 |
+
serializer_class = ModelNameSerializer
|
| 216 |
+
|
| 217 |
+
IF it has ANY of ["retrieve","update","delete"]:
|
| 218 |
+
Generate:
|
| 219 |
+
|
| 220 |
+
class ModelNameRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView):
|
| 221 |
+
queryset = ModelName.objects.all()
|
| 222 |
+
serializer_class = ModelNameSerializer
|
| 223 |
+
|
| 224 |
+
Output ONLY class definitions.
|
| 225 |
+
"""
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
URLS_PROMPT = """
|
| 231 |
+
You are an expert Django developer.
|
| 232 |
+
|
| 233 |
+
CRITICAL RULES:
|
| 234 |
+
- Output ONLY valid Python code for urls.py.
|
| 235 |
+
- NO markdown, NO comments, NO explanation.
|
| 236 |
+
- DO NOT reference json_input inside output.
|
| 237 |
+
- DO NOT use leading slashes.
|
| 238 |
+
- DO NOT generate dynamic code.
|
| 239 |
+
|
| 240 |
+
Required imports:
|
| 241 |
+
from django.urls import path
|
| 242 |
+
from . import views
|
| 243 |
+
|
| 244 |
+
Model names are provided in json_input["model_names"].
|
| 245 |
+
Plural name = model.lower() + "s"
|
| 246 |
+
|
| 247 |
+
For EACH model:
|
| 248 |
+
Generate EXACTLY these two routes:
|
| 249 |
+
|
| 250 |
+
path("<plural>/", views.ModelNameListCreateAPIView.as_view()),
|
| 251 |
+
path("<plural>/<int:pk>/", views.ModelNameRetrieveUpdateDestroyAPIView.as_view()),
|
| 252 |
+
|
| 253 |
+
Wrap everything inside:
|
| 254 |
+
|
| 255 |
+
urlpatterns = [
|
| 256 |
+
...
|
| 257 |
+
]
|
| 258 |
+
|
| 259 |
+
Output ONLY urlpatterns code.
|
| 260 |
+
"""
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
SETTINGS_PROMPT = """
|
| 266 |
+
You are an expert Django architect.
|
| 267 |
+
|
| 268 |
+
CRITICAL RULES:
|
| 269 |
+
- Output ONLY valid Python code for settings.py.
|
| 270 |
+
- NO markdown, NO comments, NO explanation.
|
| 271 |
+
- DO NOT reference json_input inside output.
|
| 272 |
+
- DO NOT add unused settings.
|
| 273 |
+
|
| 274 |
+
json_input contains:
|
| 275 |
+
- project_name
|
| 276 |
+
- database
|
| 277 |
+
- auth
|
| 278 |
+
- api_config
|
| 279 |
+
- deployment
|
| 280 |
+
- apps
|
| 281 |
+
|
| 282 |
+
MUST include:
|
| 283 |
+
- BASE_DIR
|
| 284 |
+
- SECRET_KEY
|
| 285 |
+
- DEBUG = True
|
| 286 |
+
- ALLOWED_HOSTS = ["*"]
|
| 287 |
+
|
| 288 |
+
INSTALLED_APPS MUST include:
|
| 289 |
+
- django.contrib.admin
|
| 290 |
+
- django.contrib.auth
|
| 291 |
+
- django.contrib.contenttypes
|
| 292 |
+
- django.contrib.sessions
|
| 293 |
+
- django.contrib.messages
|
| 294 |
+
- django.contrib.staticfiles
|
| 295 |
+
- rest_framework
|
| 296 |
+
- every app from json_input["apps"]
|
| 297 |
+
|
| 298 |
+
MIDDLEWARE must be exactly the standard Django middleware list.
|
| 299 |
+
|
| 300 |
+
DATABASE RULES:
|
| 301 |
+
If database.engine == "sqlite":
|
| 302 |
+
ENGINE = "django.db.backends.sqlite3"
|
| 303 |
+
NAME = BASE_DIR / "db.sqlite3"
|
| 304 |
+
|
| 305 |
+
If database.engine == "postgresql":
|
| 306 |
+
ENGINE = "django.db.backends.postgresql"
|
| 307 |
+
NAME, USER, PASSWORD, HOST, PORT from json_input["database"].
|
| 308 |
+
|
| 309 |
+
TEMPLATES BLOCK MUST BE EXACTLY:
|
| 310 |
+
|
| 311 |
+
TEMPLATES = [
|
| 312 |
+
{{
|
| 313 |
+
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
| 314 |
+
"DIRS": [BASE_DIR / "templates"],
|
| 315 |
+
"APP_DIRS": True,
|
| 316 |
+
"OPTIONS": {{
|
| 317 |
+
"context_processors": [
|
| 318 |
+
"django.template.context_processors.debug",
|
| 319 |
+
"django.template.context_processors.request",
|
| 320 |
+
"django.contrib.auth.context_processors.auth",
|
| 321 |
+
"django.contrib.messages.context_processors.messages",
|
| 322 |
+
],
|
| 323 |
+
}},
|
| 324 |
+
}},
|
| 325 |
+
]
|
| 326 |
+
|
| 327 |
+
REST FRAMEWORK RULES:
|
| 328 |
+
DEFAULT_PAGINATION_CLASS must be json_input["api_config"]["pagination"]
|
| 329 |
+
PAGE_SIZE must be json_input["api_config"]["page_size"]
|
| 330 |
+
|
| 331 |
+
AUTH RULES:
|
| 332 |
+
If auth.type == "jwt":
|
| 333 |
+
REST_FRAMEWORK must include:
|
| 334 |
+
{{
|
| 335 |
+
"DEFAULT_AUTHENTICATION_CLASSES": [
|
| 336 |
+
"rest_framework_simplejwt.authentication.JWTAuthentication"
|
| 337 |
+
]
|
| 338 |
+
}}
|
| 339 |
+
And include SIMPLE_JWT minimal config using timedelta.
|
| 340 |
+
|
| 341 |
+
STATIC/MEDIA RULES:
|
| 342 |
+
STATIC_URL = "static/"
|
| 343 |
+
STATICFILES_DIRS = [BASE_DIR / "static"]
|
| 344 |
+
MEDIA_URL = "media/"
|
| 345 |
+
MEDIA_ROOT = BASE_DIR / "media"
|
| 346 |
+
|
| 347 |
+
ROOT_URLCONF = "<project_name>.urls"
|
| 348 |
+
WSGI_APPLICATION = "<project_name>.wsgi.application"
|
| 349 |
+
|
| 350 |
+
Your output MUST be a complete, valid Python settings.py file with:
|
| 351 |
+
- imports
|
| 352 |
+
- BASE_DIR
|
| 353 |
+
- SECRET_KEY
|
| 354 |
+
- INSTALLED_APPS
|
| 355 |
+
- MIDDLEWARE
|
| 356 |
+
- TEMPLATES
|
| 357 |
+
- DATABASES
|
| 358 |
+
- REST_FRAMEWORK
|
| 359 |
+
- SIMPLE_JWT (if applicable)
|
| 360 |
+
- STATIC & MEDIA settings
|
| 361 |
+
- ROOT_URLCONF
|
| 362 |
+
- WSGI_APPLICATION
|
| 363 |
+
|
| 364 |
+
No missing sections, no placeholders, no dynamic code.
|
| 365 |
+
"""
|
| 366 |
+
ADMIN_PROMPT = """
|
| 367 |
+
You are an expert Django developer.
|
| 368 |
+
|
| 369 |
+
CRITICAL RULES:
|
| 370 |
+
- Output ONLY valid Python code for admin.py.
|
| 371 |
+
- NO markdown, NO comments, NO explanation.
|
| 372 |
+
- DO NOT reference json_input inside output.
|
| 373 |
+
- DO NOT generate loops, dynamic code, globals(), or any function.
|
| 374 |
+
|
| 375 |
+
json_input["model_names"] is a list of model class names inside this app.
|
| 376 |
+
|
| 377 |
+
Required imports:
|
| 378 |
+
from django.contrib import admin
|
| 379 |
+
from .models import Model1, Model2, ...
|
| 380 |
+
|
| 381 |
+
For EACH model:
|
| 382 |
+
Generate EXACTLY:
|
| 383 |
+
|
| 384 |
+
@admin.register(ModelName)
|
| 385 |
+
class ModelNameAdmin(admin.ModelAdmin):
|
| 386 |
+
list_display = ["id"] # keep simple
|
| 387 |
+
search_fields = ["id"]
|
| 388 |
+
|
| 389 |
+
Nothing else.
|
| 390 |
+
"""
|
| 391 |
+
PROJECT_URLS_PROMPT = """
|
| 392 |
+
You are an expert Django developer.
|
| 393 |
+
|
| 394 |
+
CRITICAL RULES:
|
| 395 |
+
- Output ONLY valid Python code for project-level urls.py.
|
| 396 |
+
- NO markdown, NO comments, NO explanation.
|
| 397 |
+
- DO NOT reference json_input inside output.
|
| 398 |
+
- DO NOT generate dynamic code.
|
| 399 |
+
|
| 400 |
+
json_input contains:
|
| 401 |
+
- project_name
|
| 402 |
+
- apps (list of app names)
|
| 403 |
+
- base_url (example: "/api/")
|
| 404 |
+
|
| 405 |
+
Required imports:
|
| 406 |
+
from django.contrib import admin
|
| 407 |
+
from django.urls import path, include
|
| 408 |
+
|
| 409 |
+
MUST generate:
|
| 410 |
+
|
| 411 |
+
urlpatterns = [
|
| 412 |
+
path("admin/", admin.site.urls),
|
| 413 |
+
]
|
| 414 |
+
|
| 415 |
+
For EACH app in json_input["apps"]:
|
| 416 |
+
Append EXACTLY this route:
|
| 417 |
+
|
| 418 |
+
path("<base_url><app_name>/", include("<app_name>.urls")),
|
| 419 |
+
|
| 420 |
+
Rules:
|
| 421 |
+
- Remove leading slash from <base_url> if present.
|
| 422 |
+
- Ensure ONLY ONE trailing slash ("/") between base_url and app_name.
|
| 423 |
+
|
| 424 |
+
Output MUST be a complete valid Python urls.py file.
|
| 425 |
+
"""
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
REQUIREMENTS_PROMPT = """
|
| 431 |
+
You generate ONLY a requirements.txt file.
|
| 432 |
+
|
| 433 |
+
CRITICAL RULES:
|
| 434 |
+
- Output ONLY raw package names.
|
| 435 |
+
- NO markdown, NO comments.
|
| 436 |
+
- DO NOT reference json_input inside output.
|
| 437 |
+
|
| 438 |
+
ALWAYS include:
|
| 439 |
+
Django>=5.0
|
| 440 |
+
djangorestframework
|
| 441 |
+
|
| 442 |
+
IF auth.type == "jwt":
|
| 443 |
+
include:
|
| 444 |
+
djangorestframework-simplejwt
|
| 445 |
+
|
| 446 |
+
IF database.engine == "postgresql":
|
| 447 |
+
include:
|
| 448 |
+
psycopg2-binary
|
| 449 |
+
|
| 450 |
+
IF deployment.gunicorn == true:
|
| 451 |
+
include:
|
| 452 |
+
gunicorn
|
| 453 |
+
|
| 454 |
+
One package per line.
|
| 455 |
+
"""
|
| 456 |
+
|
requirements.txt
CHANGED
|
@@ -1,3 +1,9 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit
|
| 2 |
+
django
|
| 3 |
+
djangorestframework
|
| 4 |
+
langchain
|
| 5 |
+
langchain-openai
|
| 6 |
+
langchain-groq
|
| 7 |
+
jsonschema
|
| 8 |
+
python-dotenv
|
| 9 |
+
pydantic
|
settings_generator.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def ensure_dir(path: Path):
|
| 6 |
+
path.mkdir(parents=True, exist_ok=True)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def _add_to_installed_apps(content: str, app_name: str) -> str:
|
| 10 |
+
"""
|
| 11 |
+
Safely add an app to INSTALLED_APPS if not already present.
|
| 12 |
+
"""
|
| 13 |
+
pattern = re.compile(r"INSTALLED_APPS\s*=\s*\[(.*?)\]", re.DOTALL)
|
| 14 |
+
match = pattern.search(content)
|
| 15 |
+
|
| 16 |
+
if not match:
|
| 17 |
+
return content
|
| 18 |
+
|
| 19 |
+
apps_block = match.group(1)
|
| 20 |
+
|
| 21 |
+
if f"'{app_name}'" in apps_block or f'"{app_name}"' in apps_block:
|
| 22 |
+
return content
|
| 23 |
+
|
| 24 |
+
updated_apps = apps_block.rstrip() + f"\n '{app_name}',"
|
| 25 |
+
return content[:match.start(1)] + updated_apps + content[match.end(1):]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def update_settings_py(settings_path, db_spec, app_names=None):
|
| 29 |
+
settings_path = Path(settings_path)
|
| 30 |
+
content = settings_path.read_text(encoding="utf-8")
|
| 31 |
+
|
| 32 |
+
# --------------------------------------------------
|
| 33 |
+
# 1. Ensure BASE_DIR
|
| 34 |
+
# --------------------------------------------------
|
| 35 |
+
if "BASE_DIR" not in content:
|
| 36 |
+
content = (
|
| 37 |
+
"from pathlib import Path\n\n"
|
| 38 |
+
"BASE_DIR = Path(__file__).resolve().parent.parent\n\n"
|
| 39 |
+
+ content
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# --------------------------------------------------
|
| 43 |
+
# 2. Ensure rest_framework + project apps
|
| 44 |
+
# --------------------------------------------------
|
| 45 |
+
content = _add_to_installed_apps(content, "rest_framework")
|
| 46 |
+
|
| 47 |
+
if app_names:
|
| 48 |
+
for app in app_names:
|
| 49 |
+
content = _add_to_installed_apps(content, app)
|
| 50 |
+
|
| 51 |
+
# --------------------------------------------------
|
| 52 |
+
# 3. Database config (ONLY if NOT sqlite)
|
| 53 |
+
# --------------------------------------------------
|
| 54 |
+
engine = (db_spec or {}).get("engine", "").lower()
|
| 55 |
+
|
| 56 |
+
if engine and engine != "sqlite":
|
| 57 |
+
engine_map = {
|
| 58 |
+
"postgresql": "django.db.backends.postgresql",
|
| 59 |
+
"postgres": "django.db.backends.postgresql",
|
| 60 |
+
"mysql": "django.db.backends.mysql",
|
| 61 |
+
"mariadb": "django.db.backends.mysql",
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
django_engine = engine_map.get(engine)
|
| 65 |
+
if not django_engine:
|
| 66 |
+
raise ValueError(f"Unsupported DB engine: {engine}")
|
| 67 |
+
|
| 68 |
+
database_block = f"""
|
| 69 |
+
DATABASES = {{
|
| 70 |
+
'default': {{
|
| 71 |
+
'ENGINE': '{django_engine}',
|
| 72 |
+
'NAME': '{db_spec.get("name")}',
|
| 73 |
+
'USER': '{db_spec.get("user", "")}',
|
| 74 |
+
'PASSWORD': '{db_spec.get("password", "")}',
|
| 75 |
+
'HOST': '{db_spec.get("host", "")}',
|
| 76 |
+
'PORT': '{db_spec.get("port", "")}',
|
| 77 |
+
}}
|
| 78 |
+
}}
|
| 79 |
+
""".strip()
|
| 80 |
+
|
| 81 |
+
content = re.sub(
|
| 82 |
+
r"DATABASES\s*=\s*\{.*?\}\s*\}\s*",
|
| 83 |
+
database_block + "\n\n",
|
| 84 |
+
content,
|
| 85 |
+
flags=re.DOTALL
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
# --------------------------------------------------
|
| 89 |
+
# 4. Static & Media (idempotent)
|
| 90 |
+
# --------------------------------------------------
|
| 91 |
+
if "STATIC_ROOT" not in content:
|
| 92 |
+
content += """
|
| 93 |
+
|
| 94 |
+
# Static files
|
| 95 |
+
STATIC_URL = "/static/"
|
| 96 |
+
STATIC_ROOT = BASE_DIR / "staticfiles"
|
| 97 |
+
STATICFILES_DIRS = [BASE_DIR / "static"]
|
| 98 |
+
|
| 99 |
+
# Media files
|
| 100 |
+
MEDIA_URL = "/media/"
|
| 101 |
+
MEDIA_ROOT = BASE_DIR / "media"
|
| 102 |
+
"""
|
| 103 |
+
|
| 104 |
+
# --------------------------------------------------
|
| 105 |
+
# 5. Write file
|
| 106 |
+
# --------------------------------------------------
|
| 107 |
+
settings_path.write_text(content, encoding="utf-8")
|
| 108 |
+
|
| 109 |
+
# --------------------------------------------------
|
| 110 |
+
# 6. Create folders
|
| 111 |
+
# --------------------------------------------------
|
| 112 |
+
project_root = settings_path.parent.parent
|
| 113 |
+
ensure_dir(project_root / "static")
|
| 114 |
+
ensure_dir(project_root / "media")
|
| 115 |
+
ensure_dir(project_root / "staticfiles")
|
| 116 |
+
|
| 117 |
+
print("✅ settings.py updated correctly")
|
spec_schema.json
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"title": "Minimal Django Backend Generator Spec",
|
| 4 |
+
"type": "object",
|
| 5 |
+
|
| 6 |
+
"required": [
|
| 7 |
+
"project_name",
|
| 8 |
+
"database",
|
| 9 |
+
"auth",
|
| 10 |
+
"apps",
|
| 11 |
+
"api_config"
|
| 12 |
+
],
|
| 13 |
+
|
| 14 |
+
"properties": {
|
| 15 |
+
"project_name": {
|
| 16 |
+
"type": "string"
|
| 17 |
+
},
|
| 18 |
+
|
| 19 |
+
"database": {
|
| 20 |
+
"type": "object",
|
| 21 |
+
"required": ["engine", "name"],
|
| 22 |
+
"properties": {
|
| 23 |
+
"engine": {
|
| 24 |
+
"type": "string",
|
| 25 |
+
"enum": ["sqlite", "postgresql"]
|
| 26 |
+
},
|
| 27 |
+
"name": {
|
| 28 |
+
"type": "string"
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
},
|
| 32 |
+
|
| 33 |
+
"auth": {
|
| 34 |
+
"type": "object",
|
| 35 |
+
"required": ["type"],
|
| 36 |
+
"properties": {
|
| 37 |
+
"type": {
|
| 38 |
+
"type": "string",
|
| 39 |
+
"enum": ["jwt", "session"]
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
},
|
| 43 |
+
|
| 44 |
+
"apps": {
|
| 45 |
+
"type": "object",
|
| 46 |
+
"required": ["core"],
|
| 47 |
+
"properties": {
|
| 48 |
+
"core": {
|
| 49 |
+
"type": "object",
|
| 50 |
+
"required": ["models", "apis"],
|
| 51 |
+
"properties": {
|
| 52 |
+
"models": {
|
| 53 |
+
"type": "object",
|
| 54 |
+
"patternProperties": {
|
| 55 |
+
"^[A-Z][a-zA-Z0-9]*$": {
|
| 56 |
+
"type": "object",
|
| 57 |
+
"required": ["fields"],
|
| 58 |
+
"properties": {
|
| 59 |
+
"fields": {
|
| 60 |
+
"type": "object",
|
| 61 |
+
"patternProperties": {
|
| 62 |
+
"^[a-z_][a-z0-9_]*$": {
|
| 63 |
+
"type": "object",
|
| 64 |
+
"required": ["type"],
|
| 65 |
+
"properties": {
|
| 66 |
+
"type": {
|
| 67 |
+
"type": "string",
|
| 68 |
+
"enum": [
|
| 69 |
+
"CharField",
|
| 70 |
+
"TextField",
|
| 71 |
+
"EmailField",
|
| 72 |
+
"IntegerField",
|
| 73 |
+
"BooleanField",
|
| 74 |
+
"DateField",
|
| 75 |
+
"DateTimeField",
|
| 76 |
+
"ForeignKey"
|
| 77 |
+
]
|
| 78 |
+
},
|
| 79 |
+
"to": {
|
| 80 |
+
"type": "string"
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
},
|
| 90 |
+
|
| 91 |
+
"apis": {
|
| 92 |
+
"type": "object",
|
| 93 |
+
"patternProperties": {
|
| 94 |
+
"^[A-Z][a-zA-Z0-9]*$": {
|
| 95 |
+
"type": "array",
|
| 96 |
+
"items": {
|
| 97 |
+
"type": "string",
|
| 98 |
+
"enum": ["list", "create", "retrieve", "update", "delete"]
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
},
|
| 107 |
+
|
| 108 |
+
"api_config": {
|
| 109 |
+
"type": "object",
|
| 110 |
+
"required": ["base_url"],
|
| 111 |
+
"properties": {
|
| 112 |
+
"base_url": {
|
| 113 |
+
"type": "string"
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
}
|
spec_validator.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
spec_validator_v2.py
|
| 3 |
+
|
| 4 |
+
Strict JSON schema validator + generator for Django backend specs.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import json
|
| 8 |
+
import re
|
| 9 |
+
from copy import deepcopy
|
| 10 |
+
from typing import Dict, Any, List, Tuple
|
| 11 |
+
|
| 12 |
+
from jsonschema import Draft202012Validator, ValidationError
|
| 13 |
+
from langchain.prompts import ChatPromptTemplate
|
| 14 |
+
from langchain_core.output_parsers import JsonOutputParser
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# =====================================================
|
| 18 |
+
# FILE LOADERS
|
| 19 |
+
# =====================================================
|
| 20 |
+
|
| 21 |
+
def load_schema(path: str) -> Dict[str, Any]:
|
| 22 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 23 |
+
return json.load(f)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def load_json(path: str) -> Dict[str, Any]:
|
| 27 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 28 |
+
return json.load(f)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# =====================================================
|
| 32 |
+
# VALIDATION HELPERS
|
| 33 |
+
# =====================================================
|
| 34 |
+
|
| 35 |
+
def _format_error(err: ValidationError) -> Dict[str, str]:
|
| 36 |
+
path = "/" + "/".join(map(str, err.absolute_path)) if err.absolute_path else "/"
|
| 37 |
+
return {"path": path, "message": err.message}
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def validate_schema(
|
| 41 |
+
spec: Dict[str, Any],
|
| 42 |
+
schema: Dict[str, Any]
|
| 43 |
+
) -> List[Dict[str, str]]:
|
| 44 |
+
validator = Draft202012Validator(schema)
|
| 45 |
+
errors = sorted(validator.iter_errors(spec), key=lambda e: e.path)
|
| 46 |
+
return [_format_error(e) for e in errors]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# =====================================================
|
| 50 |
+
# SAFE NORMALIZATIONS (SCHEMA-COMPATIBLE ONLY)
|
| 51 |
+
# =====================================================
|
| 52 |
+
|
| 53 |
+
def _to_pascal_case(name: str) -> str:
|
| 54 |
+
return "".join(part.capitalize() for part in re.split(r"[_\\s-]+", name) if part)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def normalize_spec(spec: Dict[str, Any]) -> List[str]:
|
| 58 |
+
"""
|
| 59 |
+
Normalize names without introducing new keys.
|
| 60 |
+
"""
|
| 61 |
+
warnings = []
|
| 62 |
+
|
| 63 |
+
# Normalize project name
|
| 64 |
+
if "project_name" in spec:
|
| 65 |
+
normalized = re.sub(r"[^a-z0-9_]", "_", spec["project_name"].lower())
|
| 66 |
+
if normalized != spec["project_name"]:
|
| 67 |
+
warnings.append("Normalized project_name")
|
| 68 |
+
spec["project_name"] = normalized
|
| 69 |
+
|
| 70 |
+
# Normalize model names
|
| 71 |
+
models = spec.get("apps", {}).get("core", {}).get("models", {})
|
| 72 |
+
new_models = {}
|
| 73 |
+
|
| 74 |
+
for model_name, model_def in models.items():
|
| 75 |
+
new_name = _to_pascal_case(model_name)
|
| 76 |
+
if new_name != model_name:
|
| 77 |
+
warnings.append(f"Renamed model '{model_name}' → '{new_name}'")
|
| 78 |
+
new_models[new_name] = model_def
|
| 79 |
+
|
| 80 |
+
if new_models:
|
| 81 |
+
spec["apps"]["core"]["models"] = new_models
|
| 82 |
+
|
| 83 |
+
return warnings
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# =====================================================
|
| 87 |
+
# MAIN VALIDATION FUNCTION
|
| 88 |
+
# =====================================================
|
| 89 |
+
|
| 90 |
+
def validate_and_clean_spec(
|
| 91 |
+
spec: Dict[str, Any],
|
| 92 |
+
schema: Dict[str, Any],
|
| 93 |
+
auto_fix: bool = True
|
| 94 |
+
) -> Tuple[bool, Dict[str, Any], List[Dict[str, str]], List[str]]:
|
| 95 |
+
|
| 96 |
+
cleaned = deepcopy(spec)
|
| 97 |
+
warnings: List[str] = []
|
| 98 |
+
|
| 99 |
+
if auto_fix:
|
| 100 |
+
warnings.extend(normalize_spec(cleaned))
|
| 101 |
+
|
| 102 |
+
errors = validate_schema(cleaned, schema)
|
| 103 |
+
|
| 104 |
+
if errors:
|
| 105 |
+
return False, cleaned, errors, warnings
|
| 106 |
+
|
| 107 |
+
return True, cleaned, [], warnings
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
# =====================================================
|
| 111 |
+
# JSON SPEC GENERATION (LLM)
|
| 112 |
+
# =====================================================
|
| 113 |
+
|
| 114 |
+
def generate_valid_json_spec(
|
| 115 |
+
*,
|
| 116 |
+
user_prompt: str,
|
| 117 |
+
llm,
|
| 118 |
+
retries: int = 3,
|
| 119 |
+
) -> Dict[str, Any]:
|
| 120 |
+
"""
|
| 121 |
+
Generates schema-valid JSON using LLM + strict validation.
|
| 122 |
+
"""
|
| 123 |
+
|
| 124 |
+
system_prompt = """
|
| 125 |
+
You are a JSON compiler.
|
| 126 |
+
|
| 127 |
+
Return ONLY a JSON object that EXACTLY matches this schema.
|
| 128 |
+
Do NOT add, remove, or rename keys.
|
| 129 |
+
Do NOT include explanations or formatting.
|
| 130 |
+
|
| 131 |
+
Schema:
|
| 132 |
+
{{
|
| 133 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 134 |
+
"title": "Minimal Django Backend Generator Spec",
|
| 135 |
+
"type": "object",
|
| 136 |
+
|
| 137 |
+
"required": [
|
| 138 |
+
"project_name",
|
| 139 |
+
"database",
|
| 140 |
+
"auth",
|
| 141 |
+
"apps",
|
| 142 |
+
"api_config"
|
| 143 |
+
],
|
| 144 |
+
|
| 145 |
+
"properties": {{
|
| 146 |
+
"project_name": {{
|
| 147 |
+
"type": "string"
|
| 148 |
+
}},
|
| 149 |
+
|
| 150 |
+
"database": {{
|
| 151 |
+
"type": "object",
|
| 152 |
+
"required": ["engine", "name"],
|
| 153 |
+
"properties": {{
|
| 154 |
+
"engine": {{
|
| 155 |
+
"type": "string",
|
| 156 |
+
"enum": ["sqlite", "postgresql"]
|
| 157 |
+
}},
|
| 158 |
+
"name": {{
|
| 159 |
+
"type": "string"
|
| 160 |
+
}}
|
| 161 |
+
}}
|
| 162 |
+
}},
|
| 163 |
+
|
| 164 |
+
"auth": {{
|
| 165 |
+
"type": "object",
|
| 166 |
+
"required": ["type"],
|
| 167 |
+
"properties": {{
|
| 168 |
+
"type": {{
|
| 169 |
+
"type": "string",
|
| 170 |
+
"enum": ["jwt", "session"]
|
| 171 |
+
}}
|
| 172 |
+
}}
|
| 173 |
+
}},
|
| 174 |
+
|
| 175 |
+
"apps": {{
|
| 176 |
+
"type": "object",
|
| 177 |
+
"required": ["core"],
|
| 178 |
+
"properties": {{
|
| 179 |
+
"core": {{
|
| 180 |
+
"type": "object",
|
| 181 |
+
"required": ["models", "apis"],
|
| 182 |
+
"properties": {{
|
| 183 |
+
"models": {{
|
| 184 |
+
"type": "object",
|
| 185 |
+
"patternProperties": {{
|
| 186 |
+
"^[A-Z][a-zA-Z0-9]*$": {{
|
| 187 |
+
"type": "object",
|
| 188 |
+
"required": ["fields"],
|
| 189 |
+
"properties": {{
|
| 190 |
+
"fields": {{
|
| 191 |
+
"type": "object",
|
| 192 |
+
"patternProperties": {{
|
| 193 |
+
"^[a-z_][a-z0-9_]*$": {{
|
| 194 |
+
"type": "object",
|
| 195 |
+
"required": ["type"],
|
| 196 |
+
"properties": {{
|
| 197 |
+
"type": {{
|
| 198 |
+
"type": "string",
|
| 199 |
+
"enum": [
|
| 200 |
+
"CharField",
|
| 201 |
+
"TextField",
|
| 202 |
+
"EmailField",
|
| 203 |
+
"IntegerField",
|
| 204 |
+
"BooleanField",
|
| 205 |
+
"DateField",
|
| 206 |
+
"DateTimeField",
|
| 207 |
+
"ForeignKey"
|
| 208 |
+
]
|
| 209 |
+
}},
|
| 210 |
+
"to": {{
|
| 211 |
+
"type": "string"
|
| 212 |
+
}}
|
| 213 |
+
}}
|
| 214 |
+
}}
|
| 215 |
+
}}
|
| 216 |
+
}}
|
| 217 |
+
}}
|
| 218 |
+
}}
|
| 219 |
+
}}
|
| 220 |
+
}},
|
| 221 |
+
|
| 222 |
+
"apis": {{
|
| 223 |
+
"type": "object",
|
| 224 |
+
"patternProperties": {{
|
| 225 |
+
"^[A-Z][a-zA-Z0-9]*$": {{
|
| 226 |
+
"type": "array",
|
| 227 |
+
"items": {{
|
| 228 |
+
"type": "string",
|
| 229 |
+
"enum": ["list", "create", "retrieve", "update", "delete"]
|
| 230 |
+
}}
|
| 231 |
+
}}
|
| 232 |
+
}}
|
| 233 |
+
}}
|
| 234 |
+
}}
|
| 235 |
+
}}
|
| 236 |
+
}}
|
| 237 |
+
}},
|
| 238 |
+
|
| 239 |
+
"api_config": {{
|
| 240 |
+
"type": "object",
|
| 241 |
+
"required": ["base_url"],
|
| 242 |
+
"properties": {{
|
| 243 |
+
"base_url": {{
|
| 244 |
+
"type": "string"
|
| 245 |
+
}}
|
| 246 |
+
}}
|
| 247 |
+
}}
|
| 248 |
+
}}
|
| 249 |
+
}}
|
| 250 |
+
|
| 251 |
+
"""
|
| 252 |
+
|
| 253 |
+
prompt = ChatPromptTemplate.from_messages([
|
| 254 |
+
("system", system_prompt),
|
| 255 |
+
("user", "{user_prompt}")
|
| 256 |
+
])
|
| 257 |
+
|
| 258 |
+
# usage
|
| 259 |
+
schema = load_schema('spec_schema.json')
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
chain = prompt | llm | JsonOutputParser()
|
| 263 |
+
|
| 264 |
+
for attempt in range(1, retries + 1):
|
| 265 |
+
try:
|
| 266 |
+
result = chain.invoke({"user_prompt": user_prompt})
|
| 267 |
+
|
| 268 |
+
valid, cleaned, errors, _ = validate_and_clean_spec(
|
| 269 |
+
result,
|
| 270 |
+
schema,
|
| 271 |
+
auto_fix=False
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
if valid:
|
| 275 |
+
return cleaned
|
| 276 |
+
|
| 277 |
+
raise ValueError(errors)
|
| 278 |
+
|
| 279 |
+
except Exception as e:
|
| 280 |
+
if attempt == retries:
|
| 281 |
+
raise RuntimeError(f"Failed after {retries} attempts: {e}")
|
| 282 |
+
|
| 283 |
+
raise RuntimeError("Unreachable")
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
# =====================================================
|
| 287 |
+
# CLI (OPTIONAL)
|
| 288 |
+
# =====================================================
|
| 289 |
+
|
| 290 |
+
if __name__ == "__main__":
|
| 291 |
+
import argparse
|
| 292 |
+
|
| 293 |
+
parser = argparse.ArgumentParser(description="Validate Django backend JSON spec")
|
| 294 |
+
parser.add_argument("spec", help="Path to spec.json")
|
| 295 |
+
parser.add_argument("schema", help="Path to schema.json")
|
| 296 |
+
args = parser.parse_args()
|
| 297 |
+
|
| 298 |
+
schema = load_schema(args.schema)
|
| 299 |
+
spec = load_json(args.spec)
|
| 300 |
+
|
| 301 |
+
valid, cleaned, errors, warnings = validate_and_clean_spec(spec, schema)
|
| 302 |
+
|
| 303 |
+
print("VALID:", valid)
|
| 304 |
+
if warnings:
|
| 305 |
+
print("WARNINGS:")
|
| 306 |
+
for w in warnings:
|
| 307 |
+
print("-", w)
|
| 308 |
+
|
| 309 |
+
if errors:
|
| 310 |
+
print("ERRORS:")
|
| 311 |
+
for e in errors:
|
| 312 |
+
print(f"{e['path']}: {e['message']}")
|
| 313 |
+
else:
|
| 314 |
+
print("Spec is valid.")
|