diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..7dd21f9 --- /dev/null +++ b/backend/config.py @@ -0,0 +1,11 @@ +# Useful for debugging purposes when you don't want to waste GPT4-Vision credits +# Setting to True will stream a mock response instead of calling the OpenAI API +# TODO: Should only be set to true when value is 'True', not any abitrary truthy value +import os + + +SHOULD_MOCK_AI_RESPONSE = bool(os.environ.get("MOCK", False)) + +# Set to True when running in production (on the hosted version) +# Used as a feature flag to enable or disable certain features +IS_PROD = os.environ.get("IS_PROD", False) diff --git a/backend/main.py b/backend/main.py index 593ec3a..3ea43d3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,27 +1,12 @@ # Load environment variables first from dotenv import load_dotenv - load_dotenv() -import json -import os -import traceback -from datetime import datetime -from fastapi import FastAPI, WebSocket +from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import HTMLResponse -import openai -from llm import stream_openai_response -from openai.types.chat import ChatCompletionMessageParam -from mock_llm import mock_completion -from utils import pprint_prompt -from typing import Dict, List -from image_generation import create_alt_url_mapping, generate_images -from prompts import assemble_prompt -from routes import screenshot -from access_token import validate_access_token +from routes import screenshot, generate_code, home app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None) @@ -34,240 +19,7 @@ app.add_middleware( allow_headers=["*"], ) - -# Useful for debugging purposes when you don't want to waste GPT4-Vision credits -# Setting to True will stream a mock response instead of calling the OpenAI API -# TODO: Should only be set to true when value is 'True', not any abitrary truthy value -SHOULD_MOCK_AI_RESPONSE = bool(os.environ.get("MOCK", False)) - -# Set to True when running in production (on the hosted version) -# Used as a feature flag to enable or disable certain features -IS_PROD = os.environ.get("IS_PROD", False) - - +# Add routes +app.include_router(generate_code.router) app.include_router(screenshot.router) - - -@app.get("/") -async def get_status(): - return HTMLResponse( - content="

