""" LLM Helper Module for generating HTML/JS/CSS applications using OpenRouter API Includes deterministic builders for specific briefs to avoid external dependency. """ import os import json import logging from typing import Dict, List, Any, Optional, Tuple try: from openai import OpenAI # type: ignore except Exception: # openai may be absent or fail when not configured OpenAI = None # type: ignore from pydantic import BaseModel # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class AppGenerationRequest(BaseModel): task: str brief: str round: int = 1 attachments: Optional[List[Dict[str, Any]]] = None class GeneratedApp(BaseModel): html_content: str css_content: str js_content: str metadata: Dict[str, Any] extra_files: Optional[Dict[str, str]] = None class LLMHelper: def __init__(self): """ Initializes OpenRouter/OpenAI-compatible client when API key is available. """ api_key = os.getenv("OPENAI_API_KEY") self.client = None if api_key and OpenAI is not None: self.client = OpenAI( api_key=api_key, base_url=os.getenv("OPENAI_API_BASE", "https://openrouter.ai/api/v1"), ) self.model = os.getenv("OPENAI_MODEL", "arliai/qwq-32b-arliai-rpr-v1:free") logger.info(f"Using model: {self.model}") def generate_app(self, request: AppGenerationRequest) -> GeneratedApp: """ Generate complete web app based on user brief. Uses deterministic builders for known briefs, otherwise uses LLM if configured. """ brief_lower = (request.brief or "").lower() try: if "sum-of-sales" in brief_lower or "sales" in brief_lower: return self._build_sum_of_sales_app(request) if "markdown-to-html" in brief_lower or "markdown" in brief_lower: return self._build_markdown_to_html_app(request) if "github-user" in brief_lower: return self._build_github_user_created_app(request) except Exception as e: logger.error(f"Deterministic builder failed, trying LLM if available: {e}") if not self.client: raise RuntimeError("LLM client not configured and no deterministic builder matched") try: prompt = ( self._build_initial_prompt(request) if request.round == 1 else self._build_revision_prompt(request) ) logger.info(f"Generating app (Round {request.round}) using {self.model}") response = self.client.chat.completions.create( model=self.model, messages=[ { "role": "system", "content": ( "You are an expert web developer. " "Always respond in strict JSON format with keys: " "html_content, css_content, js_content, metadata." ), }, {"role": "user", "content": prompt}, ], temperature=0.6, max_tokens=4000, ) content = response.choices[0].message.content app_data = json.loads(content) return GeneratedApp( html_content=app_data.get("html_content", ""), css_content=app_data.get("css_content", ""), js_content=app_data.get("js_content", ""), metadata=app_data.get("metadata", {}), ) except json.JSONDecodeError as e: logger.error(f"Invalid JSON from LLM: {e}") raise ValueError("Invalid JSON response from OpenRouter API") except Exception as e: logger.error(f"App generation failed: {e}") raise # ------------------------ Deterministic builders ------------------------ def _decode_data_url(self, url: str) -> Tuple[str, str]: try: if not url or not url.startswith("data:"): return "", "" header, b64 = url.split(",", 1) mime = header[5:] if ";base64" in mime: mime = mime.replace(";base64", "") import base64 decoded = base64.b64decode(b64).decode("utf-8", errors="replace") else: from urllib.parse import unquote decoded = unquote(b64) return mime, decoded except Exception: return "", "" def _collect_attachments(self, request: AppGenerationRequest) -> Dict[str, str]: files: Dict[str, str] = {} if not request.attachments: return files for a in request.attachments: name = a.get("name") if isinstance(a, dict) else None url = a.get("url") if isinstance(a, dict) else None if not name or not url: continue _, text = self._decode_data_url(url) files[name] = text return files def _build_sum_of_sales_app(self, request: AppGenerationRequest) -> GeneratedApp: files = self._collect_attachments(request) data_csv = files.get("data.csv", "") html = ( "" "" "Sales Summary" "" "" "
" "

Sales Summary

" "
Total: 0
" "
" "" "" "
" "
" "" "" "
" "
ProductSales
" "
" "" "" "" ) extra_files: Dict[str, str] = {} if data_csv: extra_files["data.csv"] = data_csv metadata = {"title": "Sales Summary"} return GeneratedApp(html_content=html, css_content="", js_content="", metadata=metadata, extra_files=extra_files) def _build_markdown_to_html_app(self, request: AppGenerationRequest) -> GeneratedApp: files = self._collect_attachments(request) input_md = files.get("input.md", "") html = ( "" "Markdown Viewer" "" "" "" "
" "
" "" "" "" "0" "
" "
" "
"
            "
" "" "" "" "" ) extra_files: Dict[str, str] = {} if input_md: extra_files["input.md"] = input_md metadata = {"title": "Markdown Viewer"} return GeneratedApp(html_content=html, css_content="", js_content="", metadata=metadata, extra_files=extra_files) def _build_github_user_created_app(self, request: AppGenerationRequest) -> GeneratedApp: html = ( "" "GitHub User Info" "" "" "
" "

GitHub User Info

" "
Idle
" "
" "
" "
" "
" "
Created:
" "
" "" "" ) metadata = {"title": "GitHub User Info"} return GeneratedApp(html_content=html, css_content="", js_content="", metadata=metadata) # ------------------------ Prompt builders ------------------------ def _build_initial_prompt(self, request: AppGenerationRequest) -> str: prompt = f""" Create a complete, minimal web app based on: TASK: {request.task} BRIEF: {request.brief} Requirements: 1. Single-page HTML5 app (self-contained) 2. Include embedded CSS and JavaScript 3. Be functional and visually appealing 4. Responsive and works directly in browser Respond strictly as JSON with: - html_content - css_content - js_content - metadata (title, description) """ if request.attachments: prompt += f"\nAttachments:\n{json.dumps(request.attachments, indent=2)}" return prompt def _build_revision_prompt(self, request: AppGenerationRequest) -> str: prompt = f""" Revise the previous app as per feedback. TASK: {request.task} BRIEF: {request.brief} ROUND: {request.round} Keep same functionality but improve UI/UX and fix issues. Respond with JSON (same keys as before). """ if request.attachments: prompt += f"\nRevision context:\n{json.dumps(request.attachments, indent=2)}" return prompt def validate_generated_app(self, app: GeneratedApp) -> bool: try: if not app.html_content or "