""" OpenAPI access via http://localhost:5000/openapi/ on local docker-compose deployment """ #import warnings #warnings.filterwarnings("ignore") #------std lib modules:------- import os, sys, json, time import os.path from typing import Any, Tuple, List, Dict, Any, Callable, Optional from datetime import datetime, date #from collections import namedtuple import hashlib, traceback, logging from functools import wraps import base64 #-------ext libs-------------- #llm from langchain.callbacks.manager import CallbackManager from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler #import tiktoken from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.chains import RetrievalQA from langchain.callbacks.base import BaseCallbackHandler, BaseCallbackManager from langchain.prompts import PromptTemplate from langchain_community.llms import Ollama from langchain_community.document_loaders import PyPDFLoader, Docx2txtLoader from langchain_community.embeddings import OllamaEmbeddings #from langchain_community.vectorstores.elasticsearch import ElasticsearchStore #deprecated from langchain_elasticsearch import ElasticsearchStore from uuid import uuid4 from elasticsearch import NotFoundError, Elasticsearch # for normal read/write without vectors from elasticsearch_dsl import Search, A, Document, Date, Integer, Keyword, Float, Long, Text, connections from elasticsearch.exceptions import ConnectionError from pydantic import BaseModel, Field import logging_loki import jwt as pyjwt #flask, openapi from flask import Flask, send_from_directory, send_file, Response, request, jsonify from flask_cors import CORS, cross_origin from werkzeug.utils import secure_filename from flask_openapi3 import Info, Tag, OpenAPI, Server, FileStorage from flask_socketio import SocketIO, join_room, leave_room, rooms, send from cryptography.fernet import Fernet from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC #----------home grown-------------- #from scraper import WebScraper from funcs import group_by from elastictools import get_by_id, update_by_id, delete_by_id from models import init_indicies, QueryLog, Chatbot, User, Text from chatbot import ask_bot, train_text from speech import text_to_speech BOT_ROOT_PATH = os.getenv("BOT_ROOT_PATH") # JWT Bearer Sample jwt = { "type": "http", "scheme": "bearer", "bearerFormat": "JWT" } security_schemes = {"jwt": jwt} security = [{"jwt": []}] info = Info( title="Chatbot-API", version="1.0.0", summary="The REST-API", description="Default model: ..." ) servers = [ Server(url=BOT_ROOT_PATH ) ] class NotFoundResponse(BaseModel): code: int = Field(-1, description="Status Code") message: str = Field("Resource not found!", description="Exception Information") app = OpenAPI( __name__, info=info, servers=servers, responses={404: NotFoundResponse}, security_schemes=security_schemes ) def uses_jwt(required=True): """ Wraps routes in a jwt-required logic and passes decoded jwt and user from elasticsearch to the route as keyword """ def non_param_deco(f): @wraps(f) def decorated_route(*args, **kwargs): token = None if "Authorization" in request.headers: token = request.headers["Authorization"].split(" ")[1] if not token: if required: return jsonify({ 'status': 'error', "message": "Authentication Token is missing!", }), 401 else: kwargs["decoded_jwt"] = {} kwargs["user"] = None return f(*args, **kwargs) try: data = pyjwt.decode(token, app.config["jwt_secret"], algorithms=["HS256"]) except Exception as e: return jsonify({ 'status': 'error', "message": "JWT-decryption: " + str(e) }), 401 try: response = User.search().filter("term", **{"email": data["email"]})[0:5].execute() for hit in response: user = hit break except Exception as e: return jsonify({ 'status': 'error', "message": "Invalid Authentication token!" }), 401 kwargs["decoded_jwt"] = data kwargs["user"] = user return f(*args, **kwargs) return decorated_route return non_param_deco def create_key(salt: str, user_email: str) -> Fernet: """ Example salt: 9c46f833b3376c5f3b64d8a93951df4b Fernet usage: token = f.encrypt(b"Secret message!") """ salt_bstr = bytes(salt, "utf-8") email_bstr = bytes(user_email, "utf-8") #password = b"password" #salt = os.urandom(16) #salt = b"9c46f833b3376c5f3b64d8a93951df4b" kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, salt=salt_bstr, iterations=48, ) key = base64.urlsafe_b64encode(kdf.derive(email_bstr)) return Fernet(key) app.config['UPLOAD_FOLDER'] = 'uploads' app.config['CORS_HEADERS'] = 'Content-Type' app.config['CORS_METHODS'] = ["GET,POST,OPTIONS,DELETE,PUT"] env_to_conf = { "ELASTIC_URI": "elastic_uri", "SECRET": "jwt_secret" } #import values from env into flask config and do existence check for env_key, conf_key in env_to_conf.items(): x = os.getenv(env_key) if not x: msg = "Environment variable '%s' not set!" % env_key app.logger.fatal(msg) sys.exit(1) else: app.config[conf_key] = x #from flask_cors import CORS #falls cross-orgin verwendet werden soll #CORS(app) socket = SocketIO(app, cors_allowed_origins="*") @socket.on('connect') def sockcon(data): """ put every connection into it's own room to avoid broadcasting messages answer in callback only to room with sid """ room = request.sid + request.remote_addr join_room(room) socket.emit('backend response', {'msg': f'Connected to room {room} !', "room": room}) # looks like iOS needs an answer #TODO: pydantic message type validation @socket.on('client message') def handle_message(message): #try: room = message["room"] question = message["question"] system_prompt = message["system_prompt"] bot_id = message["bot_id"] #except: # return for chunk in ask_bot(system_prompt + " " + question, bot_id): socket.emit('backend token', {'data': chunk, "done": False}, to=room) socket.emit('backend token', {'done': True}, to=room) def hash_password(s: str) -> str: return hashlib.md5(s.encode('utf-8')).hexdigest() #======================= TAGS ============================= not_implemented_tag = Tag(name='Not implemented', description='Functionality not yet implemented beyond an empty response') debug_tag = Tag(name='Debug', description='Debug') bot_tag = Tag(name='Bot', description='Bot') user_tag = Tag(name='User', description='User') #==============Routes=============== class LoginRequest(BaseModel): email: str = Field(None, description='The users E-Mail that serves as nick too.') password: str = Field(None, description='A short text by the user explaining the rating.') @app.post('/user/login', summary="", tags=[user_tag]) def login(form: LoginRequest): """ Get your JWT to verify access rights """ if form.email is None or form.password is None: msg = "Invalid password!" app.logger.error(msg) return jsonify({ 'status': 'error', 'message': msg }), 400 client = Elasticsearch(app.config['elastic_uri']) match get_by_id(client, index="user", id_field_name="email", id_value=form.email): case []: msg = "User with email '%s' doesn't exist!" % form.email app.logger.error(msg) return jsonify({ 'status': 'error', 'message': msg }), 400 case [user]: if user["password_hash"] == hash_password(form.password + form.email): token = pyjwt.encode({"email": form.email}, app.config['jwt_secret'], algorithm="HS256") #app.logger.info(token) return jsonify({ 'status': 'success', 'jwt': token }) else: msg = "Invalid password!" app.logger.error(msg) return jsonify({ 'status': 'error', 'message': msg }), 400 from mail import send_mail class RegisterRequest(BaseModel): email: str = Field(None, description='The users E-Mail that serves as nick too.') password: str = Field(None, description='A short text by the user explaining the rating.') @app.post('/user/register', summary="", tags=[user_tag]) def register(form: RegisterRequest): """ Register an account """ if form.email is None or form.password is None: msg = "Parameters missing!" app.logger.error(msg) return jsonify({ 'status': 'error', 'message': msg }), 400 if User.get(id=form.email, ignore=404) is not None: return jsonify({ 'status': 'error', "message": "User with that e-mail address already exists!" }) else: user = User(meta={'id': form.email}) user.creation_date = datetime.now() user.email = form.email user.password_hash = hash_password(form.password + form.email) user.role = "User" user.isEmailVerified = False user.save() msg = """