From d65495541fb6f970aff41172a174f6dfdbbcc7fd Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Tue, 22 Jul 2025 20:25:42 +1000 Subject: [PATCH] Move LLM logic to seperate route --- app.py | 150 +----------------- routes/llm.py | 149 +++++++++++++++++ static/js/mithril/editor.js | 3 +- .../dashboard/http_functions/editor.html | 1 + templates/dashboard/http_functions/new.html | 1 + templates/dashboard/timer_functions/edit.html | 1 + templates/dashboard/timer_functions/new.html | 1 + templates/home.html | 1 + 8 files changed, 164 insertions(+), 143 deletions(-) create mode 100644 routes/llm.py diff --git a/app.py b/app.py index 48f66f2..3042201 100644 --- a/app.py +++ b/app.py @@ -14,6 +14,7 @@ from routes.timer import timer from routes.test import test from routes.home import home from routes.http import http +from routes.llm import llm from flask_apscheduler import APScheduler import asyncio import aiohttp @@ -39,6 +40,7 @@ app.register_blueprint(timer, url_prefix='/timer') app.register_blueprint(test, url_prefix='/test') app.register_blueprint(home, url_prefix='/home') app.register_blueprint(http, url_prefix='/http') +app.register_blueprint(llm, url_prefix='/llm') class User(UserMixin): def __init__(self, id, username, password_hash, created_at): @@ -120,149 +122,13 @@ def home(): def documentation(): return render_template("documentation.html") -def _generate_script_from_natural_language(natural_query): - """Generates a Javascript function from natural language using Gemini REST API.""" - gemni_model = os.environ.get("GEMINI_MODEL", "gemini-1.5-flash") - api_key = os.environ.get("GEMINI_API_KEY") - if not api_key: - return None, "GEMINI_API_KEY environment variable not set." - - api_url = f"https://generativelanguage.googleapis.com/v1beta/models/{gemni_model}:generateContent?key={api_key}" - headers = {'Content-Type': 'application/json'} - - try: - prompt = f""" -You are an expert Javascript developer. Your task is to write a complete, production-ready Javascript async function based on the user's request. - -**Function Signature:** -Your function MUST have the following signature: `async (req, environment) => {{ ... }}` -- `req`: An object containing details about the incoming HTTP request (e.g., `req.method`, `req.headers`, `req.body`, `req.query`). -- `environment`: A mutable JSON object that persists across executions. You can read and write to it to maintain state. - -**Environment & Constraints:** -- The function will be executed in a simple, sandboxed Javascript environment. -- **CRITICAL**: ALL helper functions or variables MUST be defined *inside* the main `async` function. Do not define anything in the global scope. -- **DO NOT** use `require()`, `import`, or any other module loading system. -- **DO NOT** access the file system (`fs` module) or make network requests. -- You have access to a `console.log()` function for logging. - -**Response Helpers:** -You must use one of the following functions to return a response: -- `HtmlResponse(body)`: Returns an HTML response. -- `JsonResponse(body)`: Returns a JSON response. -- `TextResponse(body)`: Returns a plain text response. -- `RedirectResponse(url)`: Redirects the user. - -**Complex Example (Tic-Tac-Toe):** -```javascript -async (req, environment) => {{ - // Helper function defined INSIDE the main function - function checkWinner(board) {{ - const winConditions = [ - [0, 1, 2], [3, 4, 5], [6, 7, 8], - [0, 3, 6], [1, 4, 7], [2, 5, 8], - [0, 4, 8], [2, 4, 6] - ]; - for (const condition of winConditions) {{ - const [a, b, c] = condition; - if (board[a] && board[a] === board[b] && board[a] === board[c]) {{ - return board[a]; - }} - }} - return null; - }} - - if (!environment.board) {{ - environment.board = Array(9).fill(""); - environment.turn = "X"; - environment.winner = null; - }} - - if (req.method === "POST" && req.json && req.json.move !== undefined) {{ - const move = parseInt(req.json.move); - if (environment.board[move] === "" && !environment.winner) {{ - environment.board[move] = environment.turn; - environment.turn = environment.turn === "X" ? "O" : "X"; - environment.winner = checkWinner(environment.board); - }} - }} - - const boardHTML = environment.board.map((cell, index) => ``).join(""); - const message = environment.winner ? `Player ${{environment.winner}} wins!` : `Turn: ${{environment.turn}}`; - - return HtmlResponse(` - - Tic-Tac-Toe -

${{message}}

${{boardHTML}}
- - `); -}} -``` - -**User's request:** "{natural_query}" - -Return ONLY the complete Javascript function code, without any explanation, comments, or surrounding text/markdown. -""" - payload = json.dumps({ - "contents": [{"parts": [{"text": prompt}]}] - }) - - response = requests.post(api_url, headers=headers, data=payload) - response.raise_for_status() - - response_data = response.json() - - candidates = response_data.get('candidates', []) - if not candidates: - return None, "No candidates found in API response." - - content = candidates[0].get('content', {}) - parts = content.get('parts', []) - if not parts: - return None, "No parts found in API response content." - - generated_script = parts[0].get('text', '').strip() - - # More robustly extract from markdown - if generated_script.startswith("```javascript"): - generated_script = generated_script[12:] - if generated_script.endswith("```"): - generated_script = generated_script[:-3] - - generated_script = generated_script.strip() - - # Remove any leading non-code characters - if not generated_script.startswith('async ('): - async_start = generated_script.find('async (') - if async_start != -1: - generated_script = generated_script[async_start:] - - return generated_script.strip(), None - - except requests.exceptions.RequestException as e: - app.logger.error(f"Gemini API request error: {e}") - return None, f"Error communicating with API: {e}" - except (KeyError, IndexError, Exception) as e: - app.logger.error(f"Error processing Gemini API response: {e} - Response: {response_data if 'response_data' in locals() else 'N/A'}") - return None, f"Error processing API response: {e}" - -@app.route("/api/generate_script", methods=["POST"]) +@ app.route("/dashboard", methods=["GET"]) @login_required -def generate_script(): - try: - natural_query = request.json.get('natural_query') - if not natural_query: - return jsonify({"error": "natural_query is required"}), 400 - - script_content, error = _generate_script_from_natural_language(natural_query) - - if error: - return jsonify({"error": error}), 500 - - return jsonify({"script_content": script_content}) - - except Exception as e: - return jsonify({'error': str(e)}), 500 +def dashboard(): + user_id = current_user.id + http_functions = db.get_http_functions_for_user(user_id) + http_functions = create_http_functions_view_model(http_functions) + return render_template("dashboard/http_functions/overview.html", http_functions=http_functions) @app.route('/execute', methods=['POST']) def execute_code(): diff --git a/routes/llm.py b/routes/llm.py new file mode 100644 index 0000000..c49c03b --- /dev/null +++ b/routes/llm.py @@ -0,0 +1,149 @@ +import os +import json +import requests +from flask import Blueprint, jsonify, request +from flask_login import login_required + +llm = Blueprint('llm', __name__) + +def _generate_script_from_natural_language(natural_query): + """Generates a Javascript function from natural language using Gemini REST API.""" + gemni_model = os.environ.get("GEMINI_MODEL", "gemini-1.5-flash") + api_key = os.environ.get("GEMINI_API_KEY") + if not api_key: + return None, "GEMINI_API_KEY environment variable not set." + + api_url = f"https://generativelanguage.googleapis.com/v1beta/models/{gemni_model}:generateContent?key={api_key}" + headers = {'Content-Type': 'application/json'} + + try: + prompt = f""" +You are an expert Javascript developer. Your task is to write a complete, production-ready Javascript async function based on the user's request. + +**Function Signature:** +Your function MUST have the following signature: `async (req, environment) => {{ ... }}` +- `req`: An object containing details about the incoming HTTP request (e.g., `req.method`, `req.headers`, `req.body`, `req.query`). +- `environment`: A mutable JSON object that persists across executions. You can read and write to it to maintain state. + +**Environment & Constraints:** +- The function will be executed in a simple, sandboxed Javascript environment. +- **CRITICAL**: ALL helper functions or variables MUST be defined *inside* the main `async` function. Do not define anything in the global scope. +- **DO NOT** use `require()`, `import`, or any other module loading system. +- **DO NOT** access the file system (`fs` module) or make network requests. +- You have access to a `console.log()` function for logging. + +**Response Helpers:** +You must use one of the following functions to return a response: +- `HtmlResponse(body)`: Returns an HTML response. +- `JsonResponse(body)`: Returns a JSON response. +- `TextResponse(body)`: Returns a plain text response. +- `RedirectResponse(url)`: Redirects the user. + +**Complex Example (Tic-Tac-Toe):** +```javascript +async (req, environment) => {{ + // Helper function defined INSIDE the main function + function checkWinner(board) {{ + const winConditions = [ + [0, 1, 2], [3, 4, 5], [6, 7, 8], + [0, 3, 6], [1, 4, 7], [2, 5, 8], + [0, 4, 8], [2, 4, 6] + ]; + for (const condition of winConditions) {{ + const [a, b, c] = condition; + if (board[a] && board[a] === board[b] && board[a] === board[c]) {{ + return board[a]; + }} + }} + return null; + }} + + if (!environment.board) {{ + environment.board = Array(9).fill(""); + environment.turn = "X"; + environment.winner = null; + }} + + if (req.method === "POST" && req.json && req.json.move !== undefined) {{ + const move = parseInt(req.json.move); + if (environment.board[move] === "" && !environment.winner) {{ + environment.board[move] = environment.turn; + environment.turn = environment.turn === "X" ? "O" : "X"; + environment.winner = checkWinner(environment.board); + }} + }} + + const boardHTML = environment.board.map((cell, index) => ``).join(""); + const message = environment.winner ? `Player ${{environment.winner}} wins!` : `Turn: ${{environment.turn}}`; + + return HtmlResponse(` + + Tic-Tac-Toe +

