├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.md ├── README.md ├── api ├── __init__.py ├── completions.py ├── functions.py ├── runs.py └── runtimes.py ├── config.yaml.example ├── docker-compose.setup.yml ├── docker-compose.yml ├── main.py ├── requirements.txt ├── runtimes ├── cpp.yaml ├── kali.yaml ├── node.yaml ├── python.yaml └── ruby.yaml ├── seed.sql ├── services ├── __init__.py ├── config.py ├── create_dockerfile.py ├── database.py ├── helpers.py ├── llm │ ├── __init__.py │ └── openai.py ├── redis_hostname.py ├── run_code.py └── run_containerized_code.py ├── setup.py └── static ├── css └── style.css ├── function-details.html ├── index.html └── js ├── function-details.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .DS_Store 3 | functions.db 4 | dockerfiles/ 5 | tmp/* 6 | config.yaml 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to LambdaPi 2 | 3 | Thank you for your interest in contributing to LambdaPi! We appreciate your effort and look forward to collaborating with you. To ensure a smooth and effective contribution process, please read through this guide before submitting any pull requests or issues. 4 | 5 | ## Reporting Issues 6 | 7 | If you encounter a bug or want to request a new feature, please follow these steps: 8 | 9 | 1. Check if there's an existing issue in our [Issue Tracker](https://github.com/jb41/lambdapi/issues) that is related to your issue. 10 | 2. If there isn't an existing issue, create a new one. Please provide a clear and concise description of the problem or feature request. Include relevant information such as error messages, screenshots, or reproduction steps. 11 | 12 | ## Contributing Code 13 | 14 | If you want to contribute code to fix a bug, implement a feature, or improve the project, please follow these steps: 15 | 16 | 1. [Fork the repository](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) and create a new branch with a descriptive name, such as `fix-bug-123` or `add-new-feature`. 17 | 2. Clone your fork to your local machine and switch to your new branch. 18 | 3. Make your changes in the new branch. Please follow the project's coding style and conventions. 19 | 4. Add and commit your changes with a descriptive commit message. 20 | 5. Push your changes to your fork on GitHub. 21 | 6. Create a [pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request) against the main branch of the original repository. 22 | 23 | ## Setting Up Your Development Environment 24 | 25 | To set up your local development environment, follow these steps: 26 | 27 | 1. Install the required dependencies for the project. 28 | 2. Clone the repository and create a new branch for your changes. 29 | 30 | For more detailed instructions, please refer to the [README.md](README.md) file. 31 | 32 | ## Additional Resources 33 | 34 | - [GitHub Documentation](https://docs.github.com) 35 | - [Git Documentation](https://git-scm.com/doc) 36 | 37 | Once again, thank you for your interest in contributing to LambdaPi. We look forward to working together and making this project even better! 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Python base image 2 | FROM python:3.11 3 | 4 | # Set the working directory 5 | WORKDIR /app 6 | 7 | # Copy the requirements.txt file into the container 8 | COPY requirements.txt . 9 | 10 | # Install the dependencies from the requirements.txt file 11 | RUN pip install --no-cache-dir -r requirements.txt 12 | 13 | # Install Docker 14 | RUN apt-get update && \ 15 | apt-get install -y --no-install-recommends apt-transport-https ca-certificates curl gnupg lsb-release && \ 16 | curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \ 17 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list && \ 18 | apt-get update && \ 19 | apt-get install -y --no-install-recommends docker-ce docker-ce-cli containerd.io && \ 20 | apt-get clean && \ 21 | rm -rf /var/lib/apt/lists/* 22 | 23 | # Copy the rest of your application code into the container 24 | COPY . . 25 | 26 | # Create a volume for the /tmp/lambdapi/files directory 27 | VOLUME ["/tmp/lambdapi/files"] 28 | 29 | # Expose the FastAPI application's port (8000) to the outside world 30 | EXPOSE 8000 31 | 32 | # Start the FastAPI application using single Uvicorn server 33 | # CMD ["python", "main.py"] 34 | 35 | # TO RUN MORE WORKERS IN PRODUCTION USE THIS 36 | CMD ["sh", "-c", "gunicorn -w $(nproc) --timeout 360 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 main:app"] 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Michal Oblak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LambdaPi: GPT-Driven Serverless Code Plugin 2 | 3 | LambdaPi is a serverless runtime environment designed for code generated by Large Language Models (LLMs), complete with a user interface for writing, testing, and deploying the code. 4 | 5 | As LLMs continue to revolutionize software development, LambdaPi aims to provide a robust open-source ecosystem for building, testing, and deploying APIs (and eventually other types of software). By harnessing the power of GPT models, LambdaPi automatically containerizes and deploys code in a serverless fashion, with containers created and destroyed with each request. 6 | 7 | LambdaPi features a shared Redis database and file storage for communication, storage, and input/output needs. Functions can be called using JSON or query parameters and return a variety of formats, including JSON, plain text, and files (png, csv). 8 | 9 | LambdaPi supports a wide range of runtimes, including Python, Node, Bash, Ruby, Shell, C++, and others of your choice (just create Dockerconfig). 10 | Functions are designed as straightforward programs, with parameters passed as command line arguments. Output is returned to the user via stdout, ensuring a streamlined process: 11 | ``` 12 | command line parameters as input -> 13 | -> program execution -> 14 | -> printed result. 15 | ``` 16 | This simplicity caters to both humans and LLMs, making it easier for anyone to create and deploy code. 17 | With support for multiple runtimes and an emphasis on simplicity, LambdaPi enables developers and LLMs alike to create powerful, efficient code with minimal barriers to entry. 18 | 19 | View the system prompt configuration in [config.yaml.example](config.yaml.example) 20 | 21 | # 🎬 Demo 22 | 23 | https://user-images.githubusercontent.com/3681934/231136633-9a68ab81-a4db-43b2-8b5d-8c04b1e38915.mp4 24 | 25 | 26 | # ✨ Features 27 | 28 | - 📚 Runtimes: 29 | - Python 30 | - Node.js 31 | - C++ 32 | - Kali Linux 33 | - Ruby 34 | - 📁 File storage 35 | - 🗃️ Redis storage 36 | - 📤 JSON, plain text, or file output support 37 | - 🖋️ Easy scripting and command line execution 38 | 39 | # 📋 Requirements 40 | 41 | ## With Docker (recommended setup) 42 | Here, main app is run inside Docker, and all functions are run as a Docker-in-Docker (DinD) 43 | - Docker 44 | 45 | ## Without Docker 46 | - Python 3.x 47 | - Redis 48 | 49 | # 🛠️ Setup 50 | 51 | ## Docker (recommended) 52 | Clone the repository: 53 | ```sh 54 | git clone https://github.com/jb41/lambdapi 55 | ``` 56 | Navigate to the project directory: 57 | ```sh 58 | cd lambdapi 59 | ``` 60 | Copy the example configuration: 61 | ``` 62 | cp config.yaml.example config.yaml 63 | ``` 64 | Add your GPT API key to config.yaml (obtain it from [https://platform.openai.com/account/api-keys](https://platform.openai.com/account/api-keys)) 65 | 66 | _Building Kali Linux images can be time-consuming due to the installation of the kali-linux-headless package. To speed up the setup process, you can remove the package by executing the command `rm -rf runtimes/kali.yaml`._ 67 | 68 | Execute the setup script, which will setup database, configure directories, generate Dockerfiles, and build the corresponding Docker images. 69 | ```sh 70 | docker-compose -f docker-compose.setup.yml up 71 | ``` 72 | 73 | ## Without Docker 74 | Install dependencies: 75 | ```sh 76 | apt-get install redis docker-ce docker-ce-cli containerd.io 77 | ``` 78 | Install required libraries: 79 | ```sh 80 | pip3 install -r requirements.txt 81 | ``` 82 | 83 | Run 84 | ```sh 85 | python3 setup.py 86 | ``` 87 | 88 | # 🎮 Usage 89 | 90 | ## Docker (recommended) 91 | Run 92 | ```sh 93 | docker-compose up --build 94 | ``` 95 | Visit [http://localhost:8000/index.html](http://localhost:8000/index.html) to view the dashboard. 96 | 97 | ## Without Docker 98 | Run 99 | ```sh 100 | python3 main.py 101 | ``` 102 | 103 | Visit [http://localhost:8000/index.html](http://localhost:8000/index.html) to view the dashboard. 104 | 105 | # 📄 Documentation 106 | 107 | ## File Storage 108 | 109 | All files saved in the current directory (/app) are returned in the response. These files, along with the container, are deleted after the request is completed. If you want to preserve a file for future use or access existing files, store or read them from the /data directory. 110 | 111 | ## Redis 112 | 113 | The Redis URL is stored in the REDIS_HOST environment variable. Redis is shared among all containers and operates on the default port. 114 | 115 | ## Parameters 116 | 117 | You can pass parameters as query parameters or a JSON body. However, they are forwarded to the script as command-line arguments. For instance, a JSON body like this: 118 | 119 | ```json 120 | { 121 | "a": 1337, 122 | "b": 42, 123 | "foo": "bar" 124 | } 125 | ``` 126 | will be passed to a Python script as: 127 | 128 | ```sh 129 | python3 "1337" "42" "bar" 130 | ``` 131 | In the UI (`/functions-details.html`), you can provide parameters in the input text area. Separate each parameter with a newline. 132 | 133 | Please note that there are limitations in preserving parameter names and handling more complex data structures, such as strings with spaces, hashes, and others. Contributions to address these issues are welcome. 134 | 135 | # 🤖 GPT-3.5 vs GPT-4 136 | 137 | LambdaPi is compatible with both GPT-3.5 and GPT-4 models, but performs significantly better with GPT-4, often requiring little or no manual input. GPT-3.5 can be used with some manual adjustments or with simpler scripts. 138 | 139 | 140 | # 💡 Examples 141 | | Runtime | Description | URL | 142 | |---------|-------------|-----| 143 | | Ruby | Open a website, do a search, return results | http://localhost:8000/function-details.html?id=1 | 144 | ||| 145 | | Python | Generate an image with a text from the user | http://localhost:8000/function-details.html?id=2 | 146 | | Python | Save input data to redis | http://localhost:8000/function-details.html?id=3 | 147 | | Python | Read from redis | http://localhost:8000/function-details.html?id=4 | 148 | | Python | Generate graph from numbers provided in CSV | http://localhost:8000/function-details.html?id=5 | 149 | | Python | download website and return links | http://localhost:8000/function-details.html?id=6 | 150 | ||| 151 | | Node | Add caption to an image | http://localhost:8000/function-details.html?id=7 | 152 | | Node | Request a joke from jokes API and return | http://localhost:8000/function-details.html?id=8 | 153 | ||| 154 | | Kali Linux | Scan ports with nmap | http://localhost:8000/function-details.html?id=9 | 155 | | Kali Linux | Search for vulnerabilities with nikto _(rather slow, ~300 seconds)_ | http://localhost:8000/function-details.html?id=10 | 156 | | Kali Linux | Bruteforce MD5 password with john | http://localhost:8000/function-details.html?id=11 | 157 | ||| 158 | | C++ | Read data from redis | http://localhost:8000/function-details.html?id=12 | 159 | | C++ | Calculate first 100 fibbonaci numbers with boost | http://localhost:8000/function-details.html?id=13 | 160 | 161 | # 🚀 Running in production 162 | To optimize performance in a production environment, it's important to run multiple workers. When running LambdaPi using Docker, the application will automatically utilize a number of workers equal to the number of CPU cores. You can configure this setting for your specific Docker instance to ensure efficient operation. 163 | 164 | # 🤝 Contributing 165 | 166 | We warmly welcome your contributions and appreciate your support in enhancing the project. There are several areas where you can help improve LambdaPi: 167 | 168 | - **Frontend expansion:** Generate HTML files with frontend JavaScript and set up a /public directory for hosting files (similar to the /data implementation). 169 | - **Chaining:** Develop unit tests with LLM, execute code, and verify output before presenting to users. 170 | - **Dockerfile optimization:** Reconsider the use of Python for generating Dockerfiles, and explore potential improvements in managing Docker containers (including handling, setup, and execution). 171 | - **Additional runtimes:** Incorporate more programming languages and technologies, such as Go or Rust, to further broaden the range of supported runtimes. 172 | - **Alternative Linux distributions:** Evaluate other Linux distributions that may be beneficial for the project (currently using Kali Linux - beacause of it's preinstalled tooling). 173 | - **UX/UI enhancements:** Improve the user interface by rethinking the layout of elements like the code editor, prompt, LLM text response, parameters, and results. 174 | - **User management:** Consider implementing user registration and authentication features. 175 | - **HTTP method restrictions:** Limit access to specific HTTP methods, instead of allowing all methods (GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD). 176 | - **HTTP Basic Auth:** Implement endpoint protection using HTTP Basic Authentication. 177 | - **Cronjobs:** Enable the execution of scripts using cron jobs. 178 | - **Sqlite3 integration:** Add support for SQLite3 databases, providing an alternative to the existing file system for those who require a relational database. 179 | - **Chaining when errors**: Run /completions with with code and error in order to fix it 180 | - **Fix formatting for non-JSON reponses**: in `/functions-details.html`, non JSON responses with `\r\n` could be formatted better. 181 | -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jb41/lambdapi/3b8e06450cd210b8a80c96b687b62ef3c95b81fe/api/__init__.py -------------------------------------------------------------------------------- /api/completions.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Body 2 | from fastapi.responses import JSONResponse 3 | from typing import Dict, List 4 | 5 | from services.llm.openai import OpenAi 6 | from services.helpers import extract_code_and_text 7 | 8 | 9 | 10 | router = APIRouter() 11 | 12 | 13 | @router.post("") 14 | async def get_completions(messages: List[Dict] = Body(...), language: str = Body(...)): 15 | message = OpenAi.chat_completion(messages, language) 16 | splitted_response_data = extract_code_and_text(message['content']) 17 | response_data = { **splitted_response_data, 'message': message } 18 | 19 | return JSONResponse(content=response_data, status_code=200) 20 | -------------------------------------------------------------------------------- /api/functions.py: -------------------------------------------------------------------------------- 1 | import aiosqlite, \ 2 | json 3 | 4 | from fastapi import APIRouter, HTTPException, Request, Response 5 | from fastapi.responses import JSONResponse 6 | 7 | from services.helpers import generate_unique_slug, \ 8 | endpoint_url 9 | from services.database import database_name as db_name 10 | 11 | 12 | 13 | router = APIRouter() 14 | 15 | 16 | @router.get("") 17 | async def get_functions(): 18 | async with aiosqlite.connect(db_name) as db: 19 | cursor = await db.execute("SELECT * FROM functions ORDER BY name ASC, created_at DESC") 20 | rows = await cursor.fetchall() 21 | return [ 22 | { 23 | "id": r[0], "name": r[1], "runtime": r[3], "created_at": r[5], "endpoint_url": endpoint_url(r[2]) 24 | } for r in rows 25 | ] 26 | 27 | 28 | @router.get("/{function_id}") 29 | async def get_function(function_id: int): 30 | async with aiosqlite.connect(db_name) as db: 31 | cursor = await db.execute("SELECT * FROM functions WHERE id=?", (function_id,)) 32 | row = await cursor.fetchone() 33 | 34 | if row: 35 | return { 36 | "id": row[0], "name": row[1], "runtime": row[3], "code": row[4], "endpoint_url": endpoint_url(row[2]) 37 | } 38 | else: 39 | raise HTTPException(status_code=404, detail="Function not found") 40 | 41 | 42 | @router.post("", status_code=201) 43 | async def create_function(): 44 | slug = await generate_unique_slug() 45 | if not slug: 46 | raise HTTPException(status_code=500, detail="Failed to generate a unique slug") 47 | 48 | async with aiosqlite.connect(db_name) as db: 49 | cursor = await db.execute("INSERT INTO functions (slug, name, created_at) VALUES (?, ?, datetime('now'))", (slug, "Unnamed Function")) 50 | await db.commit() 51 | 52 | 53 | 54 | return JSONResponse(content={ "id": cursor.lastrowid, "slug": slug }, status_code=201) 55 | 56 | 57 | @router.put("/{function_id}") 58 | async def update_function(function_id: int, request: Request): 59 | try: 60 | function_data = await request.json() 61 | except json.JSONDecodeError: 62 | raise HTTPException(status_code=400, detail="Invalid JSON payload") 63 | 64 | async with aiosqlite.connect(db_name) as db: 65 | set_clauses = [] 66 | values = [] 67 | 68 | if 'name' in function_data and function_data['name'] is not None: 69 | set_clauses.append("name=?") 70 | values.append(function_data['name']) 71 | if 'runtime' in function_data and function_data['runtime'] is not None: 72 | set_clauses.append("runtime=?") 73 | values.append(function_data['runtime']) 74 | if 'code' in function_data and function_data['code'] is not None: 75 | set_clauses.append("code=?") 76 | values.append(function_data['code']) 77 | 78 | if not set_clauses: 79 | raise HTTPException(status_code=400, detail="No fields to update") 80 | 81 | values.append(function_id) 82 | await db.execute("UPDATE functions SET " + ', '.join(set_clauses) + " WHERE id=?", tuple(values)) 83 | await db.commit() 84 | 85 | return await get_function(function_id) 86 | 87 | 88 | @router.delete("/{function_id}") 89 | async def delete_function(function_id: int): 90 | async with aiosqlite.connect(db_name) as db: 91 | cursor = await db.execute("SELECT COUNT(*) FROM functions WHERE id=?", (function_id,)) 92 | count = await cursor.fetchone() 93 | 94 | if count[0] == 0: 95 | raise HTTPException(status_code=404, detail=f"Function with ID {function_id} not found") 96 | 97 | await db.execute("DELETE FROM functions WHERE id=?", (function_id,)) 98 | await db.commit() 99 | 100 | return Response(status_code=204) 101 | -------------------------------------------------------------------------------- /api/runs.py: -------------------------------------------------------------------------------- 1 | import aiosqlite 2 | from typing import Dict, \ 3 | Optional 4 | from fastapi import APIRouter, \ 5 | Body, \ 6 | HTTPException, \ 7 | Request 8 | from fastapi.responses import JSONResponse, \ 9 | StreamingResponse 10 | 11 | from services.database import database_name as db_name 12 | from services.helpers import parse_output 13 | from services.run_code import RunCode 14 | 15 | 16 | 17 | router = APIRouter() 18 | 19 | 20 | 21 | @router.api_route("/{slug}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]) 22 | async def run(slug: str, request: Request, body_params: Optional[Dict[str, str]] = Body(None)): 23 | try: 24 | async with aiosqlite.connect(db_name) as db: 25 | cursor = await db.execute("SELECT runtime, code FROM functions WHERE slug=?", (slug,)) 26 | row = await cursor.fetchone() 27 | 28 | if row: 29 | query_params = dict(request.query_params) 30 | body_params = body_params if body_params else {} 31 | params = { **query_params, **body_params } 32 | 33 | runtime_name, code = row[0], row[1] 34 | stdout, stderr, output_file = RunCode(slug, runtime_name, code).run(params.values()) 35 | 36 | if stderr: 37 | return JSONResponse(content={ "error": stderr.strip() }, status_code=500) 38 | else: 39 | if output_file: 40 | return StreamingResponse(output_file["content"], media_type=output_file["mime_type"]) 41 | else: 42 | return JSONResponse(content=parse_output(stdout.strip()), status_code=200) 43 | 44 | else: 45 | raise HTTPException(status_code=404, detail="Function not found") 46 | 47 | except Exception as excep: 48 | import sys, traceback 49 | _, _, tb = sys.exc_info() # Get the traceback information 50 | tb_info = traceback.extract_tb(tb) 51 | filename, line, func, _ = tb_info[-1] # Extract the file name, line number, and function name from the traceback 52 | print(f"An exception occurred in {filename}, line {line}, in {func}: {excep}") 53 | 54 | return JSONResponse(content={ "error": str(excep) }, status_code=500) 55 | 56 | 57 | -------------------------------------------------------------------------------- /api/runtimes.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from fastapi.responses import JSONResponse 3 | 4 | from services.config import runtimes_config 5 | 6 | 7 | 8 | router = APIRouter() 9 | 10 | 11 | @router.get("/available") 12 | async def get_available_runtimes(): 13 | available_runtimes = [] 14 | for k, v in runtimes_config.items(): 15 | available_runtimes.append({ 16 | 'name': v['name'], 17 | 'runtime': k, 18 | 'monaco_editor_id': v['monaco_editor_id'] 19 | }) 20 | available_runtimes = sorted(available_runtimes, key=lambda x: x['name']) 21 | 22 | return JSONResponse(content=available_runtimes, status_code=200) 23 | -------------------------------------------------------------------------------- /config.yaml.example: -------------------------------------------------------------------------------- 1 | hostname: localhost:8000 2 | storage_mount_point: "/tmp/lambdapi/files" 3 | llm: 4 | openai: 5 | apikey: "YOUR API KEY" 6 | system_prompt: | 7 | As a proficient {language} developer, you are tasked with providing code snippets based on given function or code descriptions. Your response should include the requested code along with clear and concise code comments to explain the functionality. Moreover, the code should print the output, preferably in JSON format. 8 | 9 | User input data will be passed as command line arguments. 10 | 11 | You have access to a Redis database, with the Redis hostname provided through the REDIS_HOST environment variable. 12 | 13 | You can also utilize the filesystem to save and read files. All files should be saved and read from the /data directory. 14 | model_params: 15 | model: "gpt-3.5-turbo" 16 | temperature: 0.4 17 | top_p: 1 18 | # redis_host: redis.on-some-server.com 19 | -------------------------------------------------------------------------------- /docker-compose.setup.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | lambdapi: 5 | build: . 6 | command: python setup.py 7 | volumes: 8 | - .:/app 9 | - /var/run/docker.sock:/var/run/docker.sock 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | lambdapi: 5 | build: . 6 | volumes: 7 | - .:/app 8 | - ./functions.db:/app/functions.db 9 | - /var/run/docker.sock:/var/run/docker.sock 10 | - /tmp/lambdapi/files:/tmp/lambdapi/files 11 | ports: 12 | - "8000:8000" 13 | depends_on: 14 | - redis 15 | 16 | redis: 17 | image: "redis:latest" 18 | container_name: redis 19 | expose: 20 | - 6379 21 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.staticfiles import StaticFiles 3 | 4 | import uvicorn 5 | 6 | from api.runs import router as runs_router 7 | from api.functions import router as functions_router 8 | from api.runtimes import router as runtimes_router 9 | from api.completions import router as completions_router 10 | 11 | 12 | 13 | app = FastAPI() 14 | app.include_router(runs_router, prefix="/run") 15 | app.include_router(functions_router, prefix="/api/functions") 16 | app.include_router(runtimes_router, prefix="/api/runtimes") 17 | app.include_router(completions_router, prefix="/api/completions") 18 | app.mount("/", StaticFiles(directory="static"), name="static") 19 | 20 | 21 | if __name__ == "__main__": 22 | uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiosqlite==0.18.0 2 | asyncio==3.4.3 3 | docker==5.0.3 4 | fastapi==0.88.0 5 | gunicorn==20.1.0 6 | openai==0.27.2 7 | pyyaml==5.4.1 8 | uvicorn==0.20.0 9 | -------------------------------------------------------------------------------- /runtimes/cpp.yaml: -------------------------------------------------------------------------------- 1 | name: "C++" 2 | maximum_number_of_params: 16 3 | docker: 4 | repository_image: "ubuntu:22.04" 5 | run_cmd: "export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y g++ git cmake libboost-all-dev libarmadillo-dev libopencv-dev libcurl4-openssl-dev libpoco-dev libhiredis-dev libprotobuf-dev protobuf-compiler && git clone https://github.com/nlohmann/json.git && cd json && mkdir build && cd build && cmake .. && make && make install && cd ../.. && rm -rf json && apt-get clean" 6 | image_name: "cpp" 7 | monaco_editor_id: cpp 8 | run: 9 | compile: "g++ -x c++ /app/main -o /app/main.out -lboost_system -lboost_filesystem -larmadillo -lopencv_core -lopencv_highgui -lopencv_imgproc -lcurl -lPocoFoundation -lPocoNet -lhiredis -lprotobuf" 10 | execute: "/app/main.out" 11 | -------------------------------------------------------------------------------- /runtimes/kali.yaml: -------------------------------------------------------------------------------- 1 | name: "Kali Linux" 2 | maximum_number_of_params: 16 3 | docker: 4 | repository_image: "kalilinux/kali-rolling" 5 | run_cmd: | 6 | export DEBIAN_FRONTEND=noninteractive \ 7 | && apt-get update && apt-get install -y kali-linux-headless \ 8 | && apt-get clean \ 9 | && rm -rf /var/lib/apt/lists/* 10 | image_name: "kali-linux" 11 | monaco_editor_id: shell 12 | run: 13 | execute: "bash /app/main" 14 | -------------------------------------------------------------------------------- /runtimes/node.yaml: -------------------------------------------------------------------------------- 1 | name: "Node.js 16" 2 | maximum_number_of_params: 16 3 | docker: 4 | repository_image: "node:16" 5 | run_cmd: "npm install --save axios node-fetch request got lodash moment csv-parser sharp jimp gm aws-sdk redis mongoose knex" 6 | image_name: "nodejs-16" 7 | monaco_editor_id: javascript 8 | run: 9 | execute: "node /app/main" 10 | -------------------------------------------------------------------------------- /runtimes/python.yaml: -------------------------------------------------------------------------------- 1 | name: "Python 3.10" 2 | maximum_number_of_params: 16 3 | docker: 4 | repository_image: "python:3.10" 5 | run_cmd: "pip install --no-cache-dir pandas numpy matplotlib scikit-learn scipy statsmodels redis requests Pillow beautifulsoup4 lxml boto3 google-cloud-storage seaborn gensim geopy arrow" 6 | image_name: "python-3-10" 7 | monaco_editor_id: python 8 | run: 9 | execute: "python /app/main" 10 | -------------------------------------------------------------------------------- /runtimes/ruby.yaml: -------------------------------------------------------------------------------- 1 | name: "Ruby 3.2" 2 | maximum_number_of_params: 16 3 | docker: 4 | repository_image: "ruby:3.2" 5 | run_cmd: "gem install bundler && echo \"source 'https://rubygems.org'\" >> Gemfile && echo \"gem 'httparty'\" >> Gemfile && echo \"gem 'nokogiri'\" >> Gemfile && echo \"gem 'rmagick'\" >> Gemfile && echo \"gem 'redis'\" >> Gemfile && echo \"gem 'aws-sdk-s3'\" >> Gemfile && echo \"gem 'csv'\" >> Gemfile && echo \"gem 'mechanize'\" >> Gemfile && bundle install" 6 | image_name: "ruby-3-2" 7 | monaco_editor_id: ruby 8 | run: 9 | execute: "ruby /app/main" 10 | -------------------------------------------------------------------------------- /seed.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO functions (slug, runtime, name, code, created_at) VALUES ('lmob8usbx9otu597', 'ruby', 'Open a website, do a search', 2 | 'require ''mechanize'' 3 | require ''json'' 4 | 5 | # Function to get top 10 search results from DuckDuckGo 6 | def get_search_results(query) 7 | # Initialize Mechanize agent 8 | agent = Mechanize.new 9 | 10 | # Send a GET request to DuckDuckGo with the query 11 | page = agent.get("https://duckduckgo.com/html/?q=#{query}") 12 | 13 | # Find the search results 14 | results = page.search(''.result'') 15 | 16 | # Extract the top 10 results'' title and URL 17 | top_results = results.take(10).map do |result| 18 | { 19 | title: result.at(''.result__title'').text.strip, 20 | url: result.at(''.result__url'')[''href''] 21 | } 22 | end 23 | 24 | # Return the results as a Ruby hash 25 | top_results 26 | end 27 | 28 | # Get user input from command line 29 | query = ARGV[0] 30 | 31 | # Get the top 10 search results 32 | search_results = get_search_results(query) 33 | 34 | # Print the results as proper JSON 35 | puts JSON.pretty_generate({ "r": search_results}) 36 | ', datetime('now')); 37 | 38 | INSERT INTO functions (slug, runtime, name, code, created_at) VALUES ('h7oggnvtyxfmvps1', 'python', 'Generate an image with a text from the user', 39 | 'from PIL import Image, ImageDraw 40 | import sys 41 | 42 | # create a black background image 43 | img = Image.new(''RGB'', (400, 400), color=''black'') 44 | 45 | # create a draw object 46 | draw = ImageDraw.Draw(img) 47 | 48 | text = sys.argv[1] if len(sys.argv) > 1 else ''Hello world'' 49 | 50 | # get the size of the text 51 | text_size = draw.textsize(text) 52 | 53 | # calculate the position to center the text 54 | x = (400 - text_size[0]) // 2 55 | y = (400 - text_size[1]) // 2 56 | 57 | # draw the text in red color 58 | draw.text((x, y), text, fill=''red'') 59 | 60 | # save the image as output.png 61 | img.save(''output.png'') 62 | ', datetime('now')); 63 | 64 | INSERT INTO functions (slug, runtime, name, code, created_at) VALUES ('v77aj50hy7922kb9', 'python', 'Save input data to redis', 65 | 'import json 66 | import os 67 | import sys 68 | 69 | import redis 70 | 71 | 72 | # Connect to Redis 73 | redis_host = os.environ.get(''REDIS_HOST'', ''localhost'') 74 | redis_client = redis.Redis(host=redis_host) 75 | 76 | 77 | def save_param(param_value): 78 | redis_client.rpush(''params'', param_value) 79 | 80 | 81 | def get_params(): 82 | params = redis_client.lrange(''params'', 0, -1) 83 | params = [param.decode(''utf-8'') for param in params] 84 | return { ''params'': params } 85 | 86 | 87 | if __name__ == ''__main__'': 88 | # Get user input data 89 | param_value = sys.argv[1] 90 | 91 | # Save param 92 | save_param(param_value) 93 | 94 | # Get and print all params 95 | params = get_params() 96 | print(json.dumps(params)) 97 | ', datetime('now')); 98 | 99 | INSERT INTO functions (slug, runtime, name, code, created_at) VALUES ('dk8jnu1b9ccf39o5', 'python', 'Read from redis', 100 | 'import json 101 | import os 102 | import sys 103 | 104 | import redis 105 | 106 | 107 | # Connect to Redis 108 | redis_host = os.environ.get(''REDIS_HOST'', ''localhost'') 109 | redis_client = redis.Redis(host=redis_host) 110 | 111 | 112 | def save_param(param_value): 113 | redis_client.rpush(''params'', param_value) 114 | 115 | 116 | def get_params(): 117 | params = redis_client.lrange(''params'', 0, -1) 118 | params = [param.decode(''utf-8'') for param in params] 119 | return { ''params'': params } 120 | 121 | 122 | if __name__ == ''__main__'': 123 | # Get user input data 124 | param_value = sys.argv[1] 125 | 126 | # Save param 127 | save_param(param_value) 128 | 129 | # Get and print all params 130 | params = get_params() 131 | print(json.dumps(params)) 132 | ', datetime('now')); 133 | 134 | INSERT INTO functions (slug, runtime, name, code, created_at) VALUES ('2tocc2qwbu6dfk5u', 'python', 'Generate graph from numbers provided in CSV', 135 | 'import csv 136 | import os 137 | import matplotlib.pyplot as plt 138 | 139 | # Read numbers from a CSV file 140 | def read_numbers_from_csv(file_path): 141 | numbers = [] 142 | with open(file_path, ''r'') as csvfile: 143 | csvreader = csv.reader(csvfile) 144 | for row in csvreader: 145 | for number in row: 146 | try: 147 | numbers.append(float(number)) 148 | except ValueError: 149 | print(f"Skipping non-numeric value: {number}") 150 | return numbers 151 | 152 | # Plot numbers and save the plot to disk 153 | def plot_and_save_numbers(numbers, output_path): 154 | plt.plot(numbers) 155 | plt.xlabel(''Index'') 156 | plt.ylabel(''Number'') 157 | plt.title(''Numbers Plot'') 158 | plt.savefig(output_path) 159 | 160 | # Main function 161 | def main(): 162 | input_file_path = os.path.join(''/data'', ''numbers.csv'') 163 | output_file_path = os.path.join(''numbers_plot.png'') 164 | 165 | numbers = read_numbers_from_csv(input_file_path) 166 | plot_and_save_numbers(numbers, output_file_path) 167 | print(f"Plot saved to {output_file_path}") 168 | 169 | if __name__ == ''__main__'': 170 | main() 171 | ', datetime('now')); 172 | 173 | INSERT INTO functions (slug, runtime, name, code, created_at) VALUES ('gvp0amaq8jz52nkd', 'python', 'Download website and return links', 174 | 'import os 175 | import sys 176 | import json 177 | import requests 178 | from bs4 import BeautifulSoup 179 | 180 | def get_top_stories(): 181 | # Fetch the Hacker News page content 182 | response = requests.get(''https://news.ycombinator.com'') 183 | content = response.content 184 | 185 | # Parse the content using BeautifulSoup 186 | soup = BeautifulSoup(content, ''lxml'') 187 | 188 | # Find the top 10 story titles and store them in a list 189 | titles = [] 190 | stories = soup.find_all(''span'', class_=''titleline'') 191 | for story in stories[:10]: 192 | title = story.find(''a'').text 193 | titles.append(title) 194 | 195 | return titles 196 | 197 | 198 | top_stories_titles = get_top_stories() 199 | print(json.dumps({''r'': top_stories_titles}, indent=2)) 200 | ', datetime('now')); 201 | 202 | INSERT INTO functions (slug, runtime, name, code, created_at) VALUES ('6do6tb92ml7e054o', 'node', 'Add caption to an image', 203 | '/* 204 | USE THOSE AS PARAMS ⬇️ 205 | https://www.asiamediajournal.com/wp-content/uploads/2022/11/Free-Download-Funny-Cat-PFP-300x300.jpg 206 | Some_Text 207 | ⬆️ 208 | */ 209 | 210 | // Import required modules 211 | const axios = require(''axios''); 212 | const Jimp = require(''jimp''); 213 | 214 | // Function to download an image from a URL 215 | async function downloadImage(url) { 216 | const response = await axios.get(url, { responseType: ''arraybuffer'' }); 217 | return Buffer.from(response.data, ''binary''); 218 | } 219 | 220 | // Function to add a caption to an image 221 | async function addCaptionToImage(imageUrl, captionText) { 222 | try { 223 | // Download the image 224 | const imageBuffer = await downloadImage(imageUrl); 225 | 226 | // Read the image using Jimp 227 | const image = await Jimp.read(imageBuffer); 228 | 229 | // Load the default font with 48px size 230 | const font = await Jimp.loadFont(Jimp.FONT_SANS_64_BLACK); 231 | 232 | // Add the caption text to the image 233 | image.print(font, 10, 10, captionText); 234 | 235 | // Save the image as output.png 236 | await image.writeAsync(''output.png''); 237 | 238 | console.log(''Caption added to the image and saved as output.png''); 239 | } catch (error) { 240 | console.log(''Error adding caption to the image:'', error.message); 241 | } 242 | } 243 | 244 | // Get the command line arguments 245 | const args = process.argv.slice(2); 246 | 247 | if (args.length < 2) { 248 | console.log(''Please provide an image URL and caption text as command line arguments.''); 249 | } else { 250 | const imageUrl = args[0]; 251 | const captionText = args[1]; 252 | 253 | // Call the addCaptionToImage function with the provided arguments 254 | addCaptionToImage(imageUrl, captionText); 255 | } 256 | 257 | /* 258 | USE THOSE AS PARAMS ⬇️ 259 | https://www.asiamediajournal.com/wp-content/uploads/2022/11/Free-Download-Funny-Cat-PFP-300x300.jpg 260 | Some_Text 261 | ⬆️ 262 | */ 263 | ', datetime('now')); 264 | 265 | INSERT INTO functions (slug, runtime, name, code, created_at) VALUES ('hhbj4pmr1870mf6a', 'node', 'Request a joke from jokes API and return', 266 | '// Import the required modules 267 | const axios = require(''axios''); 268 | 269 | // Function to fetch a joke from the JokeAPI 270 | async function fetchJoke() { 271 | try { 272 | // Fetch a joke from the JokeAPI using axios 273 | const response = await axios.get(''https://v2.jokeapi.dev/joke/Any?blacklistFlags=nsfw,religious,political,racist,sexist,explicit''); 274 | 275 | // Get the joke data from the response 276 | const jokeData = response.data; 277 | 278 | // Check if the joke is a single-part joke or a two-part joke 279 | if (jokeData.type === ''single'') { 280 | // Print the single-part joke 281 | console.log(jokeData.joke); 282 | } else { 283 | // Print the two-part joke 284 | console.log(`${jokeData.setup}\ 285 | ${jokeData.delivery}`); 286 | } 287 | } catch (error) { 288 | console.error(''Error fetching joke:'', error.message); 289 | } 290 | } 291 | 292 | // Fetch and print a joke 293 | fetchJoke(); 294 | ', datetime('now')); 295 | 296 | INSERT INTO functions (slug, runtime, name, code, created_at) VALUES ('j3vd3ayr3zxnvdy0', 'kali', 'Scan ports with nmap', 297 | 'nmap -sV -p 80,443,22 example.com 298 | ', datetime('now')); 299 | 300 | INSERT INTO functions (slug, runtime, name, code, created_at) VALUES ('zd6ulzzeie95yusa', 'kali', 'Search for vulnerabilities with nikto (rather slow, ~300 seconds)', 301 | 'nikto -h http://example.com -Tuning 1-2 302 | ', datetime('now')); 303 | 304 | INSERT INTO functions (slug, runtime, name, code, created_at) VALUES ('i0doyn6v4jsm9vwu', 'kali', 'Bruteforce MD5 password with john the ripper', 305 | '# Save the hash to a file 306 | hash=''5f4dcc3b5aa765d61d8327deb882cf99'' 307 | echo "$hash" > hash.txt 308 | 309 | # Run John the Ripper on the hash file using the default wordlist and suppress output 310 | john --format=raw-md5 hash.txt > /dev/null 2>&1 311 | 312 | # Get the cracked password from John the Ripper''s output and suppress output 313 | cracked_password=$(john --show --format=raw-md5 hash.txt 2>/dev/null | grep -oP ''(?<=:).*'') 314 | 315 | # Print the cracked password 316 | echo "Cracked password: ''$cracked_password''" 317 | 318 | # Cleanup 319 | rm hash.txt 320 | rm hash.txt.john.pot 2>/dev/null 321 | ', datetime('now')); 322 | 323 | INSERT INTO functions (slug, runtime, name, code, created_at) VALUES ('4uv97vivibwwa6q1', 'cpp', 'Read data from redis', 324 | '#include 325 | #include 326 | #include 327 | #include 328 | 329 | using json = nlohmann::json; 330 | 331 | int main(int argc, char *argv[]) { 332 | // Connect to Redis 333 | redisContext *redis = redisConnect(getenv("REDIS_HOST"), 6379); 334 | if (redis == NULL || redis->err) { 335 | std::cerr << "Error connecting to Redis" << std::endl; 336 | return 1; 337 | } 338 | 339 | // Create JSON object and serialize it 340 | json data; 341 | data["name"] = "John"; 342 | data["age"] = 30; 343 | std::string serialized_data = data.dump(); 344 | 345 | // Save data to Redis 346 | redisReply *reply = (redisReply*) redisCommand(redis, "SET foo %s", serialized_data.c_str()); 347 | if (reply == NULL) { 348 | std::cerr << "Error saving data to Redis" << std::endl; 349 | return 1; 350 | } 351 | freeReplyObject(reply); 352 | 353 | // Print output 354 | json output; 355 | output["message"] = "Data saved to Redis"; 356 | std::cout << output.dump(4) << std::endl; 357 | 358 | // Disconnect from Redis 359 | redisFree(redis); 360 | 361 | return 0; 362 | } 363 | ', datetime('now')); 364 | 365 | INSERT INTO functions (slug, runtime, name, code, created_at) VALUES ('guv9a3dqg5v4zyhv', 'cpp', 'Calculate first 1000 Fibbonaci numbers with boost', 366 | '#include 367 | #include 368 | #include 369 | #include 370 | 371 | using namespace std; 372 | using namespace boost::multiprecision; 373 | using json = nlohmann::json; 374 | 375 | // Custom serializer for cpp_int 376 | namespace nlohmann { 377 | template<> 378 | struct adl_serializer { 379 | static void to_json(json& j, const cpp_int& num) { 380 | j = num.str(); 381 | } 382 | 383 | static void from_json(const json& j, cpp_int& num) { 384 | num = cpp_int(j.get()); 385 | } 386 | }; 387 | } 388 | 389 | int main() { 390 | cpp_int a = 0, b = 1, c; 391 | int n = 1000; 392 | 393 | // Create a vector to store the Fibonacci numbers 394 | vector fib_numbers; 395 | 396 | // Add the first two Fibonacci numbers to the vector 397 | fib_numbers.push_back(a); 398 | fib_numbers.push_back(b); 399 | 400 | // Calculate and store the next n-2 Fibonacci numbers 401 | for (int i = 2; i < n; i++) { 402 | c = a + b; 403 | a = b; 404 | b = c; 405 | fib_numbers.push_back(c); 406 | } 407 | 408 | // Create a JSON object and add the Fibonacci numbers array 409 | json output; 410 | output["fib"] = fib_numbers; 411 | 412 | // Print the JSON object to stdout 413 | cout << output.dump() << endl; 414 | 415 | return 0; 416 | } 417 | ', datetime('now')); 418 | -------------------------------------------------------------------------------- /services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jb41/lambdapi/3b8e06450cd210b8a80c96b687b62ef3c95b81fe/services/__init__.py -------------------------------------------------------------------------------- /services/config.py: -------------------------------------------------------------------------------- 1 | import os, \ 2 | yaml 3 | from typing import Any 4 | 5 | 6 | 7 | def get_config(filename) -> dict[str, Any]: 8 | with open(filename) as file: 9 | return yaml.safe_load(file) 10 | 11 | 12 | def load_yaml_files(directory: str) -> dict[str, Any]: 13 | yaml_files = {} 14 | for filename in os.listdir(directory): 15 | if filename.endswith('.yaml') or filename.endswith('.yml'): 16 | with open(os.path.join(directory, filename)) as file: 17 | yaml_data = yaml.safe_load(file) 18 | key = filename.rsplit('.', 1)[0] 19 | yaml_files[key] = yaml_data 20 | 21 | return yaml_files 22 | 23 | 24 | config = get_config("config.yaml") 25 | runtimes_config = load_yaml_files("runtimes") 26 | -------------------------------------------------------------------------------- /services/create_dockerfile.py: -------------------------------------------------------------------------------- 1 | class CreateDockerfile: 2 | def __init__(self, runtime_name: str, runtime_config: str): 3 | self.runtime_config = runtime_config 4 | self.dockerfile_filename = self.__dockerfile_full_name(runtime_name) 5 | 6 | 7 | def __call__(self): 8 | dockerfile = self.__dockerfile_template().format( 9 | repository_image_name=self.runtime_config["docker"]["repository_image"], 10 | docker_run_cmd=self.runtime_config["docker"]["run_cmd"], 11 | ) 12 | 13 | with open(self.dockerfile_filename, "w") as f: 14 | f.write(dockerfile) 15 | 16 | 17 | def __dockerfile_template(self): 18 | return """\ 19 | FROM {repository_image_name} 20 | WORKDIR /app 21 | RUN {docker_run_cmd} 22 | VOLUME ["/data"] 23 | CMD ["sh", "-c", "while true; do sleep 10; done"] 24 | """ 25 | 26 | 27 | def __dockerfile_full_name(self, filename: str): 28 | return "dockerfiles/" + filename + ".Dockerfile" 29 | -------------------------------------------------------------------------------- /services/database.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | 4 | 5 | database_name = "functions.db" 6 | 7 | 8 | def create_table(): 9 | with sqlite3.connect(database_name) as db: 10 | db.execute(""" 11 | CREATE TABLE IF NOT EXISTS functions ( 12 | id INTEGER PRIMARY KEY AUTOINCREMENT, 13 | name TEXT, 14 | slug TEXT, 15 | runtime TEXT, 16 | code TEXT, 17 | created_at DATETIME NOT NULL, 18 | UNIQUE (slug) 19 | ); 20 | """) 21 | 22 | db.execute(""" 23 | CREATE INDEX functions_slug_index ON functions (slug); 24 | """) 25 | 26 | db.commit() 27 | 28 | 29 | def seed_database(): 30 | with sqlite3.connect(database_name) as db: 31 | with open('seed.sql', 'r') as f: 32 | seed_script = f.read() 33 | 34 | db.executescript(seed_script) 35 | db.commit() 36 | -------------------------------------------------------------------------------- /services/helpers.py: -------------------------------------------------------------------------------- 1 | import ast, \ 2 | os, \ 3 | random, \ 4 | re, \ 5 | string, \ 6 | yaml 7 | from typing import Any, Optional 8 | 9 | from services.config import config 10 | 11 | 12 | 13 | dockerfiles_dir = 'dockerfiles' 14 | 15 | 16 | def parse_output(data: str) -> Any: 17 | try: 18 | parsed_data = ast.literal_eval(data) 19 | if isinstance(parsed_data, dict): 20 | return parsed_data 21 | else: 22 | return str(parsed_data) 23 | except (ValueError, SyntaxError): 24 | return data 25 | 26 | 27 | def get_openai_config() -> dict[str, Any]: 28 | return config.get('llm', {}).get('openai', {}) 29 | 30 | 31 | def openai_apikey() -> str: 32 | return get_openai_config().get('apikey') 33 | 34 | 35 | def openai_system_prompt(language) -> str: 36 | return get_openai_config().get('system_prompt').format(language=language) 37 | 38 | 39 | def openai_model_params() -> dict[str, Any]: 40 | return get_openai_config().get('model_params', { 'model': 'gpt-3.5-turbo', 'temperature': 0.4, 'top_p': 1 }) 41 | 42 | 43 | # TODO 44 | # Change to https 45 | def endpoint_url(slug: str) -> str: 46 | return f"http://{config.get('hostname')}/run/{slug}" 47 | 48 | 49 | # TODO: Add a check to make sure the slug is unique 50 | async def generate_unique_slug(length: int = 16) -> Optional[str]: 51 | characters = string.ascii_lowercase + string.digits 52 | slug = ''.join(random.choices(characters, k=length)) 53 | 54 | return slug 55 | 56 | 57 | def extract_code_and_text(input_str): 58 | code_regex = re.compile(r"```([\s\S]*?)```", re.MULTILINE) 59 | code_blocks = [] 60 | text_blocks = [] 61 | last_index = 0 62 | 63 | for match in code_regex.finditer(input_str): 64 | code_block = match.group(1).strip() 65 | if re.match(r'^\w+\n', code_block): 66 | code_block = re.sub(r'^\w+\n', '', code_block) 67 | code_blocks.append(code_block) 68 | text_blocks.append(input_str[last_index:match.start()].strip()) 69 | last_index = match.end() 70 | 71 | text_blocks.append(input_str[last_index:].strip()) 72 | 73 | return { 74 | 'code': '\n'.join(code_blocks), 75 | 'text': '\n'.join(text_blocks) 76 | } 77 | -------------------------------------------------------------------------------- /services/llm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jb41/lambdapi/3b8e06450cd210b8a80c96b687b62ef3c95b81fe/services/llm/__init__.py -------------------------------------------------------------------------------- /services/llm/openai.py: -------------------------------------------------------------------------------- 1 | import openai 2 | from typing import List 3 | 4 | from services.helpers import openai_apikey, \ 5 | openai_system_prompt, \ 6 | openai_model_params 7 | 8 | 9 | 10 | openai.api_key = openai_apikey() 11 | model_params = openai_model_params() 12 | 13 | class OpenAi: 14 | @staticmethod 15 | def chat_completion(messages: List[dict], language: str) -> dict: 16 | completion = openai.ChatCompletion.create( 17 | model=model_params['model'], 18 | temperature=model_params['temperature'], 19 | top_p=model_params['top_p'], 20 | messages=OpenAi._messages_with_prompt(messages, language) 21 | ) 22 | return completion.choices[0].message 23 | 24 | 25 | @staticmethod 26 | def _messages_with_prompt(messages: List[str], language) -> List[dict]: 27 | return [{ "role": "system", "content": openai_system_prompt(language) }] + messages 28 | -------------------------------------------------------------------------------- /services/redis_hostname.py: -------------------------------------------------------------------------------- 1 | import docker 2 | 3 | from services.config import config 4 | 5 | 6 | 7 | def get_redis_container(): 8 | client = docker.from_env() 9 | for container in client.containers.list(): 10 | if container.name == 'redis': 11 | return container 12 | return None 13 | 14 | 15 | def get_redis_ip_address(container): 16 | for network in container.attrs["NetworkSettings"]["Networks"].values(): 17 | if network["IPAddress"]: 18 | return network["IPAddress"] 19 | return None 20 | 21 | 22 | def redis_hostname(): 23 | redis_host = config.get("redis_host") 24 | 25 | if redis_host: 26 | return redis_host 27 | 28 | redis_container = get_redis_container() 29 | if redis_container: 30 | return get_redis_ip_address(redis_container) 31 | 32 | return None 33 | -------------------------------------------------------------------------------- /services/run_code.py: -------------------------------------------------------------------------------- 1 | import os, \ 2 | random, \ 3 | string 4 | from typing import Tuple 5 | 6 | from services.config import runtimes_config as rt_config 7 | from services.run_containerized_code import RunContainerizedCode 8 | 9 | 10 | 11 | class RunCode: 12 | def __init__(self, slug: str, runtime_name: str, code: str) -> Tuple[str, str]: 13 | self._slug = slug 14 | self._runtime_name = runtime_name 15 | self._code = code 16 | 17 | 18 | def run(self, args: list): 19 | try: 20 | runtime_config = rt_config[self._runtime_name] 21 | 22 | if runtime_config: 23 | filename = self._slug + ''.join(random.choices(string.ascii_letters, k=16)) 24 | input_file = os.path.join("tmp", filename) 25 | 26 | with open(input_file, "w") as f: 27 | f.write(self._code) 28 | 29 | stdout, stderr, output_file = RunContainerizedCode(image_name=runtime_config['image_name'], 30 | compile_cmd=runtime_config['run'].get('compile'), 31 | execute_cmd=runtime_config['run'].get('execute') 32 | ).run(input_file, args) 33 | 34 | return stdout, stderr, output_file 35 | 36 | else: 37 | raise Exception("Runtime not found") 38 | 39 | finally: 40 | os.remove(input_file) 41 | -------------------------------------------------------------------------------- /services/run_containerized_code.py: -------------------------------------------------------------------------------- 1 | import docker, \ 2 | os, \ 3 | random, \ 4 | string, \ 5 | subprocess 6 | from io import BytesIO 7 | import mimetypes 8 | from typing import Tuple 9 | 10 | from services.config import config 11 | from services.redis_hostname import redis_hostname 12 | 13 | 14 | 15 | class RunContainerizedCode: 16 | def __init__(self, image_name: str, compile_cmd: str, execute_cmd: str) -> Tuple[str, str]: 17 | self._client = docker.from_env() 18 | self._image_name = image_name 19 | self._compile_cmd = compile_cmd 20 | self._execute_cmd = execute_cmd 21 | 22 | 23 | def __random_string(self, prefix: str, length: int = 16) -> str: 24 | return prefix + '-' + ''.join(random.choices(string.ascii_letters, k=length)) 25 | 26 | 27 | def __outside_file_name(self, container_name: str, file_name: str) -> str: 28 | file_extension = file_name.split('.')[-1] 29 | return f"tmp/{container_name}.{file_extension}" 30 | 31 | 32 | def __copy_file_outside(self, container, output_file): 33 | file_name = self.__outside_file_name(container.name, output_file) 34 | copy_cmd = f"docker cp {container.name}:/app/{output_file} {file_name}" 35 | subprocess.run(copy_cmd, shell=True, check=True) 36 | 37 | return file_name 38 | 39 | 40 | def __system_files(self): 41 | return [ 42 | 'main', 'main.out', 43 | 'node_modules', 'package-lock.json', 'package.json', 44 | 'Gemfile', 'Gemfile.lock', 45 | ] 46 | 47 | 48 | def __get_first_file(self, exec_result): 49 | if exec_result.exit_code != 0: 50 | return None 51 | 52 | files = exec_result.output.decode('utf-8').splitlines() 53 | files = [file for file in files if file not in self.__system_files()] 54 | 55 | if len(files) == 0: 56 | return None 57 | 58 | files.sort() 59 | return files[0] 60 | 61 | 62 | def __output_file_name(self, container): 63 | cmd = 'find /app -maxdepth 1 -type f ! -name ".*" -exec basename {} \\;' 64 | output_file = self.__get_first_file(container.exec_run(cmd=cmd)) 65 | 66 | return output_file 67 | 68 | 69 | def __read_file_to_memory(self, file_name): 70 | mime_type, _ = mimetypes.guess_type(file_name) 71 | if mime_type is None: 72 | mime_type = "application/octet-stream" 73 | 74 | with open(file_name, "rb") as file: 75 | content = file.read() 76 | 77 | return { "content": BytesIO(content), "mime_type": mime_type } 78 | 79 | 80 | def __volumes(self): 81 | return { config['storage_mount_point']: { "bind": "/data", "mode": "rw" } } 82 | 83 | 84 | def __env_vars(self): 85 | envs = {} 86 | redis_host = redis_hostname() 87 | if redis_host: 88 | envs["REDIS_HOST"] = redis_host 89 | 90 | return envs 91 | 92 | 93 | def run(self, input_file: str, args: list = []): 94 | container_name = self.__random_string(self._image_name) 95 | container = self._client.containers.run(self._image_name, name=container_name, detach=True, 96 | volumes=self.__volumes(), 97 | network_mode="host", 98 | environment=self.__env_vars()) 99 | 100 | copy_to_docker_cmd = f"docker cp {input_file} {container_name}:/app/main" 101 | subprocess.run(copy_to_docker_cmd, shell=True, check=True) 102 | 103 | if self._compile_cmd: 104 | result = container.exec_run(cmd=self._compile_cmd, tty=True, stderr=True) 105 | 106 | if result.exit_code != 0: 107 | container.kill() 108 | container.remove() 109 | 110 | return "", result.output.decode("utf-8"), None 111 | 112 | run_cmd = self._execute_cmd + ' ' + ' '.join(args) 113 | result = container.exec_run(cmd=run_cmd, tty=True, stderr=True) 114 | 115 | output_file_name = self.__output_file_name(container) 116 | if output_file_name: 117 | outside_file_name = self.__copy_file_outside(container, output_file_name) 118 | output_file = self.__read_file_to_memory(outside_file_name) 119 | os.remove(outside_file_name) 120 | else: 121 | output_file = None 122 | 123 | stdout = result.output.decode("utf-8") 124 | stderr = result.output.decode("utf-8") if result.exit_code != 0 else "" 125 | 126 | container.kill() 127 | container.remove() 128 | 129 | return stdout, stderr, output_file 130 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os, \ 2 | subprocess 3 | 4 | from services.config import runtimes_config as rt_config 5 | from services.create_dockerfile import CreateDockerfile 6 | from services.database import create_table, seed_database, database_name 7 | from services.helpers import dockerfiles_dir 8 | 9 | 10 | 11 | # Create functions table 12 | print("Creating functions table...") 13 | if not os.path.exists(database_name): 14 | with open(database_name, 'a'): 15 | pass 16 | create_table() 17 | seed_database() 18 | 19 | 20 | # Create tmp directory 21 | print("Creating tmp directory...") 22 | if not os.path.exists("tmp"): 23 | os.makedirs("tmp") 24 | 25 | 26 | # Create Dockerfiles and build all images 27 | print("Creating dockerfiles and building images...") 28 | if not os.path.exists(dockerfiles_dir): 29 | os.makedirs(dockerfiles_dir) 30 | 31 | 32 | for runtime_name, runtime_config in rt_config.items(): 33 | print("Building image for: " + runtime_config['name']) 34 | create_dockerfile = CreateDockerfile(runtime_name, runtime_config) 35 | create_dockerfile() 36 | 37 | command = f"docker build -t {runtime_config['image_name']} -f {dockerfiles_dir}/{runtime_name}.Dockerfile ." 38 | subprocess.run(command, shell=True, check=True) 39 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Nunito Sans', sans-serif; 3 | background-color: #f5f7fa; 4 | color: #151823; 5 | } 6 | 7 | .bg-light { 8 | background-color: #f5f7fa !important; 9 | } 10 | 11 | h1, h2, h3 { 12 | font-weight: 600; 13 | } 14 | 15 | a { 16 | text-decoration: none; 17 | } 18 | 19 | a:hover { 20 | text-decoration: underline; 21 | } 22 | 23 | .logo { 24 | font-size: 3rem; 25 | font-weight: 700; 26 | font-family: 'Noto Serif', serif; 27 | background-image: linear-gradient(to right, #09203F, #537895); 28 | -webkit-background-clip: text; 29 | background-clip: text; 30 | color: transparent; 31 | display: inline; 32 | } 33 | 34 | .container-fluid { 35 | padding-top: 20px; 36 | padding-bottom: 20px; 37 | } 38 | 39 | .full-height { 40 | height: 100%; 41 | } 42 | 43 | .editor-container, 44 | .output-container { 45 | height: 95%; 46 | overflow: auto; 47 | background-color: #fff; 48 | border-radius: 5px; 49 | } 50 | 51 | .card { 52 | background-color: #f8f9fa; 53 | border-color: #dee2e6; 54 | } 55 | 56 | .spinner-container { 57 | display: none; 58 | position: fixed; 59 | top: 0; 60 | left: 0; 61 | right: 0; 62 | bottom: 0; 63 | background-color: rgba(0, 0, 0, 0.5); 64 | z-index: 1000; 65 | align-items: center; 66 | justify-content: center; 67 | } 68 | 69 | .spinner { 70 | display: inline-block; 71 | width: 80px; 72 | height: 80px; 73 | border: 8px solid #f3f3f3; 74 | border-radius: 50%; 75 | border-top-color: #3498db; 76 | animation: spin 0.42s linear infinite; 77 | } 78 | 79 | @keyframes spin { 80 | 0% { 81 | transform: rotate(0deg); 82 | } 83 | 100% { 84 | transform: rotate(360deg); 85 | } 86 | } 87 | 88 | .contenteditable { 89 | transition: border 0.2s, border-radius 0.2s; 90 | } 91 | 92 | .contenteditable:hover { 93 | border: 1px solid blue; 94 | border-radius: 3px; 95 | } 96 | 97 | .code-text-row { 98 | min-height: 340px; 99 | } 100 | 101 | .textarea-container { 102 | position: relative; 103 | } 104 | 105 | .floating-button { 106 | position: absolute; 107 | bottom: -2px; 108 | right: 10px; 109 | z-index: 10; 110 | padding: 0.5rem; 111 | } 112 | 113 | .prompt-input { 114 | resize: none; 115 | } 116 | -------------------------------------------------------------------------------- /static/function-details.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | λπ LambdaPi 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |

