Move LLM logic to seperate route

This commit is contained in:
Peter Stockings
2025-07-22 20:25:42 +10:00
parent 2c089fcaf7
commit d65495541f
8 changed files with 164 additions and 143 deletions

150
app.py
View File

@@ -14,6 +14,7 @@ from routes.timer import timer
from routes.test import test from routes.test import test
from routes.home import home from routes.home import home
from routes.http import http from routes.http import http
from routes.llm import llm
from flask_apscheduler import APScheduler from flask_apscheduler import APScheduler
import asyncio import asyncio
import aiohttp import aiohttp
@@ -39,6 +40,7 @@ app.register_blueprint(timer, url_prefix='/timer')
app.register_blueprint(test, url_prefix='/test') app.register_blueprint(test, url_prefix='/test')
app.register_blueprint(home, url_prefix='/home') app.register_blueprint(home, url_prefix='/home')
app.register_blueprint(http, url_prefix='/http') app.register_blueprint(http, url_prefix='/http')
app.register_blueprint(llm, url_prefix='/llm')
class User(UserMixin): class User(UserMixin):
def __init__(self, id, username, password_hash, created_at): def __init__(self, id, username, password_hash, created_at):
@@ -120,149 +122,13 @@ def home():
def documentation(): def documentation():
return render_template("documentation.html") return render_template("documentation.html")
def _generate_script_from_natural_language(natural_query): @ app.route("/dashboard", methods=["GET"])
"""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) => `<button hx-post="/f/${{req.path.split('/')[2]}}/${{req.path.split('/')[3]}}" hx-target="body" hx-swap="innerHTML" name="move" value="${{index}}">${{cell || '&nbsp;'}}</button>`).join("");
const message = environment.winner ? `Player ${{environment.winner}} wins!` : `Turn: ${{environment.turn}}`;
return HtmlResponse(`
<html>
<head><title>Tic-Tac-Toe</title><script src="https://unpkg.com/htmx.org@1.9.9"></script></head>
<body><h1>${{message}}</h1><div>${{boardHTML}}</div></body>
</html>
`);
}}
```
**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"])
@login_required @login_required
def generate_script(): def dashboard():
try: user_id = current_user.id
natural_query = request.json.get('natural_query') http_functions = db.get_http_functions_for_user(user_id)
if not natural_query: http_functions = create_http_functions_view_model(http_functions)
return jsonify({"error": "natural_query is required"}), 400 return render_template("dashboard/http_functions/overview.html", http_functions=http_functions)
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
@app.route('/execute', methods=['POST']) @app.route('/execute', methods=['POST'])
def execute_code(): def execute_code():

149
routes/llm.py Normal file
View File

@@ -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) => `<button hx-post="/f/${{req.path.split('/')[2]}}/${{req.path.split('/')[3]}}" hx-target="body" hx-swap="innerHTML" name="move" value="${{index}}">${{cell || '&nbsp;'}}</button>`).join("");
const message = environment.winner ? `Player ${{environment.winner}} wins!` : `Turn: ${{environment.turn}}`;
return HtmlResponse(`
<html>
<head><title>Tic-Tac-Toe</title><script src="https://unpkg.com/htmx.org@1.9.9"></script></head>
<body><h1>${{message}}</h1><div>${{boardHTML}}</div></body>
</html>
`);
}}
```
**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

View File

@@ -74,6 +74,7 @@ const Editor = {
this.naturalLanguageQuery = ""; this.naturalLanguageQuery = "";
this.generateLoading = false; this.generateLoading = false;
this.showNaturalLanguageQuery = false; this.showNaturalLanguageQuery = false;
this.generateUrl = vnode.attrs.generateUrl;
}, },
oncreate() { oncreate() {
@@ -245,7 +246,7 @@ const Editor = {
try { try {
const resp = await m.request({ const resp = await m.request({
method: "POST", method: "POST",
url: "/api/generate_script", // Assuming this is the new endpoint url: this.generateUrl,
body: { natural_query: this.naturalLanguageQuery }, body: { natural_query: this.naturalLanguageQuery },
}); });

View File

@@ -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') }}", 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 '' }}", deleteUrl: "{{ url_for('http.delete', function_id=id) if id else '' }}",
cancelUrl: "{{ url_for('http.overview') }}", cancelUrl: "{{ url_for('http.overview') }}",
generateUrl: "{{ url_for('llm.generate_script') }}",
showDeleteButton: true showDeleteButton: true
}) })
}) })

View File

@@ -34,6 +34,7 @@ title='New HTTP Function')
executeUrl: "{{ url_for('execute_code', playground='true') }}", executeUrl: "{{ url_for('execute_code', playground='true') }}",
saveUrl: "{{ url_for('http.new') }}", saveUrl: "{{ url_for('http.new') }}",
showDeleteButton: false, showDeleteButton: false,
generateUrl: "{{ url_for('llm.generate_script') }}",
dashboardUrl: "{{ url_for('http.overview') }}" dashboardUrl: "{{ url_for('http.overview') }}"
}) })
}) })

View File

@@ -38,6 +38,7 @@ history_url=url_for('timer.history', function_id=function_id)) }}
isTimer: true, isTimer: true,
showTimerSettings: true, showTimerSettings: true,
cancelUrl: "{{ url_for('timer.overview') }}", cancelUrl: "{{ url_for('timer.overview') }}",
generateUrl: "{{ url_for('llm.generate_script') }}",
showPublicToggle: false, showPublicToggle: false,
showLogRequestToggle: false, showLogRequestToggle: false,
showLogResponseToggle: false showLogResponseToggle: false

View File

@@ -31,6 +31,7 @@ title='New Timer Function')
saveUrl: "{{ url_for('timer.new') }}", saveUrl: "{{ url_for('timer.new') }}",
showDeleteButton: false, showDeleteButton: false,
dashboardUrl: "{{ url_for('timer.overview') }}", dashboardUrl: "{{ url_for('timer.overview') }}",
generateUrl: "{{ url_for('llm.generate_script') }}",
isTimer: true, isTimer: true,
showTimerSettings: true, showTimerSettings: true,
triggerType: 'interval', triggerType: 'interval',

View File

@@ -131,6 +131,7 @@
showHeader: false, showHeader: false,
showFunctionSettings: false, showFunctionSettings: false,
executeUrl: "{{ url_for('execute_code', playground='true') }}", executeUrl: "{{ url_for('execute_code', playground='true') }}",
generateUrl: "{{ url_for('llm.generate_script') }}"
}) })
}) })
</script> </script>