${{message}}

${{boardHTML}}
+ + `); +}} +``` + +**User's request:** "{natural_query}" + +Return ONLY the complete Javascript function code, without any explanation, comments, or surrounding text/markdown. +""" + payload = json.dumps({ + "contents": [{"parts": [{"text": prompt}]}] + }) + + response = requests.post(api_url, headers=headers, data=payload) + response.raise_for_status() + + response_data = response.json() + + candidates = response_data.get('candidates', []) + if not candidates: + return None, "No candidates found in API response." + + content = candidates[0].get('content', {}) + parts = content.get('parts', []) + if not parts: + return None, "No parts found in API response content." + + generated_script = parts[0].get('text', '').strip() + + # More robustly extract from markdown + if generated_script.startswith("```javascript"): + generated_script = generated_script[12:] + if generated_script.endswith("```"): + generated_script = generated_script[:-3] + + generated_script = generated_script.strip() + + # Remove any leading non-code characters + if not generated_script.startswith('async ('): + async_start = generated_script.find('async (') + if async_start != -1: + generated_script = generated_script[async_start:] + + return generated_script.strip(), None + + except requests.exceptions.RequestException as e: + return None, f"Error communicating with API: {e}" + except (KeyError, IndexError, Exception) as e: + return None, f"Error processing API response: {e}" + +@llm.route("/generate_script", methods=["POST"]) +@login_required +def generate_script(): + try: + natural_query = request.json.get('natural_query') + if not natural_query: + return jsonify({"error": "natural_query is required"}), 400 + + script_content, error = _generate_script_from_natural_language(natural_query) + + if error: + return jsonify({"error": error}), 500 + + return jsonify({"script_content": script_content}) + + except Exception as e: + return jsonify({'error': str(e)}), 500 \ No newline at end of file diff --git a/static/js/mithril/editor.js b/static/js/mithril/editor.js index 1b0bc11..1342646 100644 --- a/static/js/mithril/editor.js +++ b/static/js/mithril/editor.js @@ -74,6 +74,7 @@ const Editor = { this.naturalLanguageQuery = ""; this.generateLoading = false; this.showNaturalLanguageQuery = false; + this.generateUrl = vnode.attrs.generateUrl; }, oncreate() { @@ -245,7 +246,7 @@ const Editor = { try { const resp = await m.request({ method: "POST", - url: "/api/generate_script", // Assuming this is the new endpoint + url: this.generateUrl, body: { natural_query: this.naturalLanguageQuery }, }); diff --git a/templates/dashboard/http_functions/editor.html b/templates/dashboard/http_functions/editor.html index 876621c..b5c2448 100644 --- a/templates/dashboard/http_functions/editor.html +++ b/templates/dashboard/http_functions/editor.html @@ -36,6 +36,7 @@ history_url=url_for('http.history', function_id=function_id)) }} saveUrl: "{{ url_for('http.edit', function_id=id) if id else url_for('http.new') }}", deleteUrl: "{{ url_for('http.delete', function_id=id) if id else '' }}", cancelUrl: "{{ url_for('http.overview') }}", + generateUrl: "{{ url_for('llm.generate_script') }}", showDeleteButton: true }) }) diff --git a/templates/dashboard/http_functions/new.html b/templates/dashboard/http_functions/new.html index 5776e35..17f8df0 100644 --- a/templates/dashboard/http_functions/new.html +++ b/templates/dashboard/http_functions/new.html @@ -34,6 +34,7 @@ title='New HTTP Function') executeUrl: "{{ url_for('execute_code', playground='true') }}", saveUrl: "{{ url_for('http.new') }}", showDeleteButton: false, + generateUrl: "{{ url_for('llm.generate_script') }}", dashboardUrl: "{{ url_for('http.overview') }}" }) }) diff --git a/templates/dashboard/timer_functions/edit.html b/templates/dashboard/timer_functions/edit.html index a9721a9..57c3b93 100644 --- a/templates/dashboard/timer_functions/edit.html +++ b/templates/dashboard/timer_functions/edit.html @@ -38,6 +38,7 @@ history_url=url_for('timer.history', function_id=function_id)) }} isTimer: true, showTimerSettings: true, cancelUrl: "{{ url_for('timer.overview') }}", + generateUrl: "{{ url_for('llm.generate_script') }}", showPublicToggle: false, showLogRequestToggle: false, showLogResponseToggle: false diff --git a/templates/dashboard/timer_functions/new.html b/templates/dashboard/timer_functions/new.html index 537b903..5af28f0 100644 --- a/templates/dashboard/timer_functions/new.html +++ b/templates/dashboard/timer_functions/new.html @@ -31,6 +31,7 @@ title='New Timer Function') saveUrl: "{{ url_for('timer.new') }}", showDeleteButton: false, dashboardUrl: "{{ url_for('timer.overview') }}", + generateUrl: "{{ url_for('llm.generate_script') }}", isTimer: true, showTimerSettings: true, triggerType: 'interval', diff --git a/templates/home.html b/templates/home.html index f34c2e0..d35af96 100644 --- a/templates/home.html +++ b/templates/home.html @@ -131,6 +131,7 @@ showHeader: false, showFunctionSettings: false, executeUrl: "{{ url_for('execute_code', playground='true') }}", + generateUrl: "{{ url_for('llm.generate_script') }}" }) })