LambdaPi

24 |

Serverless Meets Language Models

25 |
26 | 27 |
28 |
29 | 32 |
33 | 34 |
35 |
36 |

Function name

37 |
38 |
39 | 40 |
41 |
42 |

Endpoint URL:

43 | 44 | 45 |
46 |
47 | 48 |
49 |
50 | 51 |
52 |
53 | 54 |
55 |
56 | 57 |  |  58 | 59 |
60 |
61 | 62 |
63 |
64 |

Code:

65 |
66 |
67 |
68 |

LLM non-code output:

69 |
70 |
71 |
72 |
73 | 74 |
75 |
76 |
77 | 83 | 87 | 91 |
92 |
93 |
94 | 95 |
96 |
97 |
98 |
99 | 102 | Add parameters to the request 103 |
104 |
105 |
106 | 107 |
108 |
109 |
110 |
111 |
112 | 113 |
114 |
115 | 118 |
119 |
120 | 121 |
122 |
123 |

Result:

124 |
125 | 126 | The outcome will be displayed here, which may include JSON data, an image, or a downloadable 127 | file. You can execute the code independently by making an API request. 128 | 129 |
130 |
131 |
132 |
133 | 134 |
135 | 136 |

137 | LambdaPi v1.0.0 |  138 |  GitHub 139 |

