From 799467686da7401ff31d12541afeb7b907e93a8a Mon Sep 17 00:00:00 2001 From: Parthavi K Date: Sun, 3 Aug 2025 14:39:18 +0530 Subject: [PATCH 1/4] Update ai_engine.py --- core/ai_engine.py | 95 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 25 deletions(-) diff --git a/core/ai_engine.py b/core/ai_engine.py index 1e1af823..a4781cfe 100644 --- a/core/ai_engine.py +++ b/core/ai_engine.py @@ -1,9 +1,11 @@ + import json import re import logging import requests import google.generativeai as genai from meta_ai_api import MetaAI +from fuzzy_json import loads as fuzzy_loads logging.basicConfig( filename="karbon_ai_errors.log", @@ -13,33 +15,79 @@ ai_status = {"state": "connecting", "message": "Connecting to AI service..."} - def set_ai_status(state: str, message: str): ai_status["state"] = state ai_status["message"] = message logging.info(f"AI Status Updated: {state} - {message}") - def extract_json(response: str) -> dict: - clean_response = response.strip() - if clean_response.startswith("```json"): - clean_response = clean_response[len("```json"):].strip() - if clean_response.endswith("```"): - clean_response = clean_response[:-len("```")].strip() - + """ + Extract and parse JSON from the AI response, handling malformed input. + + Args: + response (str): Raw response from the AI service. + + Returns: + dict: Parsed JSON object with 'html', 'css', 'js', and 'name' keys. + + Raises: + ValueError: If JSON parsing fails or the structure is invalid. + """ + if not response or response.isspace(): + logging.error("Empty AI response received") + raise ValueError("Empty response from AI service") + + logging.info("Raw AI response: %s", response) + + # Remove markdown formatting + cleaned_response = re.sub(r'```json\s*|\s*```', '', response, flags=re.DOTALL) + cleaned_response = re.sub(r'^\s*```|```\s*$', '', cleaned_response, flags=re.DOTALL).strip() + + logging.info("Cleaned AI response: %s", cleaned_response[:500]) + + # Extract first valid JSON object from the string try: - return json.loads(clean_response) + stack = [] + start_idx = None + for i, char in enumerate(cleaned_response): + if char == '{': + if not stack: + start_idx = i + stack.append(char) + elif char == '}': + if stack: + stack.pop() + if not stack: + json_block = cleaned_response[start_idx:i + 1] + break + else: + raise ValueError("No valid JSON object found in response.") + + logging.info("Extracted JSON block: %s", json_block[:500]) + + parsed = json.loads(json_block) + + expected_keys = {"html", "css", "js", "name"} + if not isinstance(parsed, dict) or not expected_keys.issubset(parsed.keys()): + raise ValueError(f"Parsed JSON does not contain expected keys: {expected_keys}") + return parsed except json.JSONDecodeError as e: - logging.error(f"JSONDecodeError on cleaned string: {e}") - match = re.search(r'\{.*\}', clean_response, re.DOTALL) - if match: - try: - return json.loads(match.group(0)) - except json.JSONDecodeError as e_inner: - logging.error(f"JSONDecodeError during regex fallback: {e_inner}") - logging.error(f"No valid JSON found in response: {response}") - return None + logging.warning(f"Initial JSON parse failed: {e}") + except Exception as general_e: + logging.error(f"JSON extraction logic failed: {general_e}") + # Try fuzzy JSON parser + try: + parsed = fuzzy_loads(cleaned_response) + expected_keys = {"html", "css", "js", "name"} + if not isinstance(parsed, dict) or not expected_keys.issubset(parsed.keys()): + raise ValueError(f"Fuzzy JSON parse does not contain expected keys: {expected_keys}") + logging.info("Successfully parsed JSON using fuzzy-json") + return parsed + except Exception as fuzzy_e: + logging.error(f"Fuzzy JSON parse failed: {fuzzy_e}") + logging.error(f"Cleaned response: {cleaned_response}") + raise ValueError("Failed to parse AI response as JSON.") def generate_code_from_prompt(prompt: str, api_key: str = None, retries=2) -> str: formatted = ( @@ -51,14 +99,14 @@ def generate_code_from_prompt(prompt: str, api_key: str = None, retries=2) -> st " \"css\": \"body { ... }\",\n" " \"js\": \"document.addEventListener(...)\",\n" " \"name\": \"App Name\"\n" - "}" + "}\n" + "IMPORTANT: Do NOT include markdown (like ```json) or any trailing explanation or comments. Only return pure JSON." ) for attempt in range(retries + 1): try: set_ai_status("generating", "Generating code...") if api_key: - # Try Gemini try: genai.configure(api_key=api_key) model = genai.GenerativeModel('gemini-2.5-flash') @@ -66,10 +114,10 @@ def generate_code_from_prompt(prompt: str, api_key: str = None, retries=2) -> st logging.info(f"[Gemini] Raw AI response: {response}") except Exception as gem_e: logging.warning(f"[Gemini Fallback] Gemini failed: {gem_e}. Falling back to Meta AI.") - api_key = None # Force fallback + api_key = None continue + if not api_key: - # Fallback to Meta AI ai = MetaAI() result = ai.prompt(message=formatted) response = result.get("message", "") @@ -97,7 +145,6 @@ def generate_code_from_prompt(prompt: str, api_key: str = None, retries=2) -> st set_ai_status("offline", "All attempts to use AI failed.") return "Error

AI service is currently unavailable.

" - def optimize_prompt(prompt: str, api_key: str = None) -> str: print("[optimize_prompt] Called with:", prompt) if len(prompt.strip()) >= 20 and not is_generic(prompt): @@ -120,12 +167,10 @@ def optimize_prompt(prompt: str, api_key: str = None) -> str: logging.error(f"optimize_prompt failed with Gemini: {str(e)}") return rule_based_enhancement(prompt) - def rule_based_enhancement(prompt: str) -> str: prompt = prompt.strip() return f"{prompt}\n\nUse semantic HTML5 structure.\nApply responsive CSS (mobile-first).\nInclude clean modular JavaScript with comments." - def is_generic(prompt: str) -> bool: generic_phrases = { "make a website", "build ui", "create page", "webpage", "dashboard", "login", "landing page" From fe431295ae19c22d0a318c354c9459d57659569f Mon Sep 17 00:00:00 2001 From: Parthavi K Date: Sun, 3 Aug 2025 17:38:13 +0530 Subject: [PATCH 2/4] Update ai_engine.py --- core/ai_engine.py | 36 ++++++++---------------------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/core/ai_engine.py b/core/ai_engine.py index a4781cfe..a08d8e38 100644 --- a/core/ai_engine.py +++ b/core/ai_engine.py @@ -1,11 +1,9 @@ - import json import re import logging import requests import google.generativeai as genai from meta_ai_api import MetaAI -from fuzzy_json import loads as fuzzy_loads logging.basicConfig( filename="karbon_ai_errors.log", @@ -23,15 +21,6 @@ def set_ai_status(state: str, message: str): def extract_json(response: str) -> dict: """ Extract and parse JSON from the AI response, handling malformed input. - - Args: - response (str): Raw response from the AI service. - - Returns: - dict: Parsed JSON object with 'html', 'css', 'js', and 'name' keys. - - Raises: - ValueError: If JSON parsing fails or the structure is invalid. """ if not response or response.isspace(): logging.error("Empty AI response received") @@ -49,6 +38,7 @@ def extract_json(response: str) -> dict: try: stack = [] start_idx = None + json_block = None for i, char in enumerate(cleaned_response): if char == '{': if not stack: @@ -60,34 +50,23 @@ def extract_json(response: str) -> dict: if not stack: json_block = cleaned_response[start_idx:i + 1] break - else: + if not json_block: raise ValueError("No valid JSON object found in response.") logging.info("Extracted JSON block: %s", json_block[:500]) parsed = json.loads(json_block) - expected_keys = {"html", "css", "js", "name"} if not isinstance(parsed, dict) or not expected_keys.issubset(parsed.keys()): raise ValueError(f"Parsed JSON does not contain expected keys: {expected_keys}") return parsed + except json.JSONDecodeError as e: - logging.warning(f"Initial JSON parse failed: {e}") + logging.error(f"JSON decode failed: {e}") + raise ValueError("Failed to parse JSON.") except Exception as general_e: - logging.error(f"JSON extraction logic failed: {general_e}") - - # Try fuzzy JSON parser - try: - parsed = fuzzy_loads(cleaned_response) - expected_keys = {"html", "css", "js", "name"} - if not isinstance(parsed, dict) or not expected_keys.issubset(parsed.keys()): - raise ValueError(f"Fuzzy JSON parse does not contain expected keys: {expected_keys}") - logging.info("Successfully parsed JSON using fuzzy-json") - return parsed - except Exception as fuzzy_e: - logging.error(f"Fuzzy JSON parse failed: {fuzzy_e}") - logging.error(f"Cleaned response: {cleaned_response}") - raise ValueError("Failed to parse AI response as JSON.") + logging.error(f"JSON extraction failed: {general_e}") + raise ValueError("Failed to extract valid JSON.") def generate_code_from_prompt(prompt: str, api_key: str = None, retries=2) -> str: formatted = ( @@ -176,3 +155,4 @@ def is_generic(prompt: str) -> bool: "make a website", "build ui", "create page", "webpage", "dashboard", "login", "landing page" } return prompt.lower().strip() in generic_phrases + From 494cc3cb9cc618c96c0a35c60f1afef8d3f1700c Mon Sep 17 00:00:00 2001 From: Parthavi K Date: Sun, 3 Aug 2025 17:42:59 +0530 Subject: [PATCH 3/4] Update requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 383c51d6..bbf45ae6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ PyGithub GitPython meta-ai-api tkhtmlview +requests>=2.31.0 From e8708688e7042c7391753f81cd6695f9509b485d Mon Sep 17 00:00:00 2001 From: Parthavi K Date: Sun, 3 Aug 2025 17:54:58 +0530 Subject: [PATCH 4/4] Update ai_engine.py --- core/ai_engine.py | 334 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 233 insertions(+), 101 deletions(-) diff --git a/core/ai_engine.py b/core/ai_engine.py index a08d8e38..c9c30342 100644 --- a/core/ai_engine.py +++ b/core/ai_engine.py @@ -5,154 +5,286 @@ import google.generativeai as genai from meta_ai_api import MetaAI +# Configure logging logging.basicConfig( filename="karbon_ai_errors.log", level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s" ) +# Global AI status ai_status = {"state": "connecting", "message": "Connecting to AI service..."} def set_ai_status(state: str, message: str): + """Update AI status with proper error handling.""" ai_status["state"] = state ai_status["message"] = message logging.info(f"AI Status Updated: {state} - {message}") def extract_json(response: str) -> dict: """ - Extract and parse JSON from the AI response, handling malformed input. + Extract and parse JSON from the AI response with improved error handling. """ if not response or response.isspace(): logging.error("Empty AI response received") raise ValueError("Empty response from AI service") + + logging.info("Raw AI response length: %d", len(response)) + + # Clean the response more thoroughly + cleaned_response = response.strip() + + # Remove markdown code blocks + cleaned_response = re.sub(r'```json\s*', '', cleaned_response, flags=re.IGNORECASE) + cleaned_response = re.sub(r'```\s*$', '', cleaned_response, flags=re.MULTILINE) + cleaned_response = re.sub(r'^```|```$', '', cleaned_response, flags=re.MULTILINE) + + # Remove any leading/trailing non-JSON text + cleaned_response = cleaned_response.strip() + + logging.info("Cleaned response preview: %s", cleaned_response[:200] + "..." if len(cleaned_response) > 200 else cleaned_response) + + # Try multiple JSON extraction strategies + strategies = [ + lambda x: x, # Try as-is first + lambda x: find_json_block(x), # Find JSON block + lambda x: extract_between_braces(x), # Extract content between first { and last } + ] + + for i, strategy in enumerate(strategies): + try: + candidate = strategy(cleaned_response) + if not candidate: + continue + + logging.info(f"Strategy {i+1} candidate: %s", candidate[:200] + "..." if len(candidate) > 200 else candidate) + + # Parse JSON + parsed = json.loads(candidate) + + # Validate structure + if validate_json_structure(parsed): + logging.info("Successfully extracted and validated JSON") + return parsed + else: + logging.warning(f"Strategy {i+1}: JSON structure validation failed") + + except json.JSONDecodeError as e: + logging.warning(f"Strategy {i+1}: JSON decode failed - {e}") + continue + except Exception as e: + logging.warning(f"Strategy {i+1}: General error - {e}") + continue + + # If all strategies fail, try to create a minimal valid response + logging.error("All JSON extraction strategies failed") + raise ValueError("Failed to extract valid JSON from AI response") - logging.info("Raw AI response: %s", response) - - # Remove markdown formatting - cleaned_response = re.sub(r'```json\s*|\s*```', '', response, flags=re.DOTALL) - cleaned_response = re.sub(r'^\s*```|```\s*$', '', cleaned_response, flags=re.DOTALL).strip() - - logging.info("Cleaned AI response: %s", cleaned_response[:500]) - - # Extract first valid JSON object from the string - try: - stack = [] - start_idx = None - json_block = None - for i, char in enumerate(cleaned_response): - if char == '{': - if not stack: - start_idx = i - stack.append(char) - elif char == '}': - if stack: - stack.pop() - if not stack: - json_block = cleaned_response[start_idx:i + 1] - break - if not json_block: - raise ValueError("No valid JSON object found in response.") +def find_json_block(text: str) -> str: + """Find the first complete JSON object in the text.""" + brace_count = 0 + start_idx = None + + for i, char in enumerate(text): + if char == '{': + if brace_count == 0: + start_idx = i + brace_count += 1 + elif char == '}': + brace_count -= 1 + if brace_count == 0 and start_idx is not None: + return text[start_idx:i + 1] + + return "" - logging.info("Extracted JSON block: %s", json_block[:500]) +def extract_between_braces(text: str) -> str: + """Extract content between the first { and last }.""" + first_brace = text.find('{') + last_brace = text.rfind('}') + + if first_brace != -1 and last_brace != -1 and first_brace < last_brace: + return text[first_brace:last_brace + 1] + + return "" - parsed = json.loads(json_block) - expected_keys = {"html", "css", "js", "name"} - if not isinstance(parsed, dict) or not expected_keys.issubset(parsed.keys()): - raise ValueError(f"Parsed JSON does not contain expected keys: {expected_keys}") - return parsed +def validate_json_structure(parsed: dict) -> bool: + """Validate that the parsed JSON has the expected structure.""" + if not isinstance(parsed, dict): + return False + + required_keys = {"html", "css", "js", "name"} + if not required_keys.issubset(parsed.keys()): + logging.warning(f"Missing required keys. Expected: {required_keys}, Got: {set(parsed.keys())}") + return False + + # Check that values are strings + for key in required_keys: + if not isinstance(parsed[key], str): + logging.warning(f"Key '{key}' is not a string: {type(parsed[key])}") + return False + + return True - except json.JSONDecodeError as e: - logging.error(f"JSON decode failed: {e}") - raise ValueError("Failed to parse JSON.") - except Exception as general_e: - logging.error(f"JSON extraction failed: {general_e}") - raise ValueError("Failed to extract valid JSON.") +def create_fallback_response(prompt: str) -> dict: + """Create a basic fallback response when AI fails.""" + return { + "html": f"Generated App

Welcome

App generated from: {prompt[:100]}{'...' if len(prompt) > 100 else ''}

", + "css": "body { font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }", + "js": "console.log('App loaded successfully');", + "name": "Generated App" + } -def generate_code_from_prompt(prompt: str, api_key: str = None, retries=2) -> str: - formatted = ( - f"You are a helpful assistant that writes complete frontend apps.\n" - f"Given the task: \"{prompt}\"\n" - f"Respond ONLY in this JSON format, with no additional text, markdown, or explanation before or after the JSON:\n" +def generate_code_from_prompt(prompt: str, api_key: str = None, retries: int = 3) -> str: + """ + Generate code from prompt with improved error handling and fallbacks. + """ + formatted_prompt = ( + f"Create a complete web application for: {prompt}\n\n" + f"Respond with ONLY a JSON object in this exact format (no markdown, no extra text):\n" "{\n" - " \"html\": \"...\",\n" - " \"css\": \"body { ... }\",\n" - " \"js\": \"document.addEventListener(...)\",\n" - " \"name\": \"App Name\"\n" - "}\n" - "IMPORTANT: Do NOT include markdown (like ```json) or any trailing explanation or comments. Only return pure JSON." + ' "html": "...",\n' + ' "css": "body { ... }",\n' + ' "js": "// JavaScript code here",\n' + ' "name": "App Name"\n' + "}\n\n" + "CRITICAL: Return ONLY the JSON object, no explanations, no markdown formatting." ) - + + last_error = None + for attempt in range(retries + 1): try: - set_ai_status("generating", "Generating code...") + set_ai_status("generating", f"Generating code (attempt {attempt + 1})...") + response = None + + # Try Gemini first if API key provided if api_key: try: genai.configure(api_key=api_key) - model = genai.GenerativeModel('gemini-2.5-flash') - response = model.generate_content(formatted).text - logging.info(f"[Gemini] Raw AI response: {response}") + model = genai.GenerativeModel('gemini-2.0-flash-exp') + result = model.generate_content(formatted_prompt) + response = result.text + logging.info("Gemini response received") except Exception as gem_e: - logging.warning(f"[Gemini Fallback] Gemini failed: {gem_e}. Falling back to Meta AI.") - api_key = None - continue - - if not api_key: - ai = MetaAI() - result = ai.prompt(message=formatted) - response = result.get("message", "") - logging.info(f"[MetaAI] Raw AI response: {response}") - + logging.warning(f"Gemini failed: {gem_e}") + last_error = gem_e + + # Fallback to Meta AI + if not response: + try: + ai = MetaAI() + result = ai.prompt(message=formatted_prompt) + response = result.get("message", "") + logging.info("Meta AI response received") + except Exception as meta_e: + logging.warning(f"Meta AI failed: {meta_e}") + last_error = meta_e + + if not response: + raise Exception("No response from any AI service") + + # Extract and validate JSON parsed = extract_json(response) - if not parsed: - raise ValueError("AI response couldn't be parsed into JSON.") - - set_ai_status("online", "AI service is online.") - html = str(parsed.get("html", "")) - css = str(parsed.get("css", "")) - js = str(parsed.get("js", "")) - - final_code = html.replace("", f"") \ - .replace("", f"") - logging.info(f"Final inlined HTML code (first 500 chars): {str(final_code)[:500]}...") - return final_code + + # Create final HTML with inlined CSS and JS + html = parsed.get("html", "") + css = parsed.get("css", "") + js = parsed.get("js", "") + + # Ensure HTML has proper structure + if not html.strip().startswith(""): + html = f"{parsed.get('name', 'Generated App')}{html}" + + # Inline CSS and JS + if css and "") + + if js and "") + + set_ai_status("online", "AI service is online") + logging.info("Successfully generated code") + return html + except Exception as e: - logging.error(f"[AI Error] Attempt {attempt + 1} failed: {str(e)}") - set_ai_status("error", f"AI error: {str(e)}") - import time - time.sleep(2 ** attempt) - - set_ai_status("offline", "All attempts to use AI failed.") - return "Error

AI service is currently unavailable.

" + logging.error(f"Attempt {attempt + 1} failed: {str(e)}") + last_error = e + set_ai_status("error", f"Attempt {attempt + 1} failed: {str(e)}") + + # Wait before retry (exponential backoff) + if attempt < retries: + import time + wait_time = min(2 ** attempt, 10) # Cap at 10 seconds + time.sleep(wait_time) + + # All attempts failed - create fallback response + logging.error("All attempts failed, creating fallback response") + set_ai_status("offline", f"AI service unavailable: {str(last_error)}") + + fallback = create_fallback_response(prompt) + return fallback["html"].replace("", f"").replace("", f"") def optimize_prompt(prompt: str, api_key: str = None) -> str: - print("[optimize_prompt] Called with:", prompt) + """ + Optimize vague prompts with better error handling. + """ + print(f"[optimize_prompt] Input: {prompt}") + + # If prompt is already detailed enough, return as-is if len(prompt.strip()) >= 20 and not is_generic(prompt): - return prompt - + return prompt.strip() + + optimization_prompt = ( + f"Transform this brief UI request into a detailed web development specification: '{prompt.strip()}'\n\n" + f"Include:\n" + f"- Specific layout and components\n" + f"- Visual styling preferences\n" + f"- Any interactive features\n" + f"- Target audience considerations\n\n" + f"Return only the enhanced prompt, no explanations." + ) + try: if api_key: genai.configure(api_key=api_key) - model = genai.GenerativeModel('gemini-2.5-flash') - enriched = model.generate_content( - f"Transform the following vague or minimal UI prompt into a clear, detailed, and professional instruction specifically for frontend web development. " - f"Focus on HTML, CSS, and JavaScript. Include layout details, components to be included, styling considerations, and any interactivity if relevant. " - f"Keep the improved prompt concise yet specific. Do not add commentary or formatting:\n\n" - f"User Prompt: \"{prompt.strip()}\"\n\n" - f"Refined Prompt:" - ).text.strip() - if enriched: - return enriched + model = genai.GenerativeModel('gemini-2.0-flash-exp') + result = model.generate_content(optimization_prompt) + enhanced = result.text.strip() + + if enhanced and len(enhanced) > len(prompt): + logging.info("Prompt successfully optimized with Gemini") + return enhanced + except Exception as e: - logging.error(f"optimize_prompt failed with Gemini: {str(e)}") + logging.warning(f"Prompt optimization failed: {e}") + + # Fallback to rule-based enhancement return rule_based_enhancement(prompt) def rule_based_enhancement(prompt: str) -> str: + """ + Rule-based prompt enhancement as fallback. + """ prompt = prompt.strip() - return f"{prompt}\n\nUse semantic HTML5 structure.\nApply responsive CSS (mobile-first).\nInclude clean modular JavaScript with comments." + + enhancements = [ + "Use modern HTML5 semantic structure with proper accessibility.", + "Apply responsive CSS with mobile-first approach and modern design principles.", + "Include interactive JavaScript features with smooth animations.", + "Ensure clean, professional styling with good typography and color scheme." + ] + + return f"{prompt}\n\n" + " ".join(enhancements) def is_generic(prompt: str) -> bool: + """ + Check if prompt is too generic and needs enhancement. + """ generic_phrases = { - "make a website", "build ui", "create page", "webpage", "dashboard", "login", "landing page" + "make a website", "build ui", "create page", "webpage", + "dashboard", "login", "landing page", "app", "site" } - return prompt.lower().strip() in generic_phrases + + prompt_lower = prompt.lower().strip() + return any(phrase in prompt_lower for phrase in generic_phrases) and len(prompt_lower) < 30