fabric/server/fabric_server/fabric_api_server.py

274 lines
8.1 KiB
Python

import jwt
import json
import openai
from flask import Flask, request, jsonify
from functools import wraps
import re
import requests
import os
from dotenv import load_dotenv
from importlib import resources
from .fabric_web_interface import main as web_interface
app = Flask(__name__)
@app.errorhandler(404)
def not_found(e):
return jsonify({"error": "The requested resource was not found."}), 404
@app.errorhandler(500)
def server_error(e):
return jsonify({"error": "An internal server error occurred."}), 500
##################################################
##################################################
#
# ⚠️ CAUTION: This is an HTTP-only server!
#
# If you don't know what you're doing, don't run
#
##################################################
##################################################
## Setup
## Did I mention this is HTTP only? Don't run this on the public internet.
## Set authentication on your APIs
## Let's at least have some kind of auth
load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")
## Define our own client
client = openai.OpenAI(api_key = openai.api_key)
# Read API tokens from the apikeys.json file
api_keys = resources.read_text("fabric_server", "fabric_api_keys.json")
valid_tokens = json.loads(api_keys)
# Read users from the users.json file
users = resources.read_text("fabric_server", "users.json")
users = json.loads(users)
# The function to check if the token is valid
def auth_required(f):
""" Decorator function to check if the token is valid.
Args:
f: The function to be decorated
Returns:
The decorated function
"""
@wraps(f)
def decorated_function(*args, **kwargs):
""" Decorated function to handle authentication token and API endpoint.
Args:
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
Result of the decorated function.
Raises:
KeyError: If 'Authorization' header is not found in the request.
TypeError: If 'Authorization' header value is not a string.
ValueError: If the authentication token is invalid or expired.
"""
# Get the authentication token from request header
auth_token = request.headers.get("Authorization", "")
# Remove any bearer token prefix if present
if auth_token.lower().startswith("bearer "):
auth_token = auth_token[7:]
# Get API endpoint from request
endpoint = request.path
# Check if token is valid
user = check_auth_token(auth_token, endpoint)
if user == "Unauthorized: You are not authorized for this API":
return jsonify({"error": user}), 401
return f(*args, **kwargs)
return decorated_function
# Check for a valid token/user for the given route
def check_auth_token(token, route):
""" Check if the provided token is valid for the given route and return the corresponding user.
Args:
token (str): The token to be checked for validity.
route (str): The route for which the token validity is to be checked.
Returns:
str: The user corresponding to the provided token and route if valid, otherwise returns "Unauthorized: You are not authorized for this API".
"""
# Check if token is valid for the given route and return corresponding user
if route in valid_tokens and token in valid_tokens[route]:
return users[valid_tokens[route][token]]
else:
return "Unauthorized: You are not authorized for this API"
# Define the allowlist of characters
ALLOWLIST_PATTERN = re.compile(r"^[a-zA-Z0-9\s.,;:!?\-]+$")
# Sanitize the content, sort of. Prompt injection is the main threat so this isn't a huge deal
def sanitize_content(content):
""" Sanitize the content by removing characters that do not match the ALLOWLIST_PATTERN.
Args:
content (str): The content to be sanitized.
Returns:
str: The sanitized content.
"""
return "".join(char for char in content if ALLOWLIST_PATTERN.match(char))
# Pull the URL content's from the GitHub repo
def fetch_content_from_url(url):
""" Fetches content from the given URL.
Args:
url (str): The URL from which to fetch content.
Returns:
str: The sanitized content fetched from the URL.
Raises:
requests.RequestException: If an error occurs while making the request to the URL.
"""
try:
response = requests.get(url)
response.raise_for_status()
sanitized_content = sanitize_content(response.text)
return sanitized_content
except requests.RequestException as e:
return str(e)
## APIs
# Make path mapping flexible and scalable
pattern_path_mappings = {
"extwis": {"system_url": "https://raw.githubusercontent.com/danielmiessler/fabric/main/patterns/extract_wisdom/system.md",
"user_url": "https://raw.githubusercontent.com/danielmiessler/fabric/main/patterns/extract_wisdom/user.md"},
"summarize": {"system_url": "https://raw.githubusercontent.com/danielmiessler/fabric/main/patterns/summarize/system.md",
"user_url": "https://raw.githubusercontent.com/danielmiessler/fabric/main/patterns/summarize/user.md"}
} # Add more pattern with your desire path as a key in this dictionary
# /<pattern>
@app.route("/<pattern>", methods=["POST"])
@auth_required # Require authentication
def milling(pattern):
""" Combine fabric pattern with input from user and send to OpenAI's GPT-4 model.
Returns:
JSON: A JSON response containing the generated response or an error message.
Raises:
Exception: If there is an error during the API call.
"""
data = request.get_json()
# Warn if there's no input
if "input" not in data:
return jsonify({"error": "Missing input parameter"}), 400
# Get data from client
input_data = data["input"]
# Set the system and user URLs
urls = pattern_path_mappings[pattern]
system_url, user_url = urls["system_url"], urls["user_url"]
# Fetch the prompt content
system_content = fetch_content_from_url(system_url)
user_file_content = fetch_content_from_url(user_url)
# Build the API call
system_message = {"role": "system", "content": system_content}
user_message = {"role": "user", "content": user_file_content + "\n" + input_data}
messages = [system_message, user_message]
try:
response = openai.chat.completions.create(
model="gpt-4-1106-preview",
messages=messages,
temperature=0.0,
top_p=1,
frequency_penalty=0.1,
presence_penalty=0.1,
)
assistant_message = response.choices[0].message.content
return jsonify({"response": assistant_message})
except Exception as e:
app.logger.error(f"Error occurred: {str(e)}")
return jsonify({"error": "An error occurred while processing the request."}), 500
@app.route("/register", methods=["POST"])
def register():
data = request.get_json()
username = data["username"]
password = data["password"]
if username in users:
return jsonify({"error": "Username already exists"}), 400
new_user = {
"username": username,
"password": password
}
users[username] = new_user
token = jwt.encode({"username": username}, os.getenv("JWT_SECRET"), algorithm="HS256")
return jsonify({"token": token.decode("utf-8")})
@app.route("/login", methods=["POST"])
def login():
data = request.get_json()
username = data["username"]
password = data["password"]
if username in users and users[username]["password"] == password:
# Generate a JWT token
token = jwt.encode({"username": username}, os.getenv("JWT_SECRET"), algorithm="HS256")
return jsonify({"token": token.decode("utf-8")})
return jsonify({"error": "Invalid username or password"}), 401
def main():
"""Runs the main fabric API backend server"""
app.run(host="127.0.0.1", port=13337, debug=True)
def run_web_interface():
"""Runs the web insterface"""
web_interface()
if __name__ == "__main__":
main()