harshadh01 commited on
Commit
e540463
·
verified ·
1 Parent(s): 9cbfa29

Initial deployment of AI REST API Generator

Browse files
Files changed (13) hide show
  1. .gitignore +19 -0
  2. Dockerfile +20 -20
  3. LICENSE +15 -0
  4. README.md +207 -20
  5. additional_function.py +30 -0
  6. app.py +412 -0
  7. generator.py +420 -0
  8. pipeline.py +161 -0
  9. prompt.py +456 -0
  10. requirements.txt +9 -3
  11. settings_generator.py +117 -0
  12. spec_schema.json +118 -0
  13. 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.13.5-slim
2
-
3
- WORKDIR /app
4
-
5
- RUN apt-get update && apt-get install -y \
6
- build-essential \
7
- curl \
8
- git \
9
- && rm -rf /var/lib/apt/lists/*
10
-
11
- COPY requirements.txt ./
12
- COPY src/ ./src/
13
-
14
- RUN pip3 install -r requirements.txt
15
-
16
- EXPOSE 8501
17
-
18
- HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
19
-
20
- ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
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
- title: Ai Rest Api Generator
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
- pinned: false
11
- short_description: Generate Django REST APIs automatically using AI.
12
- license: mit
13
- ---
14
-
15
- # Welcome to Streamlit!
16
-
17
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
18
-
19
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
20
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- altair
2
- pandas
3
- streamlit
 
 
 
 
 
 
 
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.")