140 |
141 |
142 | 143 |
144 |
145 |
146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | λπ LambdaPi 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

LambdaPi

23 |

Serverless Meets Language Models

24 |
25 | 26 |
27 |
28 |
29 |  Create Function 30 |
31 |
32 | 33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
IDNameRuntimeCreated atEndpoint URLActions
49 |
50 |
51 |
52 | 53 |
54 | 55 |

56 | LambdaPi v1.0.0 |  57 |  GitHub 58 |

59 |
60 |
61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /static/js/function-details.js: -------------------------------------------------------------------------------- 1 | var messages = []; 2 | var editor = null; 3 | var func = {}; 4 | var runtimes = []; 5 | var codeHistory = []; 6 | var currentCodeIndex = -1; 7 | 8 | /* 9 | * ******** ******** ******** ******** ******** ******** ******** ******** 10 | * 11 | * Code Editor 12 | * 13 | */ 14 | 15 | require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.31.1/min/vs' }}); 16 | 17 | function loadMonacoEditor() { 18 | return new Promise((resolve) => { 19 | require(['vs/editor/editor.main'], function () { 20 | const editor = monaco.editor.create(document.getElementById('code-editor'), { 21 | language: 'python', 22 | theme: 'vs-light', 23 | minimap: { enabled: false } 24 | }); 25 | resolve(editor); 26 | }); 27 | }); 28 | } 29 | 30 | /* 31 | * ******** ******** ******** ******** ******** ******** ******** ******** 32 | * 33 | * Function stuff 34 | * 35 | */ 36 | function fetchHelper(url, options = {}) { 37 | return new Promise((resolve, reject) => { 38 | fetch(url, options) 39 | .then(response => { 40 | if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } 41 | return response.json(); 42 | }) 43 | .then(data => { 44 | resolve(data); 45 | }) 46 | .catch(error => { 47 | console.error('Error fetching data:', error); 48 | reject(error); 49 | }); 50 | }); 51 | } 52 | 53 | function getFunctionId() { 54 | return (new URLSearchParams(window.location.search)).get('id'); 55 | } 56 | 57 | function getFunction(functionId) { 58 | return fetchHelper('/api/functions/' + functionId); 59 | } 60 | 61 | function updateFunction(functionId, params) { 62 | return fetchHelper('/api/functions/' + functionId, { 63 | method: 'PUT', 64 | headers: { 'Content-Type': 'application/json', }, 65 | body: JSON.stringify(params), 66 | }); 67 | } 68 | 69 | function getRuntimes() { 70 | return fetchHelper('/api/runtimes/available'); 71 | } 72 | 73 | /* 74 | * ******** ******** ******** ******** ******** ******** ******** ******** 75 | * 76 | * Run stuff 77 | * 78 | */ 79 | function runCode(endpoint_url, params) { 80 | showSpinner(); 81 | fetch(endpoint_url, { 82 | method: 'POST', 83 | headers: { 'Content-Type': 'application/json', }, 84 | body: JSON.stringify(params), 85 | }) 86 | .then(response => { 87 | hideSpinner(); 88 | const contentType = response.headers.get('content-type'); 89 | if (contentType && contentType.indexOf('application/json') !== -1) { 90 | return response.json().then(showJsonResult); 91 | } else if (contentType && contentType.indexOf('image/') !== -1) { 92 | return response.blob().then(showImageResult); 93 | } else { 94 | return response.blob().then(blob => showDownloadResult(blob, contentType)); 95 | } 96 | }) 97 | .catch(error => { console.error('Error fetching data:', error); }); 98 | } 99 | 100 | function paramsList() { 101 | const paramsList = document.getElementById('input-params').value.split('\n'); 102 | 103 | if (paramsList.length > 0 && paramsList[0].length > 0) { 104 | return paramsList.reduce((params, value, index) => { 105 | params[index.toString()] = value; 106 | return params; 107 | }, {}); 108 | } else { 109 | return {}; 110 | } 111 | } 112 | 113 | function createResultElement(childElement) { 114 | const resultOutput = document.getElementById('result-output'); 115 | resultOutput.innerHTML = ''; 116 | resultOutput.appendChild(childElement); 117 | } 118 | 119 | function showJsonResult(result) { 120 | const codeElement = document.createElement('code'); 121 | codeElement.innerText = JSON.stringify(result, null, 4); 122 | 123 | const preElement = document.createElement('pre'); 124 | preElement.appendChild(codeElement); 125 | 126 | createResultElement(preElement); 127 | } 128 | 129 | function showImageResult(imageBlob) { 130 | const imageUrl = URL.createObjectURL(imageBlob); 131 | const img = document.createElement('img'); 132 | img.src = imageUrl; 133 | 134 | createResultElement(img); 135 | } 136 | 137 | function showDownloadResult(fileBlob, contentType) { 138 | const downloadUrl = URL.createObjectURL(fileBlob); 139 | const downloadLink = document.createElement('a'); 140 | downloadLink.href = downloadUrl; 141 | downloadLink.download = 'download.' + getFileExtension(contentType); 142 | downloadLink.innerText = 'Download file'; 143 | downloadLink.classList.add('btn'); 144 | downloadLink.classList.add('btn-primary'); 145 | 146 | createResultElement(downloadLink); 147 | } 148 | 149 | function getFileExtension(contentType) { 150 | const ext = contentType.split('/').pop().split(';')[0]; 151 | return ext === 'octet-stream' ? '' : ext; 152 | } 153 | 154 | /* 155 | * ******** ******** ******** ******** ******** ******** ******** ******** 156 | * 157 | * UI Manipulation 158 | * 159 | */ 160 | function getLanguageForRuntime(runtime) { 161 | const foundRuntime = runtimes.find(rt => rt.runtime === runtime); 162 | 163 | if (foundRuntime) { 164 | return foundRuntime.monaco_editor_id; 165 | } else { 166 | throw new Error(`Runtime ${runtime} not found`); 167 | } 168 | } 169 | 170 | function setFunctionValues() { 171 | document.getElementById('function-name').textContent = func.name; 172 | 173 | document.getElementById('function-runtime').value = func.runtime || 'python'; 174 | const language = getLanguageForRuntime(func.runtime || 'python'); 175 | monaco.editor.setModelLanguage(editor.getModel(), language); 176 | 177 | document.getElementById('function-endpoint-url').text = func.endpoint_url; 178 | document.getElementById('function-endpoint-url').href = func.endpoint_url; 179 | 180 | if (func.code) { 181 | editor.setValue(func.code); 182 | codeHistory.push(func.code); 183 | currentCodeIndex += 1; 184 | } 185 | } 186 | 187 | function setRuntimes() { 188 | const select = document.getElementById('function-runtime'); 189 | runtimes.forEach(runtime => { 190 | const option = document.createElement('option'); 191 | option.value = runtime.runtime; 192 | option.text = runtime.name; 193 | select.appendChild(option); 194 | }); 195 | } 196 | 197 | /* 198 | * ******** ******** ******** ******** ******** ******** ******** ******** 199 | * 200 | * Save & Run 201 | * 202 | */ 203 | async function saveAndRun() { 204 | await updateFunction(func.id, { 205 | name: document.getElementById('function-name').textContent, 206 | code: editor.getValue(), 207 | runtime: document.getElementById('function-runtime').value, 208 | }); 209 | if (editor.getValue() !== codeHistory[currentCodeIndex]) { 210 | codeHistory.push(editor.getValue()); 211 | currentCodeIndex += 1; 212 | changePrevNextHistoryButtonDisabledState(); 213 | } 214 | runCode(func.endpoint_url, paramsList()); 215 | } 216 | 217 | /* 218 | * ******** ******** ******** ******** ******** ******** ******** ******** 219 | * 220 | * Completion 221 | * 222 | */ 223 | function createCurrentMessageForCompletion() { 224 | const code = editor.getValue(); 225 | const llmPrompt = document.getElementById('llm-prompt').value; 226 | 227 | const newMessage = "current code:\n```" + code + "```\n\n" + llmPrompt; 228 | return { "role": "user", "content": newMessage }; 229 | } 230 | 231 | function updateCodeInEditor(code) { 232 | if (code.length > 0) { 233 | editor.setValue(code); 234 | } 235 | if (code !== codeHistory[currentCodeIndex]) { 236 | codeHistory.push(code); 237 | currentCodeIndex += 1; 238 | changePrevNextHistoryButtonDisabledState(); 239 | } 240 | } 241 | 242 | function updateTextOutput(text) { 243 | document.getElementById('output').innerHTML = "

