iitmbs24f commited on
Commit
977bdc9
·
verified ·
1 Parent(s): ed22200

Upload llm_helper.py

Browse files
Files changed (1) hide show
  1. llm_helper.py +355 -0
llm_helper.py ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LLM Helper Module for generating HTML/JS/CSS applications using OpenRouter API
3
+ Includes deterministic builders for specific briefs to avoid external dependency.
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import logging
9
+ from typing import Dict, List, Any, Optional, Tuple
10
+
11
+ try:
12
+ from openai import OpenAI # type: ignore
13
+ except Exception: # openai may be absent or fail when not configured
14
+ OpenAI = None # type: ignore
15
+
16
+ from pydantic import BaseModel
17
+
18
+ # Configure logging
19
+ logging.basicConfig(level=logging.INFO)
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class AppGenerationRequest(BaseModel):
24
+ task: str
25
+ brief: str
26
+ round: int = 1
27
+ attachments: Optional[List[Dict[str, Any]]] = None
28
+
29
+
30
+ class GeneratedApp(BaseModel):
31
+ html_content: str
32
+ css_content: str
33
+ js_content: str
34
+ metadata: Dict[str, Any]
35
+ extra_files: Optional[Dict[str, str]] = None
36
+
37
+
38
+ class LLMHelper:
39
+ def __init__(self):
40
+ """
41
+ Initializes OpenRouter/OpenAI-compatible client when API key is available.
42
+ """
43
+ api_key = os.getenv("OPENAI_API_KEY")
44
+ self.client = None
45
+ if api_key and OpenAI is not None:
46
+ self.client = OpenAI(
47
+ api_key=api_key,
48
+ base_url=os.getenv("OPENAI_API_BASE", "https://openrouter.ai/api/v1"),
49
+ )
50
+
51
+ self.model = os.getenv("OPENAI_MODEL", "arliai/qwq-32b-arliai-rpr-v1:free")
52
+ logger.info(f"Using model: {self.model}")
53
+
54
+ def generate_app(self, request: AppGenerationRequest) -> GeneratedApp:
55
+ """
56
+ Generate complete web app based on user brief.
57
+ Uses deterministic builders for known briefs, otherwise uses LLM if configured.
58
+ """
59
+ brief_lower = (request.brief or "").lower()
60
+ try:
61
+ if "sum-of-sales" in brief_lower or "sales" in brief_lower:
62
+ return self._build_sum_of_sales_app(request)
63
+ if "markdown-to-html" in brief_lower or "markdown" in brief_lower:
64
+ return self._build_markdown_to_html_app(request)
65
+ if "github-user" in brief_lower:
66
+ return self._build_github_user_created_app(request)
67
+ except Exception as e:
68
+ logger.error(f"Deterministic builder failed, trying LLM if available: {e}")
69
+
70
+ if not self.client:
71
+ raise RuntimeError("LLM client not configured and no deterministic builder matched")
72
+
73
+ try:
74
+ prompt = (
75
+ self._build_initial_prompt(request)
76
+ if request.round == 1
77
+ else self._build_revision_prompt(request)
78
+ )
79
+
80
+ logger.info(f"Generating app (Round {request.round}) using {self.model}")
81
+ response = self.client.chat.completions.create(
82
+ model=self.model,
83
+ messages=[
84
+ {
85
+ "role": "system",
86
+ "content": (
87
+ "You are an expert web developer. "
88
+ "Always respond in strict JSON format with keys: "
89
+ "html_content, css_content, js_content, metadata."
90
+ ),
91
+ },
92
+ {"role": "user", "content": prompt},
93
+ ],
94
+ temperature=0.6,
95
+ max_tokens=4000,
96
+ )
97
+ content = response.choices[0].message.content
98
+ app_data = json.loads(content)
99
+ return GeneratedApp(
100
+ html_content=app_data.get("html_content", ""),
101
+ css_content=app_data.get("css_content", ""),
102
+ js_content=app_data.get("js_content", ""),
103
+ metadata=app_data.get("metadata", {}),
104
+ )
105
+ except json.JSONDecodeError as e:
106
+ logger.error(f"Invalid JSON from LLM: {e}")
107
+ raise ValueError("Invalid JSON response from OpenRouter API")
108
+ except Exception as e:
109
+ logger.error(f"App generation failed: {e}")
110
+ raise
111
+
112
+ # ------------------------ Deterministic builders ------------------------
113
+ def _decode_data_url(self, url: str) -> Tuple[str, str]:
114
+ try:
115
+ if not url or not url.startswith("data:"):
116
+ return "", ""
117
+ header, b64 = url.split(",", 1)
118
+ mime = header[5:]
119
+ if ";base64" in mime:
120
+ mime = mime.replace(";base64", "")
121
+ import base64
122
+ decoded = base64.b64decode(b64).decode("utf-8", errors="replace")
123
+ else:
124
+ from urllib.parse import unquote
125
+ decoded = unquote(b64)
126
+ return mime, decoded
127
+ except Exception:
128
+ return "", ""
129
+
130
+ def _collect_attachments(self, request: AppGenerationRequest) -> Dict[str, str]:
131
+ files: Dict[str, str] = {}
132
+ if not request.attachments:
133
+ return files
134
+ for a in request.attachments:
135
+ name = a.get("name") if isinstance(a, dict) else None
136
+ url = a.get("url") if isinstance(a, dict) else None
137
+ if not name or not url:
138
+ continue
139
+ _, text = self._decode_data_url(url)
140
+ files[name] = text
141
+ return files
142
+
143
+ def _build_sum_of_sales_app(self, request: AppGenerationRequest) -> GeneratedApp:
144
+ files = self._collect_attachments(request)
145
+ data_csv = files.get("data.csv", "")
146
+ html = (
147
+ "<!doctype html><html><head><meta charset=\"utf-8\">"
148
+ "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
149
+ "<title>Sales Summary</title>"
150
+ "<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css\">"
151
+ "</head><body class=\"p-4\">"
152
+ "<div class=\"container\">"
153
+ "<h1 class=\"mb-3\">Sales Summary</h1>"
154
+ "<div class=\"mb-2\">Total: <span id=\"total-sales\">0</span> <span id=\"total-currency\"></span></div>"
155
+ "<div class=\"mb-3\">"
156
+ "<label class=\"form-label\" for=\"region-filter\">Region</label>"
157
+ "<select id=\"region-filter\" class=\"form-select\"><option value=\"all\">All</option></select>"
158
+ "</div>"
159
+ "<div class=\"mb-3\">"
160
+ "<label class=\"form-label\" for=\"currency-picker\">Currency</label>"
161
+ "<select id=\"currency-picker\" class=\"form-select\"><option value=\"USD\">USD</option></select>"
162
+ "</div>"
163
+ "<table id=\"product-sales\" class=\"table table-striped\"><thead><tr><th>Product</th><th>Sales</th></tr></thead><tbody></tbody></table>"
164
+ "</div>"
165
+ "<script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js\"></script>"
166
+ "<script>"
167
+ "(function(){\n"
168
+ " function parseCSV(text){\n"
169
+ " const lines=text.trim().split(/\\r?\\n/);\n"
170
+ " const header=lines.shift().split(',').map(s=>s.trim());\n"
171
+ " return lines.map(l=>{const cols=l.split(',');const o={};header.forEach((h,i)=>o[h]=cols[i]);return o;});\n"
172
+ " }\n"
173
+ " async function loadData(){\n"
174
+ " let csv='';\n"
175
+ " try{csv=await fetch('data.csv').then(r=>r.text());}catch(e){}\n"
176
+ " const rows = csv ? parseCSV(csv) : [];\n"
177
+ " const tbody=document.querySelector('#product-sales tbody');\n"
178
+ " const regionSel=document.getElementById('region-filter');\n"
179
+ " const regions = new Set(['all']);\n"
180
+ " for(const r of rows){\n"
181
+ " const p=r.product||r.Product||'Unknown';\n"
182
+ " const s=parseFloat(r.sales||r.Sales||0)||0;\n"
183
+ " const region=r.region||r.Region||'all'; regions.add(region);\n"
184
+ " const tr=document.createElement('tr'); tr.innerHTML=`<td>${p}</td><td>${s.toFixed(2)}</td>`; tr.dataset.region=region; tbody.appendChild(tr);\n"
185
+ " }\n"
186
+ " for(const r of regions){ if(r==='all') continue; const opt=document.createElement('option'); opt.value=r; opt.textContent=r; regionSel.appendChild(opt);}\n"
187
+ " function computeTotal(){\n"
188
+ " const region=regionSel.value; let sum=0;\n"
189
+ " [...tbody.querySelectorAll('tr')].forEach(tr=>{ if(region==='all'||tr.dataset.region===region){ sum+=parseFloat(tr.children[1].textContent)||0; } });\n"
190
+ " const el=document.getElementById('total-sales'); el.textContent=sum.toFixed(2); el.dataset.region=region;\n"
191
+ " }\n"
192
+ " regionSel.addEventListener('change', computeTotal);\n"
193
+ " computeTotal();\n"
194
+ " const picker=document.getElementById('currency-picker'); const totalCur=document.getElementById('total-currency');\n"
195
+ " let rates={USD:1};\n"
196
+ " try{ rates = await fetch('rates.json').then(r=>r.json()); }catch(e){}\n"
197
+ " function applyCurrency(){ const rate=rates[picker.value]||1; const base=parseFloat(document.getElementById('total-sales').textContent)||0; document.getElementById('total-sales').textContent=(base*rate).toFixed(2); totalCur.textContent = ' '+picker.value; }\n"
198
+ " picker.addEventListener('change', applyCurrency);\n"
199
+ " if(typeof seed!=='undefined'){ document.title = `Sales Summary ${seed}`; }\n"
200
+ " }\n"
201
+ " loadData();\n"
202
+ "})();\n"
203
+ "</script>"
204
+ "</body></html>"
205
+ )
206
+ extra_files: Dict[str, str] = {}
207
+ if data_csv:
208
+ extra_files["data.csv"] = data_csv
209
+ metadata = {"title": "Sales Summary"}
210
+ return GeneratedApp(html_content=html, css_content="", js_content="", metadata=metadata, extra_files=extra_files)
211
+
212
+ def _build_markdown_to_html_app(self, request: AppGenerationRequest) -> GeneratedApp:
213
+ files = self._collect_attachments(request)
214
+ input_md = files.get("input.md", "")
215
+ html = (
216
+ "<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
217
+ "<title>Markdown Viewer</title>"
218
+ "<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css\">"
219
+ "<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css\">"
220
+ "</head><body class=\"p-4\">"
221
+ "<div class=\"container\">"
222
+ "<div class=\"mb-3\" id=\"markdown-tabs\">"
223
+ "<button class=\"btn btn-primary me-2\" data-target=\"output\">Rendered</button>"
224
+ "<button class=\"btn btn-outline-secondary\" data-target=\"source\">Source</button>"
225
+ "<span id=\"markdown-source-label\" class=\"ms-3 text-muted\"></span>"
226
+ "<span id=\"markdown-word-count\" class=\"badge bg-secondary ms-3\">0</span>"
227
+ "</div>"
228
+ "<div id=\"markdown-output\" class=\"mb-3\"></div>"
229
+ "<pre id=\"markdown-source\" class=\"p-3 bg-light border\"></pre>"
230
+ "</div>"
231
+ "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/marked/11.1.1/marked.min.js\"></script>"
232
+ "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js\"></script>"
233
+ "<script>"
234
+ "(function(){\n"
235
+ " function wordCount(s){ return (s.trim().match(/\\S+/g)||[]).length; }\n"
236
+ " async function loadMarkdown(){\n"
237
+ " const params=new URLSearchParams(location.search);\n"
238
+ " const url=params.get('url');\n"
239
+ " let md=''; let sourceLabel='attachment';\n"
240
+ " try{ md = url ? await fetch(url).then(r=>r.text()) : await fetch('input.md').then(r=>r.text()); sourceLabel = url ? url : 'attachment'; }catch(e){}\n"
241
+ " document.getElementById('markdown-source').textContent = md;\n"
242
+ " document.getElementById('markdown-output').innerHTML = marked.parse(md);\n"
243
+ " document.getElementById('markdown-source-label').textContent = sourceLabel;\n"
244
+ " document.getElementById('markdown-word-count').textContent = new Intl.NumberFormat().format(wordCount(md));\n"
245
+ " document.querySelectorAll('pre code').forEach(el=>hljs.highlightElement(el));\n"
246
+ " }\n"
247
+ " document.getElementById('markdown-tabs').addEventListener('click', (e)=>{ const btn=e.target.closest('button'); if(!btn) return; const target=btn.getAttribute('data-target'); document.getElementById('markdown-output').style.display = target==='output'?'block':'none'; document.getElementById('markdown-source').style.display = target==='source'?'block':'none'; });\n"
248
+ " loadMarkdown();\n"
249
+ "})();\n"
250
+ "</script>"
251
+ "</body></html>"
252
+ )
253
+ extra_files: Dict[str, str] = {}
254
+ if input_md:
255
+ extra_files["input.md"] = input_md
256
+ metadata = {"title": "Markdown Viewer"}
257
+ return GeneratedApp(html_content=html, css_content="", js_content="", metadata=metadata, extra_files=extra_files)
258
+
259
+ def _build_github_user_created_app(self, request: AppGenerationRequest) -> GeneratedApp:
260
+ html = (
261
+ "<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
262
+ "<title>GitHub User Info</title>"
263
+ "<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css\">"
264
+ "</head><body class=\"p-4\">"
265
+ "<div class=\"container\">"
266
+ "<h1 class=\"mb-3\">GitHub User Info</h1>"
267
+ "<div id=\"github-status\" class=\"alert alert-info\" aria-live=\"polite\">Idle</div>"
268
+ "<form id=\"github-user-${seed}\" class=\"row g-2\">"
269
+ "<div class=\"col-auto\"><input id=\"gh-username\" class=\"form-control\" placeholder=\"Username\" required></div>"
270
+ "<div class=\"col-auto\"><button class=\"btn btn-primary\" type=\"submit\">Lookup</button></div>"
271
+ "</form>"
272
+ "<div class=\"mt-3\">Created: <span id=\"github-created-at\"></span> <span id=\"github-account-age\"></span></div>"
273
+ "</div>"
274
+ "<script>"
275
+ "(function(){\n"
276
+ " const form=document.getElementById('github-user-${seed}');\n"
277
+ " const statusEl=document.getElementById('github-status');\n"
278
+ " const createdEl=document.getElementById('github-created-at');\n"
279
+ " const ageEl=document.getElementById('github-account-age');\n"
280
+ " const stored = localStorage.getItem('github-user-${seed}');\n"
281
+ " if(stored){ try{ const d=JSON.parse(stored); document.getElementById('gh-username').value=d.username||''; }catch(e){} }\n"
282
+ " form.addEventListener('submit', async (e)=>{ e.preventDefault(); const u=document.getElementById('gh-username').value.trim(); if(!u) return; statusEl.textContent='Starting lookup...';\n"
283
+ " try{\n"
284
+ " const params=new URLSearchParams(location.search); const token=params.get('token');\n"
285
+ " const res = await fetch('https://api.github.com/users/'+encodeURIComponent(u), { headers: token? { Authorization: 'Bearer '+token } : {} });\n"
286
+ " statusEl.textContent='Lookup complete';\n"
287
+ " if(!res.ok){ createdEl.textContent=''; ageEl.textContent=''; return; }\n"
288
+ " const data=await res.json();\n"
289
+ " const created=new Date(data.created_at); const y=created.getUTCFullYear(); const m=String(created.getUTCMonth()+1).padStart(2,'0'); const d=String(created.getUTCDate()).padStart(2,'0');\n"
290
+ " createdEl.textContent = `${y}-${m}-${d}`;\n"
291
+ " const years = Math.max(0, Math.floor((Date.now()-created.getTime())/ (365*24*3600*1000)));\n"
292
+ " ageEl.textContent = ` (${years} years)`;\n"
293
+ " localStorage.setItem('github-user-${seed}', JSON.stringify({ username:u, created: data.created_at }));\n"
294
+ " }catch(e){ statusEl.textContent='Failed'; createdEl.textContent=''; ageEl.textContent=''; }\n"
295
+ " });\n"
296
+ "})();\n"
297
+ "</script>"
298
+ "</body></html>"
299
+ )
300
+ metadata = {"title": "GitHub User Info"}
301
+ return GeneratedApp(html_content=html, css_content="", js_content="", metadata=metadata)
302
+
303
+ # ------------------------ Prompt builders ------------------------
304
+ def _build_initial_prompt(self, request: AppGenerationRequest) -> str:
305
+ prompt = f"""
306
+ Create a complete, minimal web app based on:
307
+
308
+ TASK: {request.task}
309
+ BRIEF: {request.brief}
310
+
311
+ Requirements:
312
+ 1. Single-page HTML5 app (self-contained)
313
+ 2. Include embedded CSS and JavaScript
314
+ 3. Be functional and visually appealing
315
+ 4. Responsive and works directly in browser
316
+
317
+ Respond strictly as JSON with:
318
+ - html_content
319
+ - css_content
320
+ - js_content
321
+ - metadata (title, description)
322
+ """
323
+ if request.attachments:
324
+ prompt += f"\nAttachments:\n{json.dumps(request.attachments, indent=2)}"
325
+ return prompt
326
+
327
+ def _build_revision_prompt(self, request: AppGenerationRequest) -> str:
328
+ prompt = f"""
329
+ Revise the previous app as per feedback.
330
+
331
+ TASK: {request.task}
332
+ BRIEF: {request.brief}
333
+ ROUND: {request.round}
334
+
335
+ Keep same functionality but improve UI/UX and fix issues.
336
+ Respond with JSON (same keys as before).
337
+ """
338
+ if request.attachments:
339
+ prompt += f"\nRevision context:\n{json.dumps(request.attachments, indent=2)}"
340
+ return prompt
341
+
342
+ def validate_generated_app(self, app: GeneratedApp) -> bool:
343
+ try:
344
+ if not app.html_content or "<html" not in app.html_content.lower():
345
+ return False
346
+ if not app.css_content and "style" not in app.html_content.lower():
347
+ return False
348
+ if not app.js_content and "script" not in app.html_content.lower():
349
+ return False
350
+ return True
351
+ except Exception as e:
352
+ logger.error(f"Validation error: {e}")
353
+ return False
354
+
355
+