Your backend is running correctly. Please open the front-end URL (default is http://localhost:5173) to use screenshot-to-code.

" - ) - - -def write_logs(prompt_messages: List[ChatCompletionMessageParam], completion: str): - # Get the logs path from environment, default to the current working directory - logs_path = os.environ.get("LOGS_PATH", os.getcwd()) - - # Create run_logs directory if it doesn't exist within the specified logs path - logs_directory = os.path.join(logs_path, "run_logs") - if not os.path.exists(logs_directory): - os.makedirs(logs_directory) - - print("Writing to logs directory:", logs_directory) - - # Generate a unique filename using the current timestamp within the logs directory - filename = datetime.now().strftime(f"{logs_directory}/messages_%Y%m%d_%H%M%S.json") - - # Write the messages dict into a new file for each run - with open(filename, "w") as f: - f.write(json.dumps({"prompt": prompt_messages, "completion": completion})) - - -@app.websocket("/generate-code") -async def stream_code(websocket: WebSocket): - await websocket.accept() - - print("Incoming websocket connection...") - - async def throw_error( - message: str, - ): - await websocket.send_json({"type": "error", "value": message}) - await websocket.close() - - # TODO: Are the values always strings? - params: Dict[str, str] = await websocket.receive_json() - - print("Received params") - - # Read the code config settings from the request. Fall back to default if not provided. - generated_code_config = "" - if "generatedCodeConfig" in params and params["generatedCodeConfig"]: - generated_code_config = params["generatedCodeConfig"] - print(f"Generating {generated_code_config} code") - - # Get the OpenAI API key from the request. Fall back to environment variable if not provided. - # If neither is provided, we throw an error. - openai_api_key = None - if "accessCode" in params and params["accessCode"]: - print("Access code - using platform API key") - res = await validate_access_token(params["accessCode"]) - if res["success"]: - openai_api_key = os.environ.get("PLATFORM_OPENAI_API_KEY") - else: - await websocket.send_json( - { - "type": "error", - "value": res["failure_reason"], - } - ) - return - else: - if params["openAiApiKey"]: - openai_api_key = params["openAiApiKey"] - print("Using OpenAI API key from client-side settings dialog") - else: - openai_api_key = os.environ.get("OPENAI_API_KEY") - if openai_api_key: - print("Using OpenAI API key from environment variable") - - if not openai_api_key: - print("OpenAI API key not found") - await websocket.send_json( - { - "type": "error", - "value": "No OpenAI API key found. Please add your API key in the settings dialog or add it to backend/.env file.", - } - ) - return - - # Get the OpenAI Base URL from the request. Fall back to environment variable if not provided. - openai_base_url = None - # Disable user-specified OpenAI Base URL in prod - if not os.environ.get("IS_PROD"): - if "openAiBaseURL" in params and params["openAiBaseURL"]: - openai_base_url = params["openAiBaseURL"] - print("Using OpenAI Base URL from client-side settings dialog") - else: - openai_base_url = os.environ.get("OPENAI_BASE_URL") - if openai_base_url: - print("Using OpenAI Base URL from environment variable") - - if not openai_base_url: - print("Using official OpenAI URL") - - # Get the image generation flag from the request. Fall back to True if not provided. - should_generate_images = ( - params["isImageGenerationEnabled"] - if "isImageGenerationEnabled" in params - else True - ) - - print("generating code...") - await websocket.send_json({"type": "status", "value": "Generating code..."}) - - async def process_chunk(content: str): - await websocket.send_json({"type": "chunk", "value": content}) - - # Assemble the prompt - try: - if params.get("resultImage") and params["resultImage"]: - prompt_messages = assemble_prompt( - params["image"], generated_code_config, params["resultImage"] - ) - else: - prompt_messages = assemble_prompt(params["image"], generated_code_config) - except: - await websocket.send_json( - { - "type": "error", - "value": "Error assembling prompt. Contact support at support@picoapps.xyz", - } - ) - await websocket.close() - return - - # Image cache for updates so that we don't have to regenerate images - image_cache: Dict[str, str] = {} - - if params["generationType"] == "update": - # Transform into message format - # TODO: Move this to frontend - for index, text in enumerate(params["history"]): - if index % 2 == 0: - message: ChatCompletionMessageParam = { - "role": "assistant", - "content": text, - } - else: - message: ChatCompletionMessageParam = { - "role": "user", - "content": text, - } - prompt_messages.append(message) - image_cache = create_alt_url_mapping(params["history"][-2]) - - if SHOULD_MOCK_AI_RESPONSE: - completion = await mock_completion(process_chunk) - else: - try: - completion = await stream_openai_response( - prompt_messages, - api_key=openai_api_key, - base_url=openai_base_url, - callback=lambda x: process_chunk(x), - ) - except openai.AuthenticationError as e: - print("[GENERATE_CODE] Authentication failed", e) - error_message = ( - "Incorrect OpenAI key. Please make sure your OpenAI API key is correct, or create a new OpenAI API key on your OpenAI dashboard." - + ( - " Alternatively, you can purchase code generation credits directly on this website." - if IS_PROD - else "" - ) - ) - return await throw_error(error_message) - except openai.NotFoundError as e: - print("[GENERATE_CODE] Model not found", e) - error_message = ( - e.message - + ". Please make sure you have followed the instructions correctly to obtain an OpenAI key with GPT vision access: https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md" - + ( - " Alternatively, you can purchase code generation credits directly on this website." - if IS_PROD - else "" - ) - ) - return await throw_error(error_message) - except openai.RateLimitError as e: - print("[GENERATE_CODE] Rate limit exceeded", e) - error_message = ( - "OpenAI error - 'You exceeded your current quota, please check your plan and billing details.'" - + ( - " Alternatively, you can purchase code generation credits directly on this website." - if IS_PROD - else "" - ) - ) - return await throw_error(error_message) - - # Write the messages dict into a log so that we can debug later - write_logs(prompt_messages, completion) - - try: - if should_generate_images: - await websocket.send_json( - {"type": "status", "value": "Generating images..."} - ) - updated_html = await generate_images( - completion, - api_key=openai_api_key, - base_url=openai_base_url, - image_cache=image_cache, - ) - else: - updated_html = completion - await websocket.send_json({"type": "setCode", "value": updated_html}) - await websocket.send_json( - {"type": "status", "value": "Code generation complete."} - ) - except Exception as e: - traceback.print_exc() - print("Image generation failed", e) - await websocket.send_json( - {"type": "status", "value": "Image generation failed but code is complete."} - ) - - await websocket.close() +app.include_router(home.router) diff --git a/backend/routes/generate_code.py b/backend/routes/generate_code.py new file mode 100644 index 0000000..a44aeac --- /dev/null +++ b/backend/routes/generate_code.py @@ -0,0 +1,235 @@ +import os +import traceback +from fastapi import APIRouter, WebSocket +import openai +from config import IS_PROD, SHOULD_MOCK_AI_RESPONSE +from llm import stream_openai_response +from openai.types.chat import ChatCompletionMessageParam +from mock_llm import mock_completion +from typing import Dict, List +from image_generation import create_alt_url_mapping, generate_images +from prompts import assemble_prompt +from access_token import validate_access_token +from datetime import datetime +import json + + +router = APIRouter() + + +def write_logs(prompt_messages: List[ChatCompletionMessageParam], completion: str): + # Get the logs path from environment, default to the current working directory + logs_path = os.environ.get("LOGS_PATH", os.getcwd()) + + # Create run_logs directory if it doesn't exist within the specified logs path + logs_directory = os.path.join(logs_path, "run_logs") + if not os.path.exists(logs_directory): + os.makedirs(logs_directory) + + print("Writing to logs directory:", logs_directory) + + # Generate a unique filename using the current timestamp within the logs directory + filename = datetime.now().strftime(f"{logs_directory}/messages_%Y%m%d_%H%M%S.json") + + # Write the messages dict into a new file for each run + with open(filename, "w") as f: + f.write(json.dumps({"prompt": prompt_messages, "completion": completion})) + + +@router.websocket("/generate-code") +async def stream_code(websocket: WebSocket): + await websocket.accept() + + print("Incoming websocket connection...") + + async def throw_error( + message: str, + ): + await websocket.send_json({"type": "error", "value": message}) + await websocket.close() + + # TODO: Are the values always strings? + params: Dict[str, str] = await websocket.receive_json() + + print("Received params") + + # Read the code config settings from the request. Fall back to default if not provided. + generated_code_config = "" + if "generatedCodeConfig" in params and params["generatedCodeConfig"]: + generated_code_config = params["generatedCodeConfig"] + print(f"Generating {generated_code_config} code") + + # Get the OpenAI API key from the request. Fall back to environment variable if not provided. + # If neither is provided, we throw an error. + openai_api_key = None + if "accessCode" in params and params["accessCode"]: + print("Access code - using platform API key") + res = await validate_access_token(params["accessCode"]) + if res["success"]: + openai_api_key = os.environ.get("PLATFORM_OPENAI_API_KEY") + else: + await websocket.send_json( + { + "type": "error", + "value": res["failure_reason"], + } + ) + return + else: + if params["openAiApiKey"]: + openai_api_key = params["openAiApiKey"] + print("Using OpenAI API key from client-side settings dialog") + else: + openai_api_key = os.environ.get("OPENAI_API_KEY") + if openai_api_key: + print("Using OpenAI API key from environment variable") + + if not openai_api_key: + print("OpenAI API key not found") + await websocket.send_json( + { + "type": "error", + "value": "No OpenAI API key found. Please add your API key in the settings dialog or add it to backend/.env file.", + } + ) + return + + # Get the OpenAI Base URL from the request. Fall back to environment variable if not provided. + openai_base_url = None + # Disable user-specified OpenAI Base URL in prod + if not os.environ.get("IS_PROD"): + if "openAiBaseURL" in params and params["openAiBaseURL"]: + openai_base_url = params["openAiBaseURL"] + print("Using OpenAI Base URL from client-side settings dialog") + else: + openai_base_url = os.environ.get("OPENAI_BASE_URL") + if openai_base_url: + print("Using OpenAI Base URL from environment variable") + + if not openai_base_url: + print("Using official OpenAI URL") + + # Get the image generation flag from the request. Fall back to True if not provided. + should_generate_images = ( + params["isImageGenerationEnabled"] + if "isImageGenerationEnabled" in params + else True + ) + + print("generating code...") + await websocket.send_json({"type": "status", "value": "Generating code..."}) + + async def process_chunk(content: str): + await websocket.send_json({"type": "chunk", "value": content}) + + # Assemble the prompt + try: + if params.get("resultImage") and params["resultImage"]: + prompt_messages = assemble_prompt( + params["image"], generated_code_config, params["resultImage"] + ) + else: + prompt_messages = assemble_prompt(params["image"], generated_code_config) + except: + await websocket.send_json( + { + "type": "error", + "value": "Error assembling prompt. Contact support at support@picoapps.xyz", + } + ) + await websocket.close() + return + + # Image cache for updates so that we don't have to regenerate images + image_cache: Dict[str, str] = {} + + if params["generationType"] == "update": + # Transform into message format + # TODO: Move this to frontend + for index, text in enumerate(params["history"]): + if index % 2 == 0: + message: ChatCompletionMessageParam = { + "role": "assistant", + "content": text, + } + else: + message: ChatCompletionMessageParam = { + "role": "user", + "content": text, + } + prompt_messages.append(message) + image_cache = create_alt_url_mapping(params["history"][-2]) + + if SHOULD_MOCK_AI_RESPONSE: + completion = await mock_completion(process_chunk) + else: + try: + completion = await stream_openai_response( + prompt_messages, + api_key=openai_api_key, + base_url=openai_base_url, + callback=lambda x: process_chunk(x), + ) + except openai.AuthenticationError as e: + print("[GENERATE_CODE] Authentication failed", e) + error_message = ( + "Incorrect OpenAI key. Please make sure your OpenAI API key is correct, or create a new OpenAI API key on your OpenAI dashboard." + + ( + " Alternatively, you can purchase code generation credits directly on this website." + if IS_PROD + else "" + ) + ) + return await throw_error(error_message) + except openai.NotFoundError as e: + print("[GENERATE_CODE] Model not found", e) + error_message = ( + e.message + + ". Please make sure you have followed the instructions correctly to obtain an OpenAI key with GPT vision access: https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md" + + ( + " Alternatively, you can purchase code generation credits directly on this website." + if IS_PROD + else "" + ) + ) + return await throw_error(error_message) + except openai.RateLimitError as e: + print("[GENERATE_CODE] Rate limit exceeded", e) + error_message = ( + "OpenAI error - 'You exceeded your current quota, please check your plan and billing details.'" + + ( + " Alternatively, you can purchase code generation credits directly on this website." + if IS_PROD + else "" + ) + ) + return await throw_error(error_message) + + # Write the messages dict into a log so that we can debug later + write_logs(prompt_messages, completion) + + try: + if should_generate_images: + await websocket.send_json( + {"type": "status", "value": "Generating images..."} + ) + updated_html = await generate_images( + completion, + api_key=openai_api_key, + base_url=openai_base_url, + image_cache=image_cache, + ) + else: + updated_html = completion + await websocket.send_json({"type": "setCode", "value": updated_html}) + await websocket.send_json( + {"type": "status", "value": "Code generation complete."} + ) + except Exception as e: + traceback.print_exc() + print("Image generation failed", e) + await websocket.send_json( + {"type": "status", "value": "Image generation failed but code is complete."} + ) + + await websocket.close() diff --git a/backend/routes/home.py b/backend/routes/home.py new file mode 100644 index 0000000..c9f66b4 --- /dev/null +++ b/backend/routes/home.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter +from fastapi.responses import HTMLResponse + + +router = APIRouter() + + +@router.get("/") +async def get_status(): + return HTMLResponse( + content="

Your backend is running correctly. Please open the front-end URL (default is http://localhost:5173) to use screenshot-to-code.

" + )