" + text + "

"; 244 | } 245 | 246 | function requestCompletions() { 247 | showSpinner(); 248 | const selectedRuntime = document.getElementById('function-runtime').value; 249 | fetch('/api/completions', { 250 | method: 'POST', 251 | headers: { 'Content-Type': 'application/json', }, 252 | body: JSON.stringify({ 253 | messages: messages, 254 | language: getLanguageForRuntime(selectedRuntime) 255 | }) 256 | }) 257 | .then(response => { 258 | hideSpinner(); 259 | if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } 260 | return response.json(); 261 | }) 262 | .then(data => { 263 | messages.push(data.message); 264 | updateCodeInEditor(data.code); 265 | updateTextOutput(data.text); 266 | }) 267 | .catch(error => { 268 | console.error('Error fetching data:', error); 269 | }); 270 | } 271 | 272 | function getCompletion() { 273 | messages.push(createCurrentMessageForCompletion()); 274 | document.getElementById('llm-prompt').value = ''; 275 | requestCompletions(); 276 | } 277 | 278 | /* 279 | * ******** ******** ******** ******** ******** ******** ******** ******** 280 | * 281 | * Code history 282 | * 283 | */ 284 | function changePrevNextHistoryButtonDisabledState() { 285 | const prevButton = document.getElementById('prev-code-in-history'); 286 | const nextButton = document.getElementById('next-code-in-history'); 287 | 288 | prevButton.disabled = currentCodeIndex <= 0; 289 | nextButton.disabled = currentCodeIndex >= codeHistory.length - 1; 290 | } 291 | 292 | function navigateCodeHistory(direction) { 293 | const newIndex = currentCodeIndex + direction; 294 | 295 | if (newIndex >= 0 && newIndex < codeHistory.length) { 296 | currentCodeIndex = newIndex; 297 | editor.setValue(codeHistory[currentCodeIndex]); 298 | changePrevNextHistoryButtonDisabledState(); 299 | } 300 | } 301 | 302 | /* 303 | * ******** ******** ******** ******** ******** ******** ******** ******** 304 | * 305 | * Helpers 306 | * 307 | */ 308 | const debounce = (func, wait) => { 309 | let timeout; 310 | return (...args) => { 311 | clearTimeout(timeout); 312 | timeout = setTimeout(() => func(...args), wait); 313 | }; 314 | }; 315 | 316 | const showSpinner = () => document.getElementById('spinner').style.display = 'flex'; 317 | 318 | const hideSpinner = () => document.getElementById('spinner').style.display = 'none'; 319 | 320 | const autoResizeTextarea = textarea => { 321 | textarea.style.height = 'auto'; 322 | textarea.style.height = `${textarea.scrollHeight}px`; 323 | }; 324 | 325 | const copyEndpointUrlToClipboard = async () => { 326 | const textToCopy = document.getElementById('function-endpoint-url').text; 327 | try { 328 | await navigator.clipboard.writeText(textToCopy); 329 | } catch (err) { 330 | console.error('Failed to copy text:', err); 331 | } 332 | }; 333 | 334 | /* 335 | * ******** ******** ******** ******** ******** ******** ******** ******** 336 | * 337 | * Init 338 | * 339 | */ 340 | async function init() { 341 | try { 342 | editor = await loadMonacoEditor(); 343 | runtimes = await getRuntimes(); 344 | setRuntimes(runtimes); 345 | console.log(runtimes); 346 | func = await getFunction(getFunctionId()); 347 | setFunctionValues(func); 348 | } catch (error) { 349 | console.error('Error:', error); 350 | } 351 | } 352 | 353 | 354 | init(); 355 | 356 | document.getElementById('function-name').addEventListener('input', debounce((event) => { 357 | updateFunction(func.id, { name: event.target.textContent }); 358 | }, 1000)); 359 | 360 | document.getElementById('function-runtime').addEventListener('change', (event) => { 361 | const selectedRuntime = event.target.value; 362 | const language = getLanguageForRuntime(selectedRuntime); 363 | monaco.editor.setModelLanguage(editor.getModel(), language); 364 | updateFunction(func.id, { runtime: selectedRuntime }); 365 | }); 366 | 367 | document.getElementById('llm-prompt').addEventListener('input', (event) => { 368 | autoResizeTextarea(event.target); 369 | }); 370 | 371 | document.getElementById('llm-prompt').focus(); 372 | -------------------------------------------------------------------------------- /static/js/index.js: -------------------------------------------------------------------------------- 1 | const apiUrl = '/api/functions'; 2 | 3 | const fetchFunctions = async () => (await fetch(apiUrl)).json(); 4 | 5 | const deleteFunction = async functionId => { 6 | if (!confirm("Are you sure you want to delete this function?")) return; 7 | try { 8 | const response = await fetch(`${apiUrl}/${functionId}`, { method: 'DELETE' }); 9 | if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); 10 | location.reload(); 11 | } catch (error) { 12 | console.error('Error fetching data:', error); 13 | } 14 | }; 15 | 16 | const createNewFunction = async () => { 17 | try { 18 | const response = await fetch(apiUrl, { method: 'POST' }); 19 | if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); 20 | const data = await response.json(); 21 | window.location.href = `/function-details.html?id=${data.id}`; 22 | } catch (error) { 23 | console.error('Error fetching data:', error); 24 | } 25 | }; 26 | 27 | const displayFunctions = functions => { 28 | const functionList = document.getElementById('function-list'); 29 | functionList.innerHTML = ''; 30 | 31 | functions.forEach(func => { 32 | const rowItem = document.createElement('tr'); 33 | rowItem.innerHTML = ` 34 | ${func.id} 35 | ${func.name} 36 | ${func.runtime} 37 | ${func.created_at} 38 | 39 | ${func.endpoint_url}  40 | 43 | 44 |  Edit 45 | 46 | `; 47 | functionList.appendChild(rowItem); 48 | }); 49 | }; 50 | 51 | const copyEndpointUrlToClipboard = async textToCopy => { 52 | try { 53 | await navigator.clipboard.writeText(textToCopy); 54 | } catch (err) { 55 | console.error('Failed to copy text: ', err); 56 | } 57 | }; 58 | 59 | (async () => { 60 | try { 61 | const functions = await fetchFunctions(); 62 | displayFunctions(functions); 63 | } catch (error) { 64 | console.error('Error fetching data:', error); 65 | } 66 | })(); 67 | --------------------------------------------------------------------------------