diff --git a/backend/amdgpu-install_6.1.60103-1_all.deb b/backend/amdgpu-install_6.1.60103-1_all.deb deleted file mode 100644 index 7c0b124..0000000 Binary files a/backend/amdgpu-install_6.1.60103-1_all.deb and /dev/null differ diff --git a/backend/old_app.py b/backend/old_app.py deleted file mode 100644 index 4a95d00..0000000 --- a/backend/old_app.py +++ /dev/null @@ -1,1322 +0,0 @@ -""" -OpenAPI access via http://localhost/epdm/chat/bot/openapi/ on local docker-compose deployment - -""" -import warnings -warnings.filterwarnings("ignore") - -#std lib modules: -import os, sys, json -from typing import Any, Tuple, List, Dict, Any, Callable, Optional -from datetime import datetime, date -from collections import namedtuple -import hashlib, traceback, logging - -#ext libs: -import requests -import chardet -import codecs - -from elasticsearch import NotFoundError, Elasticsearch # for normal read/write without vectors -from elasticsearch_dsl import Search, A -from elasticsearch_dsl import Document, Date, Integer, Keyword, Float, Long, Text, connections - - -import openai #even used? -import tiktoken -from langchain.text_splitter import RecursiveCharacterTextSplitter -from langchain.chains import RetrievalQA - -#from langchain.callbacks import get_openai_callback -from langchain_community.callbacks import get_openai_callback - -from langchain_openai import ChatOpenAI, AzureChatOpenAI -from langchain_openai import OpenAIEmbeddings, AzureOpenAIEmbeddings -from langchain_community.vectorstores.elasticsearch import ElasticsearchStore - -#from langchain.document_loaders import PyPDFLoader, Docx2txtLoader -from langchain_community.document_loaders import PyPDFLoader, Docx2txtLoader - -from langchain.callbacks.base import BaseCallbackHandler, BaseCallbackManager -from langchain.prompts import PromptTemplate - - -from pydantic import BaseModel, Field - -#flask, openapi -from flask import Flask, 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 - -#home grown -from scraper import WebScraper -from funcs import group_by -from elastictools import update_by_id, delete_by_id - -#TODO: implement some kind of logging mechanism -#logging.basicConfig(filename='record.log', level=logging.DEBUG) -#logging.basicConfig(level=logging.DEBUG) -logging.basicConfig(level=logging.WARN) - -OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") -AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT") -OPENAI_DEPLOYMENT_NAME = os.getenv("OPENAI_DEPLOYMENT_NAME") -OPENAI_MODEL_NAME = os.getenv("OPENAI_MODEL_NAME") -OPENAI_API_VERSION = os.getenv("OPENAI_API_VERSION") -OPENAI_API_TYPE = os.getenv("OPENAI_API_TYPE") -OPENAI_EMBEDDING = os.getenv("OPENAI_EMBEDDING") -OPENAI_TEXT_EMBEDDING = os.getenv("OPENAI_TEXT_EMBEDDING") -LLM_PAYLOAD = int(os.getenv("LLM_PAYLOAD")) -CHUNK_SIZE = int(os.getenv("CHUNK_SIZE")) -BOT_ROOT_PATH = os.getenv("BOT_ROOT_PATH") - - - -# required settings - -assert OPENAI_API_KEY -assert BOT_ROOT_PATH - -from models import NextsearchLog - -info = Info(title="Bot-API", version="1.0.0", - summary="", - description="chatGPT model: " + OPENAI_MODEL_NAME) -servers = [ - Server(url=BOT_ROOT_PATH ) #+ '/') -] -app = OpenAPI(__name__, info=info, servers=servers) - -# init app and env -#app = Flask(__name__) -index_prefix = "chatbot" -app.config['UPLOAD_FOLDER'] = 'uploads' -app.config['CORS_HEADERS'] = 'Content-Type' -app.config['CORS_METHODS'] = ["GET,POST,OPTIONS,DELETE,PUT"] - -# set cors -CORS(app) - -env_to_conf = { - "BACKEND_INTERNAL_URL": "api_url", - "ELASTIC_URI": "elastic_uri" -} - -#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) - #raise Exception(msg) - sys.exit(1) - else: - app.config[conf_key] = x - - - -if OPENAI_API_TYPE == 'azure': - EMBEDDING = AzureOpenAIEmbeddings( - openai_api_key=OPENAI_API_KEY, - deployment=OPENAI_EMBEDDING, - model=OPENAI_TEXT_EMBEDDING, - openai_api_type=OPENAI_API_TYPE, - azure_endpoint=AZURE_OPENAI_ENDPOINT, - chunk_size = CHUNK_SIZE - ) -else: - EMBEDDING = OpenAIEmbeddings(openai_api_key=OPENAI_API_KEY) - - - -# socket = SocketIO(app, cors_allowed_origins="*", path="/epdm/chat/bot/socket.io") -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 - join_room(room) - socket.emit('my response', {'data': 'Connected'}) # looks like iOS needs an answer - -class StreamingCallback(BaseCallbackHandler): - # def __init__(self, key, sourceFileId): - def __init__(self, key: str, sid: str): - # print("StreamingCallback") - self.key = key - self.sid = sid - self.text = "" - self.new_sentence = "" - - def on_llm_new_token(self, token: str, **kwargs): - # print("on_llm_new_token", token) - self.text += token - self.new_sentence += token - socket.emit(self.key, self.text, to=self.sid) - - def on_llm_end(self, response, **kwargs): - # print("on_llm_end", response) - self.text = "" - socket.emit(self.key, "[END]", to=self.sid) - - -#----------------- - -class RatingRequest(BaseModel): - queryId: str = Field(None, description='The query-id. Example: fa9d2f024698b723931fe633bfe065d3') - rating: int = Field(0, ge=-1, le=1, description='A rating value of: -1, 0 or 1 for bad, neutral or good') - reason: str = Field("", description='A short text by the user explaining the rating.') - reasonTags: List[str] = Field([], description='A list of tags flagging the rating. Examples: ["LAZY", "CLEVER", "COOL", "VILE", "BUGGED", "WRONG"]') #limit tag len to 64 chars - -@app.post('/bot/rating', summary="Allows for answers to be rated", tags=[]) -def rating(body: RatingRequest): - """ - Gives a rating to an answer. - """ - queryId = body.queryId - rating = body.rating - reason = body.reason - reasonTags = body.reasonTags - - client = Elasticsearch(app.config['elastic_uri']) - - try: - update_by_id(client, index="nextsearch_log", id_field_name="queryid", id_value=queryId, values_to_set={ - "rating": rating, - "reason": reason, - "reasonTags": reasonTags - }) - except NotFoundError: - return jsonify({ - 'status': 'error', - 'message': "Unknown id: '%s!'" % queryId - }), 404 - - return "", 201 - - -#-------------- - -def get_slugs_for_names(client: Elasticsearch): - s = Search(using=client, index="chatbot") - s = s[0:10000] - response = s.execute() - return { d["slug"]: d["chatbotName"] for d in (x.to_dict() for x in response.hits)} - - -#DEAD? -class BarChartRequest(BaseModel): - chatbots: List[str] = Field([], description="""A list of chatbot names to filter for""") - start: datetime = Field("2000-01-31T16:47+00:00", description="""The interval start datetime in ISO 8601 format""") - end: datetime = Field("2100-01-31T16:47+00:00", description="""The interval end datetime in ISO 8601 format""") - -@app.get('/bot/usage/activityPerDay', summary="", tags=[]) -def usage_activity_per_day(query: BarChartRequest): - """ - - """ - chatbots = query.chatbots - start = query.start - end = query.end - - - client = Elasticsearch(app.config['elastic_uri']) - - id2name = get_slugs_for_names(client) - - s = Search(using=client, index="nextsearch_log") \ - .filter("range", timest={"gte": start}) \ - .filter("range", timest={"lte": end}) - - s = s[0:10000] #if not used size is set to 10 results - - def maybe_id2name(id): - if id in id2name: - return id2name[id] - return id - - def agg_pretty_result(d): - ls = [] - for bucket in d["aggregations"]["day"]["buckets"]: - d = {} - d["date"] = bucket["key_as_string"] - d["bots"] = [] - for bot in bucket["chatbot"]["buckets"]: - d["bots"].append({ - "bot": maybe_id2name(bot["key"]), - "cost": bot["cost_per_bot"]["value"] - }) - ls.append(d) - return ls - - - s.aggs.bucket('day', 'terms', field='date') - s.aggs['day'].bucket('chatbot', 'terms', field='chatbotid').metric('cost_per_bot', 'sum', field='inCt') - - response = s.execute() - - r = response.to_dict() - del r["hits"] - - ls = agg_pretty_result(r) - - return jsonify(ls) - -#-------------- - - -#book_tag = Tag(name="book", description="Some Book") - -class DonutChartRequest(BaseModel): - chatbots: List[str] = Field([], description="""A list of chatbot names to filter for""") - start: datetime = Field("2000-01-31T16:47+00:00", description="""The interval start datetime in ISO 8601 format""") - end: datetime = Field("2100-01-31T16:47+00:00", description="""The interval end datetime in ISO 8601 format""") - -@app.get('/bot/usage/activity', summary="Takes an interval and gives back a summary of bots and their activity and cost.", tags=[]) -def usage_activity(query: DonutChartRequest): - """ - Use datetime in ISO 8601 format: 2007-08-31T16:47+00:00 - """ - chatbots = query.chatbots - start = query.start - end = query.end - - #group nextsearch_log by chatbotid and sum inCt - - client = Elasticsearch(app.config['elastic_uri']) - - id2name = get_slugs_for_names(client) - - s = Search(using=client, index="nextsearch_log") \ - .filter("range", timest={"gte": start}) \ - .filter("range", timest={"lte": end}) - - s = s[0:10000] #if not used size is set to 10 results - - a = A('terms', field='chatbotid') \ - .metric('cost_per_bot', 'sum', field='inCt') - - s.aggs.bucket('bots', a) - response = s.execute() - #print(response.aggregations.bots.buckets) - - def maybe_id2name(id): - if id in id2name: - return id2name[id] - return id - - match chatbots: - case []: - #ls = [d for d in (d.to_dict() for d in response.aggregations.bots.buckets)] - ls = [{**d, "chatbotname": maybe_id2name(d["key"].split("_")[0])} for d in (d.to_dict() for d in response.aggregations.bots.buckets)] - case _: - ls = [{**d, "chatbotname": maybe_id2name(d["key"].split("_")[0])} for d in (d.to_dict() for d in response.aggregations.bots.buckets) if d["key"] in id2name and id2name[d["key"]] in chatbots] - - d = { - "chart": { - "series": { - "data": ls - } - } - } - - return jsonify(d) - #return jsonify(ls) - #return jsonify(list(response.aggregations.bots.buckets)) - -#------------------ - -class RetrieveChatRequest(BaseModel): - sessionId: str = Field(None, description="""The session's id. Example: d73bccba29b6376c1869944f26c3b670""") - -@app.get('/bot/usage/conversation', summary="Takes a session-id and gives you all of it's content.", tags=[]) -def usage_conversation(query: RetrieveChatRequest): - """ - Example session-id: d73bccba29b6376c1869944f26c3b670 - """ - sessionId = query.sessionId - - client = Elasticsearch(app.config['elastic_uri']) - - s = Search(using=client, index="nextsearch_log") \ - .filter("term", session=sessionId) - - s = s[0:10000] #if not used size is set to 10 results - response = s.execute() - return jsonify([hit.to_dict() for hit in response]) - -#------------ - -class DialogTableRequest(BaseModel): - chatbots: List[str] = Field([], description="""A list of chatbot names to filter for""") - start: datetime = Field("2000-01-31T16:47+00:00", description="""The interval start datetime in ISO 8601 format""") - end: datetime = Field("2100-01-31T16:47+00:00", description="""The interval end datetime in ISO 8601 format""") - -@app.get('/bot/usage/conversations', summary="Takes an interval and gives you all chatbots and their sessions within.", tags=[]) -def usage_conversations(query: DialogTableRequest): - """ - Use datetime in ISO 8601 format: 2007-08-31T16:47+00:00 - """ - #GET /bot/usage/conversations?chatbots=robobot,cyberbot,gigabot&timeStart=2024-01-15&timeEnd=2024-01-15&timeStart=00:00&timeEnd=23:00 - chatbots = query.chatbots - start = query.start - end = query.end - - client = Elasticsearch(app.config['elastic_uri']) - - s = Search(using=client, index="nextsearch_log") \ - .filter("range", timest={"gte": start}) \ - .filter("range", timest={"lte": end}) - - s = s[0:10000] #if not used size is set to 10 results - - #a = A('terms', field='chatbotid') \ - # .metric('cost_per_bot', 'sum', field='inCt') - #s.aggs.bucket('bots', a) - - response = s.execute() - hits = (x.to_dict() for x in response.hits) - - id2name = get_slugs_for_names(client) - - match chatbots: - case []: - pass - case _: - - hits = filter(lambda d: (id2name[d["chatbotid"]] in chatbots) if d["chatbotid"] in id2name else False, hits) - - - d = group_by([lambda d: d["chatbotid"], lambda d: d["session"] ], hits) - - d2 = {} - for chatbotid, v in d.items(): - if chatbotid in id2name: - d2[id2name[chatbotid]] = v - - return jsonify(d2) - - -#------------------ - -class ExtractUrlRequest(BaseModel): - url: str = Field(None, description="""The URL to a website whose HTML-embedded URLs you'd like to have.""", strict=True) - -@app.post('/bot/extract-url', summary="Get URLs from a website via its URL", tags=[]) -def extract_url(body: ExtractUrlRequest): - """ - Takes a json of form {"url": "..."} and gives back a list of URLs found within the specified URL's HTML-sourcecode. - """ - url = body.url - if not url: - return jsonify({'status': 'error', 'message': 'Missing required parameter url!'}), 400 - - with WebScraper() as web_scraper: - return jsonify(web_scraper.extract_urls(url)) - -#------------------ - -def extract_data(links: List[Dict[str, str]]) -> List[Dict[str, str]]: - """ - Webscrape pages of the given links and return a list of texts - """ - with WebScraper() as web_scraper: - return web_scraper.extract_page_data(links) - - -def get_word_splits(word_file: str) -> List: - loader = Docx2txtLoader(word_file) - pages = loader.load_and_split() - txt_spliter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100, length_function=len) - doc_list = [] - for page in pages: - pg_splits = txt_spliter.split_text(page.page_content) - doc_list.extend(pg_splits) - return doc_list - - -def get_text_splits_from_file(text_file: str) -> List: - # Detect the file's encoding - with open(text_file, 'rb') as file: - encoding_result = chardet.detect(file.read()) - - # Use the detected encoding to read the file - detected_encoding = encoding_result['encoding'] - with codecs.open(text_file, 'r', encoding=detected_encoding, errors='replace') as file: - text = file.read() - - return get_text_splits(text) - - -def determine_index(chatbot_id: str) -> str: - return f"{index_prefix}_{chatbot_id.lower()}" - - -def embed_index(doc_list: List[Dict[str, str]], chatbot_id: str) -> None: - """ - Add source documents in chatbot_xyz index! - """ - index = determine_index(chatbot_id) - - #print(f"add documents to index {index}", flush=True) - app.logger.info(f"add documents to index {index}") - - - #ElasticsearchStore.from_documents(doc_list, EMBEDDING, index_name=index, es_url=elastic_uri) - ElasticsearchStore.from_documents(doc_list, EMBEDDING, index_name=index, es_url=app.config['elastic_uri']) - - - - - - -class TrainForm(BaseModel): - #url: str = Field(None, description="""""", strict=True) - chatbotSlug: str = Field(None, description="""""") - files: List[FileStorage] = Field(None, description="""Some files""") - text: str = Field(None, description="Some text") - #filesMetadata: List[Dict[str, str]] = Field(None, description="""""") - - filesMetadata: str = Field(None, description="""A JSON string""") #a json string: [ ... ] - links: str = Field(None, description="""A JSON string""") #a json? [ ... ] - - -#TODO: needs to be reimplemented with another mechanism like celeery to manage longer running tasks and give feedback to frontend -@app.post('/bot/train', summary="", tags=[]) -def upload(form: TrainForm): - """ - Caution: Long running request! - """ - #url = body.url - - #print(form.file.filename) - #print(form.file_type) - #form.file.save('test.jpg') - - #app.logger.info("TRAIN called!") - - # extract body - chatbot_id = form.chatbotSlug - files = form.files - text = form.text - files_metadata = form.filesMetadata - if files_metadata: - files_metadata = json.loads(files_metadata) - links = form.links - if links: - links = json.loads(links) #[{url: '...'}] ? - app.logger.debug(links) - - - # validate body - if not chatbot_id: - return jsonify({ - 'status': 'error', - 'message': 'chatbotId is required' - }), 400 - - if not files_metadata and not text and not links: - return jsonify({ - 'status': 'error', - 'message': 'No data source found' - }), 400 - - if files_metadata and len(files) != len(files_metadata): - return jsonify({ - 'status': 'error', - 'message': 'Number of uploaded files metadata and files should be same' - }), 400 - - if links and len(links) == 0: - return jsonify({ - 'status': 'error', - 'message': 'No links found' - }), 400 - - - - try: - - # store raw data and extract doc_list - os.makedirs(f"{app.config['UPLOAD_FOLDER']}/{chatbot_id}", exist_ok=True) - - #train with given files - for i, file in enumerate(files): - filename = files_metadata[i]["slug"] + "_" + secure_filename(file.filename) - file_path = os.path.join(app.config['UPLOAD_FOLDER'], chatbot_id, filename) - file.save(file_path) - - app.logger.info("File saved successfully!") - - doc_list = [] - match file.filename.split(".")[-1]: - case "pdf": - doc_list = get_pdf_splits(file_path) - doc_list = add_metadata( - doc_list=doc_list, - source_type="pdf_file", - chatbot_id=chatbot_id, - source_file_id=files_metadata[i]["slug"], - filename=file.filename - ) - case "txt": - doc_list = get_text_splits_from_file(file_path) - doc_list = add_metadata( - doc_list=doc_list, - source_type="text_file", - chatbot_id=chatbot_id, - source_file_id=files_metadata[i]["slug"], - filename=file.filename - ) - case "docx" | "doc": - doc_list = get_word_splits(file_path) - doc_list = add_metadata( - doc_list=doc_list, - source_type="word_file", - chatbot_id=chatbot_id, - source_file_id=files_metadata[i]["slug"], - filename=file.filename - ) - case _: - app.logger.error("Unknown file extension: '%s'!" % file.filename.split(".")[-1]) - - - # embed file doc_list - embed_index(doc_list=doc_list, chatbot_id=chatbot_id) - - #train with given text - if text: - doc_list = get_text_splits(text) - doc_list = add_metadata( - doc_list=doc_list, - source_type="text", - chatbot_id=chatbot_id, - source_file_id="text", - txt_id=hashlib.md5(text.encode()).hexdigest() - ) - - # embed raw text doc_list - embed_index(doc_list=doc_list, chatbot_id=chatbot_id) - - #train with given links - if links and len(links) > 0: - links_docs = extract_data(links) - for i, doc in enumerate(links_docs): - if not doc['text']: - app.logger.info(f"Document {i} '{doc['url']} of {len(links_docs)} doesn't contain text. Skip.") - - else: - app.logger.info(f"embed document {i + 1} '{doc['url']}' of {len(links_docs)}") - - doc_list = get_text_splits(doc["text"], "link") - doc_list = add_metadata(doc_list, "link", chatbot_id, doc["slug"], url=doc["url"]) - - #TODO: save url with source! - - - # embed html doc_list - embed_index(doc_list=doc_list, chatbot_id=chatbot_id) - - #TODO: js backend needs to be merged into this one - # ping status endpoint - - express_api_endpoint = f"{app.config['api_url']}/api/chatbot/status/{chatbot_id}" - - #express_api_endpoint = f"{api_url}/api/chatbot/status/{chatbot_id}" - - - - try: - response = requests.put(express_api_endpoint, json={'status': 'ready'}) - - if response.status_code == 200: - app.logger.info("Express API updated successfully!") - else: - app.logger.error(f"Failed to update Express API {express_api_endpoint}") - - except Exception as e: - app.logger.error(f"Failed to update Express API {express_api_endpoint}") - app.logger.error(e) - - - return 'Files uploaded successfully' - except Exception as e: - app.logger.error(e) - - #TODO: log traceback! - traceback.print_exc() - return jsonify({'status': 'error', 'message': 'Something went wrong!'}), 400 - - -#------------------ - -class ReviseAnswerRequest(BaseModel): - revisedText: str = Field(None, description="""The new revised text""") - chatbotSlug: str = Field(None, description="""The chatbot id""") - -@app.post('/bot/revise-answer', summary="", tags=[]) -def revise2(body: ReviseAnswerRequest): - """ - - """ - revised_text = body.revisedText - chatbot_id = body.chatbotSlug - - if not revised_text: - return jsonify({ - 'status': 'error', - 'message': 'Missing required parameter revisedText!' - }), 400 - - if not chatbot_id: - return jsonify({ - 'status': 'error', - 'message': 'Missing required parameter chatbotSlug!' - }), 400 - - doc_list = get_text_splits(revised_text) - doc_list = add_metadata(doc_list, "revised_text", chatbot_id, "text") - embed_index(doc_list=doc_list, chatbot_id=chatbot_id) - return jsonify({ - 'status': 'success', - 'message': 'Answer revised successfully!' - }) - -#------------------ - -def clean_history(hist: List[Dict[str, str]]) -> str: - out = '' - for qa in hist[-5:]: # only the last 5 - if len(qa['bot']) < 2: - continue - out += 'user: ' + qa['user'] + '\nassistant: ' + qa['bot'] + "\n\n" - return out - - - -def get_prices(model_name) -> Dict[str, float]: - """ - prices in Ct. per 1000 tokens - """ - match model_name: - - # Azure OpenAI - case 'gpt-35-turbo': - inCt = 0.15 - outCt = 0.2 - - # OpenAI - case 'gpt-3.5-turbo-16k': - inCt = 0.3 - outCt = 0.4 - - case 'gpt-3.5-turbo-0125': - inCt = 0.05 - outCt = 0.15 - - case 'gpt-4': - inCt = 3.0 - outCt = 6.0 - - case 'gpt-4-32k': - inCt = 6.0 - outCt = 12.0 - - case 'gpt-4-0125-preview': - inCt = 1.0 - outCt = 3.0 - - case _: - inCt = 1.0 - outCt = 1.0 - - return { - "inCt": inCt, - "outCt": outCt - } - - - -def query_log(chatbot_id, queryId, sess, temperature, q, a, rating, llm, dura, sources, inputTokens, inCt, outputTokens, outCt): - """ - Add a doc to nextsearch_log - """ - - connections.create_connection(hosts=app.config['elastic_uri']) - - # create the mappings in elasticsearch - NextsearchLog.init() - - totalCt = ((inputTokens / 1000) * inCt) + ((outputTokens / 1000) * outCt) - esdoc = { - 'queryid': queryId, - 'chatbotid': chatbot_id, - 'timest': datetime.now(), - - 'date': date.today().isoformat(), - - 'session': sess, - 'temperature': temperature, - 'q': q, - 'a': a, - 'rating': rating, - 'reason': '', - 'reasontags': '', - 'llm': llm, - 'durasecs': dura, - 'sources': sources, - 'inToks': inputTokens, - 'inCt': inCt, - 'outToks': outputTokens, - 'outCt': outCt, - 'totalCt': totalCt - } - - client = Elasticsearch(app.config['elastic_uri']) - - resp = client.index(index='nextsearch_log', document=esdoc) - #TODO: check resp for success - - #print(resp) - app.logger.info(resp) - - return resp - - - - -def get_llm(temperature: float, stream_key: str, sid: str): - """ - Get the right LLM - """ - if OPENAI_API_TYPE == 'azure': - llm = AzureChatOpenAI( - openai_api_version=OPENAI_API_VERSION, - deployment_name=OPENAI_DEPLOYMENT_NAME, - azure_endpoint=AZURE_OPENAI_ENDPOINT, - - openai_api_key=OPENAI_API_KEY, - model_name=OPENAI_MODEL_NAME, - temperature=temperature, - streaming=True, - callbacks=BaseCallbackManager([StreamingCallback(stream_key, sid)]) - ) - else: - llm = ChatOpenAI( - openai_api_key=OPENAI_API_KEY, - model_name=OPENAI_MODEL_NAME, - temperature=temperature, - streaming=True, - callbacks=BaseCallbackManager([StreamingCallback(stream_key, sid)]) - ) - - return llm - - -class QueryRequest(BaseModel): - queryId: str = Field("", description="""The query id""") #generated by the js backend atm - key: str = Field("", description="""String used for the streaming of the chat""") - - prompt: str = Field(None, description="""The prompt/question to the bot""") - history: List[Dict[str,str]] = Field([], description="""""") - chatbotSlug: str = Field(None, description="""The chatbot id. Example: 'MyBot_c2wun1'""") - temprature: float = Field(0.1, description="""The temperature value passed to OpenAI affecting the strictness of it#s answers""") - sid: str = Field(None, description="""String used for the streaming of the chat""") - systemPrompt: str = Field("Antworte freundlich, mit einer ausführlichen Erklärung, sofern vorhanden auf Basis der folgenden Informationen. Please answer in the language of the question.", description="""A prompt always contextualizing the query used""") - - -@app.post('/bot/query', summary="Query the bot via prompt", tags=[]) -def bot_query(body: QueryRequest): - """ - The main route to use the chatbots LLM with a given prompt string, temperature, system prompt and history context - """ - dura = datetime.now().timestamp() - - queryId = body.queryId - prompt = body.prompt - history = clean_history(body.history) - chatbot_id = body.chatbotSlug - system_prompt = body.systemPrompt - temperature = body.temprature #typo in 'temprature' instead of is key temperature - key = body.key - sid = body.sid - - stream_key = key if key else f"{chatbot_id}_stream" - - sess = str(request.user_agent) + ' ' + str(request.environ.get('HTTP_X_REAL_IP', request.remote_addr)) +' '+ str(request.remote_addr) - sessMD5 = hashlib.md5(sess.encode()).hexdigest() - - #TODO: we need a better way to create these ids... it seems kind of random - if (queryId == None) or (queryId == ''): - queryId = sessMD5 - - - encoding = tiktoken.encoding_for_model(OPENAI_MODEL_NAME) - - if not chatbot_id: - return jsonify({ - 'status': 'error', - 'message': 'Missing required parameter chatbotSlug!' - }), 400 - if not prompt: - return jsonify({ - 'status': 'error', - 'message': 'Missing required parameter prompt!' - }), 400 - if not sid: - return jsonify({ - 'status': 'error', - 'message': 'Missing required parameter sid in query!' - }), 400 - - - default_temperature = 0.1 - temperature = temperature if temperature is not None else default_temperature - - llm = get_llm(temperature, stream_key, sid) - - prompt_template = system_prompt + """ - - {context} - - - """ + history + """ - - Question: {question} - """ - - chat_prompt = PromptTemplate( - template=prompt_template, input_variables=["context", "question"] - ) - - index = determine_index(chatbot_id) - - db = ElasticsearchStore( - es_url=app.config['elastic_uri'], - index_name=index, - distance_strategy="COSINE", - embedding=EMBEDDING - ) - - k = int(LLM_PAYLOAD / CHUNK_SIZE) - 1 - if (k < 2): - k = 2 - - scoredocs = db.similarity_search_with_score(prompt, k=k+10) - - query = RetrievalQA.from_chain_type( - llm=llm, - chain_type="stuff", - verbose=False, - return_source_documents=True, - retriever=db.as_retriever(search_kwargs={'k': k}), - chain_type_kwargs={"prompt": chat_prompt} - ) - - inputTokens = 0 - outputTokens = 0 - - with get_openai_callback() as cb: - qares = query.invoke({'query': prompt}) - qadocs = qares['source_documents'] #TODO: STS: deliver doc names and page numbers in the future - inputDocTxt = '' - - sources = [] - count = 0 - for qadoc in qadocs: - mdata = qadoc.metadata - if 'chatbotId' in mdata: - del mdata['chatbotId'] - - nextScore = 0.0 - for scoredoc in scoredocs: - if (len(qadoc.page_content) > 20) and (len(scoredoc[0].page_content) > 20) and (qadoc.page_content[:20] == scoredoc[0].page_content[:20]): - nextScore = scoredoc[1] - inputDocTxt += ' ' + qadoc.page_content - break - - # Lets make Percent of the score, only look at 0.6-1.0 - nextScore = float((nextScore - 0.6) * 250) - if nextScore < 1.0: - nextScore = 1.0 - if nextScore > 99.99: - nextScore = 99.99 - - mdata['score'] = round(nextScore, 2) - sources.append(mdata) - count += 1 - - answer = qares['result'] - #print(f"Total Tokens: {cb.total_tokens}") - #print(f"Prompt Tokens: {cb.prompt_tokens}") - #print(f"Completion Tokens: {cb.completion_tokens}") - - app.logger.info("ANSWER: " + answer) - - #print(ans, flush=True) - - inputTokens = len(encoding.encode(inputDocTxt + ' ' + prompt_template)) - outputTokens = len(encoding.encode(answer)) - - app.logger.info(f"Input Tokens: {inputTokens}") - app.logger.info(f"Output Tokens: {outputTokens}") - app.logger.info(f"Total Cost (USD): ${cb.total_cost}") - - - - d = get_prices(OPENAI_MODEL_NAME) - inCt = d["inCt"] - outCt = d["outCt"] - - # log question/answer - dura = round(datetime.now().timestamp() - dura, 2) - resp = query_log(chatbot_id, - queryId, - sessMD5, - temperature, - prompt, - answer, - 0, - OPENAI_MODEL_NAME, - dura, - sources, - inputTokens, - inCt, - outputTokens, - outCt) - - app.logger.info(resp) - - sources_index = "chatbot_" + chatbot_id - - client = Elasticsearch(app.config['elastic_uri']) - - s = Search(using=client, index=sources_index) - s = s[0:10000] - response = s.execute() - srcs = (x.to_dict() for x in response.hits) - src_grps = group_by([lambda d: d["metadata"]["sourceType"] ], srcs) - - #def print_type(x): - - new_sources = [] - for source in sources: - app.logger.info("Source: " + repr(source)) - match source["sourceType"]: - case "text": - if "txt_id" in source: - source["text"] = "" - d2 = group_by([lambda d: d["metadata"]["txt_id"] ], src_grps["text"]) - for src_item in d2[source["txt_id"]]: - source["text"] += " " + src_item["text"] - - new_sources.append(source) - - case "link": - if "sourceFileId" in source: - source["text"] = "" - d2 = group_by([lambda d: d["metadata"]["sourceFileId"] ], src_grps["link"]) - for src_item in d2[source["sourceFileId"]]: - source["text"] += " " + src_item["text"] - if "url" in src_item: - source["url"] = src_item["url"] - - new_sources.append(source) - - case "file": - if "sourceFileId" in source: - source["text"] = "" - d2 = group_by([lambda d: d["metadata"]["sourceFileId"] ], src_grps["file"]) - for src_item in d2[source["sourceFileId"]]: - source["text"] += " " + src_item["text"] - if "filename" in src_item: - source["filename"] = src_item["filename"] - - new_sources.append(source) - - - if resp.body["result"] == "created": - return jsonify({ - 'status': 'success', - 'answer': answer, - 'query_id': queryId, - 'sources': new_sources #sources - }) - else: - return jsonify({ - 'status': 'error', - 'message': resp.body["result"] - }), 400 - - -#------------------ - -#TODO create separate delete bot route - -class DeleteBotRequest(BaseModel): - chatbot_id: str = Field(None, description="""Chatbot id""") - -@app.delete('/bot', summary="", tags=[]) -def delete_bot(body: DeleteBotRequest): - """ - Not implemented yet - - Delete a chatbot via it's id - """ - chatbot_id = body.chatbot_id - - # Ensure chatbotId is provided - if not chatbot_id: - app.logger.error('Missing required parameter chatbotSlug!') - return jsonify({ - 'status': 'error', - 'message': 'Missing required parameter chatbotSlug!' - }), 400 - - client = Elasticsearch(app.config['elastic_uri']) - id2name = get_slugs_for_names(client) - index = determine_index(chatbot_id) - - - if not chatbot_id in id2name: - app.logger.error("Missing associated chatbot name of this id: '%s'!" % chatbot_id) - return jsonify({ - 'status': 'error', - 'message': 'Chatbot id not found!' - }), 404 - else: - chatbot_name = id_value=id2name[chatbot_id] - - - #TODO: delete index chatbot_ - try: - client.indices.delete(index=index) - app.logger.info("Deleted index '%s' !" % index) - except: - app.logger.error("Could not delete index '%s' !" % index) - - - #TODO: delete associated doc from index chatbot - #try: - delete_by_id(client, index="chatbot", id_field_name="slug", id_value=chatbot_id) - # app.logger.info("Deleted chatbot '%s' data from index '%s' !" % (chatbot_id, "chatbot")) - #except: - # app.logger.error("Could not delete data for '%s' in index 'chatbot' !" % chatbot_id) - - - #TODO: delete associated doc from index settings - #try: - delete_by_id(client, index="settings", id_field_name="displayName", id_value=chatbot_name) - # app.logger.info("Deleted chatbot '%s' data from index '%s' !" % (id2name[chatbot_id], "settings")) - #except: - # app.logger.error("Could not delete data for '%s' in index 'settings' !" % id2name[chatbot_id]) - - - #TODO: delete associated doc from index nextsearch_log - #try: - delete_by_id(client, index="nextsearch_log", id_field_name="chatbotid", id_value=chatbot_id) - # app.logger.info("Deleted chatbot '%s' data from index '%s' !" % (chatbot_id, "nextsearch_log")) - #except: - # app.logger.error("Could not delete data for '%s' in index 'nextsearch_log' !" % chatbot_id) - - return "", 202 - - -#------------------ - -#TODO: overloaded route... split into two or three, one for each resource -#server/routes/api/chatbot.js - -#FE calls js BE: /api/chatbot/resources/abotycrsh1 -#which calls bot BE - - - -class DeleteResourceRequest(BaseModel): - sourceType: str = Field(None, description="""Source type: ...link, text, file ?""") - sourceId: str = Field(None, description="""Source id?""") - chatbotSlug: str = Field(None, description="""Chatbot id""") - -@app.delete('/bot/resources', summary="delete a bot or resource via it's id", tags=[]) -def delete_resource(body: DeleteResourceRequest): - """ - * delete a bot via it's id - * delete files used as training source - - or other resources... unclear atm - """ - source_type = body.sourceType - source_id = body.sourceId - chatbot_id = body.chatbotSlug - - # Validate presence of sourceType - if not source_type: - msg = 'sourceType is required!' - app.logger.error(msg) - return jsonify({ - 'status': 'error', - 'message': msg - }), 400 - - # Ensure chatbotId is provided - if not chatbot_id: - app.logger.error('Missing required parameter chatbotSlug!') - return jsonify({ - 'status': 'error', - 'message': 'Missing required parameter chatbotSlug!' - }), 400 - - # Apply criteria based on sourceType - filter_criteria = { - "bool": { - "must": [ - {"match": {"metadata.sourceType": source_type}}, - {"match": {"metadata.chatbotId": chatbot_id}}, - ] - } - } - if source_type != 'text': - if not source_id: - app.logger.error('Missing required parameter sourceId!') - return jsonify({ - 'status': 'error', - 'message': 'Missing required parameter sourceId!' - }), 400 - new_match: Dict[str, Dict[str, Any]] = { - "match": { - "metadata.sourceFileId": source_id - } - } - filter_criteria["bool"]["must"].append(new_match) - - try: - # Assuming delete method returns a status or raises an exception on failure - app.logger.info(filter_criteria) - - index = determine_index(chatbot_id) - - store = ElasticsearchStore( - es_url=app.config['elastic_uri'], - index_name=index, - embedding=EMBEDDING - ) - - store.client.delete_by_query(index=index, query=filter_criteria) - - # isDeleted = index.delete(filter=filter_criteria) - except Exception as e: - #TODO: Handle specific exceptions if possible - - app.logger.error(str(e)) - - return jsonify({ - 'status': 'error', - 'message': str(e) - }), 500 - - - msg = 'Resource deleted successfully!' - app.logger.info(msg) - return jsonify({ - 'status': 'success', - 'message': msg - }) - - -#------------------ - - -# Splits the text into small chunks of 150 characters -def get_pdf_splits(pdf_file: str) -> List: - loader = PyPDFLoader(pdf_file) - pages = loader.load_and_split() - text_split = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100, length_function=len) - doc_list = [] - for pg in pages: - pg_splits = text_split.split_text(pg.page_content) - doc_list.extend(pg_splits) - return doc_list - - -def get_text_splits(text: str, source: str="text") -> List: - chunk_size = 1536 - chunk_overlap = 200 - - #if source == "link": - # chunk_size = 1536 - # chunk_overlap = 200 - - text_split = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap, length_function=len) - doc_list = text_split.split_text(text) - return doc_list - - -ESDocument = namedtuple('Document', ['page_content', 'metadata']) - -def add_metadata(doc_list: List[str], source_type: str, chatbot_id: str, source_file_id, tags=[], url=None, filename=None, txt_id=None) -> List[ESDocument]: - """ - - """ - for i, doc in enumerate(doc_list): - # If doc is a string, convert it to the Document format - if isinstance(doc, str): - doc = ESDocument(page_content=doc, metadata={}) - doc_list[i] = doc - - # Update the metadata - updated_metadata = doc.metadata.copy() - updated_metadata["chatbotId"] = chatbot_id - updated_metadata["tags"] = ' | '.join(tags) - - match source_type: - case "text": - updated_metadata["sourceType"] = "text" - if txt_id is not None: - updated_metadata["txt_id"] = txt_id - - case "revised_text": - updated_metadata["sourceType"] = "revised_text" - - case "pdf_file" | "word_file" | "text_file": - updated_metadata["sourceType"] = "file" - updated_metadata["sourceFileId"] = source_file_id - if filename is not None: - updated_metadata["filename"] = filename - - case "link": - updated_metadata["sourceType"] = "link" - updated_metadata["sourceFileId"] = source_file_id - if url is not None: - updated_metadata["url"] = url - - # Update the document in the doc_list with new metadata - doc_list[i] = ESDocument( - page_content=doc.page_content, - metadata=updated_metadata - ) - - return doc_list - - - -@app.errorhandler(500) -def server_error(error): - app.logger.exception('An exception occurred during a request: ' + str(error)) - return 'Internal Server Error', 500 - - - -#JS Backend routes to reimplement: -# http://localhost:8000/api/chatbot/add-resources - -if __name__ == '__main__': - app.run(debug=True, host="0.0.0.0", port=5000) - - -