├── static ├── data │ ├── dbs │ │ └── info.txt │ └── simulacra.json ├── chat1.png ├── chat2.png ├── chat3.png ├── alp-hub.png ├── alp-session-mgr.png ├── scripts │ ├── home.js │ └── chat.js └── styles │ └── style.css ├── requirements.txt ├── lib ├── params.py ├── ai_tools.py ├── chatbot.py ├── cont_proc.py └── db_handler.py ├── templates ├── home.html ├── summary.html ├── index.html ├── collection_manager.html └── session_manager.html ├── .gitignore ├── README.md └── alp.py /static/data/dbs/info.txt: -------------------------------------------------------------------------------- 1 | dir to store .db file -------------------------------------------------------------------------------- /static/chat1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rpast/ALP/HEAD/static/chat1.png -------------------------------------------------------------------------------- /static/chat2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rpast/ALP/HEAD/static/chat2.png -------------------------------------------------------------------------------- /static/chat3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rpast/ALP/HEAD/static/chat3.png -------------------------------------------------------------------------------- /static/alp-hub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rpast/ALP/HEAD/static/alp-hub.png -------------------------------------------------------------------------------- /static/alp-session-mgr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rpast/ALP/HEAD/static/alp-session-mgr.png -------------------------------------------------------------------------------- /static/data/simulacra.json: -------------------------------------------------------------------------------- 1 | { 2 | "assistant": { 3 | "role": "system", 4 | "content": "You are a helpful assistant. You provide only factual information based on the provided context.\nWhen you do not know the answer, you say it.\nWherever you can, you provide source name and page numbers in brackets (SRC: , PAGE: ).\nI will provide my query after INP tag. I will provide context you will use in your answer after the following tags: \nSRC - context from source text we are talking about; \nQRY - one of previous inputs from current conversation that may be relevant to the current INP; \nRPL - one of your previous replies from current conversation that may be relevant to current INP." 5 | }, 6 | 7 | "robb": { 8 | "role": "system", 9 | "content": "You play the role of Rob Burbea (RB), a meditation teacher.\nYou act as if we are on a retreat and I am asking you questions about the source material during one on one interview session.\nYour answers are concise and adjusted to the context of the conversation.\nYour aim is to help me refine my spiritual practice and understanding of the teachings.\nMimick Rob's style of speech and use his vocabulary.\nMimick Rob's unique empathy and care for others.\n Supplement your answer with pointers to source materials you used to formulate it. Do it at the end of your reply. Use this template: (SRC: , PAGE: ).\nMy question will come after INP tag. The context coming from Rob's and other spiritual material will be provided after SRC tag.\nPrevious inputs and replies from current conversation that may be relevant to the current INP will be provided after QRY and RPL tags respectively." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.4 2 | aiosignal==1.3.1 3 | async-timeout==4.0.2 4 | attrs==23.1.0 5 | blinker==1.6.2 6 | certifi==2023.5.7 7 | charset-normalizer==3.1.0 8 | click==8.1.3 9 | cmake==3.26.4 10 | dataclasses-json==0.5.9 11 | filelock==3.12.2 12 | Flask==2.3.2 13 | frozenlist==1.3.3 14 | fsspec==2023.6.0 15 | greenlet==2.0.2 16 | huggingface-hub==0.16.2 17 | idna==3.4 18 | itsdangerous==2.1.2 19 | Jinja2==3.1.2 20 | joblib==1.3.1 21 | langchain==0.0.224 22 | langchainplus-sdk==0.0.20 23 | lit==16.0.6 24 | MarkupSafe==2.1.3 25 | marshmallow==3.19.0 26 | marshmallow-enum==1.5.1 27 | mpmath==1.3.0 28 | multidict==6.0.4 29 | mypy-extensions==1.0.0 30 | networkx==3.1 31 | nltk==3.8.1 32 | numexpr==2.8.4 33 | numpy==1.25.0 34 | nvidia-cublas-cu11==11.10.3.66 35 | nvidia-cuda-cupti-cu11==11.7.101 36 | nvidia-cuda-nvrtc-cu11==11.7.99 37 | nvidia-cuda-runtime-cu11==11.7.99 38 | nvidia-cudnn-cu11==8.5.0.96 39 | nvidia-cufft-cu11==10.9.0.58 40 | nvidia-curand-cu11==10.2.10.91 41 | nvidia-cusolver-cu11==11.4.0.1 42 | nvidia-cusparse-cu11==11.7.4.91 43 | nvidia-nccl-cu11==2.14.3 44 | nvidia-nvtx-cu11==11.7.91 45 | openai==0.27.8 46 | openapi-schema-pydantic==1.2.4 47 | packaging==23.1 48 | pandas==2.0.3 49 | Pillow==10.0.0 50 | pydantic==1.10.11 51 | pypdf==3.12.0 52 | python-dateutil==2.8.2 53 | pytz==2023.3 54 | PyYAML==6.0 55 | regex==2023.6.3 56 | requests==2.31.0 57 | safetensors==0.3.1 58 | scikit-learn==1.3.0 59 | scipy==1.11.1 60 | sentence-transformers==2.2.2 61 | sentencepiece==0.1.99 62 | six==1.16.0 63 | SQLAlchemy==2.0.18 64 | sympy==1.12 65 | tenacity==8.2.2 66 | threadpoolctl==3.1.0 67 | tiktoken==0.4.0 68 | tokenizers==0.13.3 69 | torch==2.0.1 70 | torchvision==0.15.2 71 | tqdm==4.65.0 72 | transformers==4.30.2 73 | triton==2.0.0 74 | typing-inspect==0.9.0 75 | typing_extensions==4.7.1 76 | tzdata==2023.3 77 | urllib3==2.0.3 78 | waitress==2.1.2 79 | Werkzeug==2.3.6 80 | yarl==1.9.2 81 | -------------------------------------------------------------------------------- /static/scripts/home.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | 3 | let sessionNameInput = document.getElementById("new_session_name"); 4 | let existingSession = document.getElementById("existing_session"); 5 | let newSessionName = document.getElementById("new_session_name"); 6 | let fileInput = document.querySelector('input[type="file"]'); 7 | let conditionalText = document.getElementById("conditional-text"); 8 | 9 | 10 | // grab first element of every tupple form existing sessions 11 | let sNames = Array.from(existingSession.options).map(option => { 12 | const match = option.value.match(/\('([^']+)',/); 13 | return match ? match[1] : null; 14 | }).filter(name => name !== null); 15 | 16 | 17 | // Process the session name to make it compatible with SQLite 18 | function processSessionName(name) { 19 | name = name.trim(); 20 | // Exclude all signs that conflict with SQLite 21 | name = name.replace(/[^\w]/g, '_'); 22 | name = name.toLowerCase(); 23 | 24 | return name; 25 | } 26 | 27 | //Update text according to the condition set 28 | function updateConditionalText() { 29 | if (existingSession.value) { 30 | conditionalText.innerText = "You are about to continue a conversation. Hit 'Start' to continue."; 31 | } else if (newSessionName.value) { 32 | conditionalText.innerText = "You will create a new session. Please select collections and click 'Create'."; 33 | } else { 34 | conditionalText.innerText = "(ノ☉ヮ⚆)ノ ⌒*:・゚✧"; 35 | } 36 | } 37 | 38 | // disable/enable the session name and file upload input fields 39 | existingSession.addEventListener("change", function () { 40 | console.log("existingSession change"); 41 | if (this.value !== "") { 42 | newSessionName.disabled = true; 43 | fileInput.disabled = true; 44 | } else { 45 | newSessionName.disabled = false; 46 | fileInput.disabled = false; 47 | } 48 | }); 49 | 50 | if (sessionNameInput) { 51 | sessionNameInput.addEventListener("change", function () { 52 | console.log("sessionNameInput change"); 53 | const enteredSessionName = processSessionName(this.value);; 54 | if (sNames.includes(enteredSessionName)) { 55 | alert("The session name is already taken. Please choose a different one. Note that program turns all special characters into '_'"); 56 | this.value = ""; 57 | } 58 | }); 59 | } 60 | 61 | 62 | existingSession.addEventListener("change", updateConditionalText); 63 | newSessionName.addEventListener("input", updateConditionalText); 64 | 65 | 66 | }); -------------------------------------------------------------------------------- /lib/params.py: -------------------------------------------------------------------------------- 1 | """Contains static parameters for the project 2 | """ 3 | 4 | import os 5 | from pathlib import Path 6 | 7 | 8 | ## General parameters ## 9 | STATIC_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static") 10 | 11 | STATIC_FOLDER = Path('./static') 12 | DB_FOLDER = Path(f'./static/data/dbs') 13 | UPLOAD_FOLDER = Path(f'./static/data/uploads') 14 | 15 | DB_NAME = 'app.db' 16 | DB_PATH = os.path.join(DB_FOLDER, DB_NAME) 17 | 18 | CNT_TABLE_NAME = 'context' 19 | SESSION_TABLE_NAME = 'session' 20 | 21 | 22 | TOKEN_THRES = 500 # Number of tokens to split the document into chunks 23 | NUM_SAMPLES = 5 # Number of samples to take from the document 24 | 25 | # List of available models 26 | OPENAI_MODEL_4O = ('gpt-4o', 128000) 27 | OPENAI_MODEL_4O_MINI = ('gpt-4o-mini', 128000) 28 | 29 | OPENAI_MODEL_EMBEDDING = 'text-embedding-ada-002' 30 | SENTENCE_TRANSFORMER_MODEL = 'multi-qa-MiniLM-L6-cos-v1' 31 | 32 | # prod model << Set this param to change the model used in production 33 | PROD_MODEL = OPENAI_MODEL_4O_MINI 34 | 35 | # Set path to agent system messages 36 | AGENT_INFO_PTH = Path(STATIC_FOLDER) / 'data' / 'simulacra.json' 37 | 38 | # Model context management 39 | SUMMARY_CTXT_USR = """ 40 | How would you act when I'd ask you what's this document about or ask you to summarize source text? 41 | """ 42 | SUMMARY_TXT_ASST = """ 43 | When a user asks me to summarize the source material or explain what it is about, 44 | I would look for the best text fragment that provides general information about the document's contents. 45 | To find a text fragment for summarization, I would start with the abstract and conclusion sections, 46 | and also I'd check the table of contents. 47 | """ 48 | 49 | 50 | ## DB parameters ## 51 | # used when new db initialized under static/data/dbs/ 52 | 53 | SESSION_TABLE_SQL = """ 54 | CREATE TABLE IF NOT EXISTS session ( 55 | 56 | uuid TEXT NOT NULL, 57 | collection_uuid TEXT NOT NULL, 58 | 59 | name TEXT NOT NULL, 60 | date TEXT NOT NULL 61 | )""" 62 | 63 | EMBEDDINGS_TABLE_SQL = """ 64 | CREATE TABLE IF NOT EXISTS embeddings ( 65 | 66 | uuid TEXT NOT NULL, 67 | embedding BLOB NOT NULL 68 | )""" 69 | 70 | COLLECTIONS_TABLE_SQL = """ 71 | CREATE TABLE IF NOT EXISTS collections ( 72 | 73 | uuid TEXT NOT NULL, 74 | doc_uuid TEXT NOT NULL, 75 | 76 | name TEXT NOT NULL, 77 | interaction_type TEXT NOT NULL, 78 | text TEXT NOT NULL, 79 | text_token_no INTEGER, 80 | page INTEGER, 81 | timestamp INTEGER, 82 | embedding_model TEXT NOT NULL 83 | )""" 84 | 85 | CHAT_HIST_TABLE_SQL = """ 86 | CREATE TABLE IF NOT EXISTS chat_history ( 87 | 88 | uuid TEXT NOT NULL, 89 | doc_uuid TEXT NOT NULL, 90 | 91 | interaction_type TEXT NOT NULL, 92 | text TEXT NOT NULL, 93 | text_token_no INTEGER, 94 | page INTEGER, 95 | timestamp INTEGER 96 | )""" -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ALP 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 |
25 |
26 | ALP - HUB 27 |
28 | 29 |
30 |
31 |
32 |
33 | 34 | 35 |
36 |
37 | 38 |
39 |
40 | 41 | 42 |
43 |
44 |
45 | 46 | 47 |
48 |

49 | ALP is a knowledge-grounded conversational AI system. 50 |

51 |

52 | The system enhances the accuracy of responses of GPT model related to a specific PDF document by using a retrieval augmentation technique. 53 |

54 | You need an OpenAI API key to use ALP. 55 | You can get it here. 56 | Note that since ALP operates via localhost your API key is safe. 57 |

58 |

(ノ☉ヮ⚆)ノ ⌒*:・゚✧

59 |
60 | 61 |
62 |
63 | 64 |
65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /templates/summary.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ALP 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 | ALP - EMBEDDING SUMMARY 26 |
27 |
28 |
29 |

Session UUID:{{ session_uuid }}

30 |

Session Name: {{ session_name }}

31 |

Embedding cost: {{ embedding_cost }}

32 |

Document length: {{ doc_length }}

33 | {% if length_warning %} 34 |

35 | Long document - embedding process may take several minutes. 36 |

37 | {% endif %} 38 |
39 | 40 | 41 |
42 | 46 |
47 |
48 |

49 | A pdf embedding process is required for every new session. 50 |

51 |

52 | Embedding cost is calculated basing on the length of the document and Open AI's pricing table. 53 |

54 |

55 | The process will take from several seconds to several minutes depending on the length of the document. 56 |

57 | 58 |
59 |
60 |
61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ALP 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 |
26 | 27 |
28 |
29 |

30 | ALP - SESSION: {{session_name}}; DATE: {{session_date}}; AGENT: {{agent_name}} 31 |

32 |
33 |
34 |
35 |
36 | 40 |
41 |
42 |
43 |
44 | 45 | 46 |
47 | 48 | 49 | 50 |
51 |
52 |
53 |
54 | 55 | 56 | 62 | 63 |
64 |
65 | 66 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /static/scripts/chat.js: -------------------------------------------------------------------------------- 1 | function scrollToBottom(element) { 2 | const lastElement = element.lastElementChild; 3 | if (lastElement) { 4 | lastElement.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' }); 5 | } 6 | } 7 | 8 | 9 | // Render chat history 10 | function renderChatHistory(chatHistory) { 11 | const responseDiv = document.getElementById("response"); 12 | 13 | chatHistory.forEach((interaction) => { 14 | const role = interaction.interaction_type === "user" ? "User" : "Agent"; 15 | const messageElement = document.createElement("p"); 16 | messageElement.innerHTML = `${role}: ${interaction.text}`; 17 | responseDiv.appendChild(messageElement); 18 | }); 19 | 20 | // If chat history is empty, display the agent's message 21 | if (chatHistory.length === 0) { 22 | const messageElement = document.createElement("p"); 23 | messageElement.innerHTML = "Agent: Hi! Let's talk about your sources."; 24 | responseDiv.appendChild(messageElement); 25 | } 26 | 27 | // Scroll to the bottom 28 | scrollToBottom(responseDiv); 29 | } 30 | renderChatHistory(chatHistory); 31 | 32 | 33 | document.addEventListener("DOMContentLoaded", function () { 34 | const askForm = document.getElementById("ask-form"); 35 | const responseDiv = document.getElementById("response"); 36 | 37 | askForm.addEventListener("submit", function (event) { 38 | 39 | event.preventDefault(); 40 | 41 | const question = askForm.querySelector("textarea[name='question']").value; 42 | 43 | // Show the processing GIF 44 | document.getElementById("processing").style.display = "block"; 45 | 46 | // Append the user's input to the response div 47 | const userMessageElement = document.createElement("p"); 48 | userMessageElement.innerHTML = "User: " + question; 49 | responseDiv.appendChild(userMessageElement); 50 | 51 | // Scroll to the bottom 52 | scrollToBottom(responseDiv); 53 | 54 | fetch("/ask", { 55 | method: "POST", 56 | headers: { 57 | "Content-Type": "application/json", 58 | }, 59 | body: JSON.stringify({ 60 | question: question, 61 | }), 62 | }) 63 | .then((response) => response.json()) 64 | .then((result) => { 65 | // Hide the processing GIF 66 | document.getElementById("processing").style.display = "none"; 67 | 68 | // Append the agent's response to the response div 69 | const agentMessageElement = document.createElement("p"); 70 | agentMessageElement.innerHTML = "Agent: " + result.response.choices[0].message.content; 71 | responseDiv.appendChild(agentMessageElement); 72 | 73 | // Scroll to the bottom 74 | scrollToBottom(responseDiv); 75 | }) 76 | .catch((error) => { 77 | // Hide the processing GIF in case of an error 78 | document.getElementById("processing").style.display = "none"; 79 | 80 | // Display an error message 81 | responseDiv.textContent = "An error occurred: " + error; 82 | }); 83 | }); 84 | }); 85 | 86 | 87 | -------------------------------------------------------------------------------- /templates/collection_manager.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ALP 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 |
25 |
26 | ALP - COLLECTIONS MANAGER 27 |
28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | 43 |
44 | 45 |
46 | 47 | 48 |
49 | 50 |
51 | 52 |
53 | 54 |
55 |

56 | Create a collection by uploading a pdf file and selecting an embedding method. 57 |

58 |

59 | SBERT method uses multi-qa-MiniLM-L6-cos-v1 for embeding. 60 |

61 |

62 | If you choose Open AI, you will be charged as per their current pricing table for text-embedding-ada-002. 63 |

64 |

(ノ☉ヮ⚆)ノ ⌒*:・゚✧

65 |
66 | 67 |
68 | 69 |
70 | 71 |
72 | 73 | 74 | -------------------------------------------------------------------------------- /lib/ai_tools.py: -------------------------------------------------------------------------------- 1 | """Collection of utilities to interact with or extend of standard model capabilities 2 | """ 3 | 4 | import tiktoken 5 | import openai 6 | import time 7 | import lib.params as prm 8 | import numpy as np 9 | # import logging 10 | 11 | from sentence_transformers import SentenceTransformer 12 | from transformers import BertTokenizer 13 | from transformers import logging as hf_logging 14 | 15 | # to drop warnings from tokenizers 16 | hf_logging.set_verbosity_error() 17 | 18 | 19 | def get_tokens(message, method='SBERT'): 20 | """Count number of tokens in a message 21 | """ 22 | 23 | if method == 'openai': 24 | model == prm.OPENAI_MODEL 25 | encoder = tiktoken.encoding_for_model(model) 26 | 27 | 28 | elif method == 'SBERT': 29 | model = SentenceTransformer(prm.SENTENCE_TRANSFORMER_MODEL) 30 | encoder = model.tokenizer 31 | 32 | return encoder.encode(message) 33 | 34 | 35 | def decode_tokens(tokens, method='SBERT'): 36 | """Decode tokens 37 | """ 38 | 39 | if method == 'openai': 40 | model == prm.OPENAI_MODEL 41 | encoder = tiktoken.encoding_for_model(model) 42 | 43 | elif method == 'SBERT': 44 | model = SentenceTransformer(prm.SENTENCE_TRANSFORMER_MODEL) 45 | encoder = model.tokenizer 46 | 47 | return encoder.decode(tokens) 48 | 49 | 50 | def get_embedding_gpt( 51 | text, 52 | model=prm.OPENAI_MODEL_EMBEDDING, 53 | calls_per_minute=60 54 | ): 55 | """Returns the embedding for a given text. Limit calls per minute 56 | Makes use of Open AI API to embed the text. 57 | """ 58 | time_delay = 60 / calls_per_minute 59 | 60 | embedding = openai.Embedding.create( 61 | input=text, 62 | model=model, 63 | )['data'][0]['embedding'] 64 | 65 | time.sleep(time_delay) 66 | return embedding 67 | 68 | 69 | def get_embedding_sbert( 70 | text, 71 | model=prm.SENTENCE_TRANSFORMER_MODEL 72 | ): 73 | """Returns the embedding for a given text. 74 | Makes use of Sentence-BERT (SBERT) to embed the text. 75 | """ 76 | 77 | model = SentenceTransformer(model) 78 | embedding = model.encode(text) 79 | 80 | return embedding 81 | 82 | 83 | def vector_similarity(x, y): 84 | """ 85 | Returns a dot product of two vectors. For embedding values between 0 and 1 it is an equivalent 86 | of cosine similarity. 87 | """ 88 | 89 | # Catch all ys that are not lists or arrays 90 | if not isinstance(y, (list, np.ndarray)): 91 | return 0 92 | 93 | x = np.array(x, dtype=np.float32) 94 | y = np.array(y, dtype=np.float32) 95 | 96 | 97 | if len(x) < len(y): 98 | # pad x with zeros to match the length of y 99 | x = np.concatenate([x, np.zeros(y.shape[0] - x.shape[0], dtype=np.float32)]) 100 | elif len(y) < len(x): 101 | # pad y with zeros to match the length of x 102 | y = np.concatenate([y, np.zeros(x.shape[0] - y.shape[0], dtype=np.float32)]) 103 | 104 | return np.dot(x,y) 105 | 106 | 107 | def order_document_sections_by_query_similarity(query, contexts): 108 | """ 109 | Get embedding for the supplied query, and compare it against all of the available document embeddings 110 | to find the most relevant sections. 111 | 112 | Return the list of document sections, sorted by relevance in descending order. 113 | """ 114 | 115 | # TODO: probable inefficiency - this embedding is later calculated 116 | # again downstream when saving to the database. 117 | query_embedding = get_embedding_sbert(query) 118 | 119 | document_similarities = sorted( 120 | [ 121 | (vector_similarity(query_embedding, doc_embedding), doc_index) for doc_index, doc_embedding in contexts.items() 122 | ], reverse=True 123 | ) 124 | 125 | return document_similarities -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # session data 2 | /static/data/dbs/* 3 | !static/data/dbs/info.txt 4 | 5 | /static/data/uploads/* 6 | !static/data/uploads/info.txt 7 | 8 | _archive/ 9 | 10 | api_key.txt 11 | devlog.txt 12 | db_schema.txt 13 | bash_utils 14 | 15 | # virtualenv 16 | alp_venv/ 17 | 18 | 19 | 20 | ## BOILERPLATE ## 21 | ### Flask ### 22 | instance/* 23 | !instance/.gitignore 24 | .webassets-cache 25 | .env 26 | 27 | ### Flask.Python Stack ### 28 | # Byte-compiled / optimized / DLL files 29 | __pycache__/ 30 | *.py[cod] 31 | *$py.class 32 | 33 | # C extensions 34 | *.so 35 | 36 | # Distribution / packaging 37 | .Python 38 | build/ 39 | develop-eggs/ 40 | dist/ 41 | downloads/ 42 | eggs/ 43 | .eggs/ 44 | lib64/ 45 | parts/ 46 | sdist/ 47 | var/ 48 | wheels/ 49 | share/python-wheels/ 50 | *.egg-info/ 51 | .installed.cfg 52 | *.egg 53 | MANIFEST 54 | 55 | # PyInstaller 56 | # Usually these files are written by a python script from a template 57 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 58 | *.manifest 59 | *.spec 60 | 61 | # Installer logs 62 | pip-log.txt 63 | pip-delete-this-directory.txt 64 | 65 | # Unit test / coverage reports 66 | htmlcov/ 67 | .tox/ 68 | .nox/ 69 | .coverage 70 | .coverage.* 71 | .cache 72 | nosetests.xml 73 | coverage.xml 74 | *.cover 75 | *.py,cover 76 | .hypothesis/ 77 | .pytest_cache/ 78 | cover/ 79 | 80 | # Translations 81 | *.mo 82 | *.pot 83 | 84 | # Django stuff: 85 | *.log 86 | local_settings.py 87 | db.sqlite3 88 | db.sqlite3-journal 89 | 90 | # Flask stuff: 91 | instance/ 92 | 93 | # Scrapy stuff: 94 | .scrapy 95 | 96 | # Sphinx documentation 97 | docs/_build/ 98 | 99 | # PyBuilder 100 | .pybuilder/ 101 | target/ 102 | 103 | # Jupyter Notebook 104 | .ipynb_checkpoints 105 | 106 | # IPython 107 | profile_default/ 108 | ipython_config.py 109 | 110 | # pyenv 111 | # For a library or package, you might want to ignore these files since the code is 112 | # intended to run in multiple environments; otherwise, check them in: 113 | # .python-version 114 | 115 | # pipenv 116 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 117 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 118 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 119 | # install all needed dependencies. 120 | #Pipfile.lock 121 | 122 | # poetry 123 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 124 | # This is especially recommended for binary packages to ensure reproducibility, and is more 125 | # commonly ignored for libraries. 126 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 127 | #poetry.lock 128 | 129 | # pdm 130 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 131 | #pdm.lock 132 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 133 | # in version control. 134 | # https://pdm.fming.dev/#use-with-ide 135 | .pdm.toml 136 | 137 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 138 | __pypackages__/ 139 | 140 | # Celery stuff 141 | celerybeat-schedule 142 | celerybeat.pid 143 | 144 | # SageMath parsed files 145 | *.sage.py 146 | 147 | # Environments 148 | .venv 149 | env/ 150 | venv/ 151 | ENV/ 152 | env.bak/ 153 | venv.bak/ 154 | 155 | # Spyder project settings 156 | .spyderproject 157 | .spyproject 158 | 159 | # Rope project settings 160 | .ropeproject 161 | 162 | # mkdocs documentation 163 | /site 164 | 165 | # mypy 166 | .mypy_cache/ 167 | .dmypy.json 168 | dmypy.json 169 | 170 | # Pyre type checker 171 | .pyre/ 172 | 173 | # pytype static type analyzer 174 | .pytype/ 175 | 176 | # Cython debug symbols 177 | cython_debug/ 178 | 179 | # PyCharm 180 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 181 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 182 | # and can be added to the global gitignore or merged into this file. For a more nuclear 183 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 184 | #.idea/ 185 | -------------------------------------------------------------------------------- /templates/session_manager.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ALP 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 |
25 |
26 | ALP - SESSION MANAGER 27 |
28 | 29 |
30 |
31 | 32 |
33 | 34 |
35 | 36 | 40 |
41 | 42 |

^^^

43 | 44 |
45 | 46 | 52 | 53 |
54 | 55 |
56 | 57 | 58 | 59 | 60 | 65 | 66 | 67 | 68 |
69 | 70 |
71 |
72 | 73 |
74 |

75 | Choose session to start from the dropdown menu or create a new session by entering a new session name. 76 |

77 |

78 | If you create a new session, you can select the collections to be included in the session. 79 | Hold down the Ctrl (windows) / Command (Mac) button to select multiple options. 80 |

81 |

(ノ☉ヮ⚆)ノ ⌒*:・゚✧

82 |
83 |
84 |
85 | 86 |
87 | 88 | 89 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /lib/chatbot.py: -------------------------------------------------------------------------------- 1 | """ALP general chatbot class 2 | """ 3 | 4 | import openai 5 | import json 6 | import pandas as pd 7 | import lib.ai_tools as ait 8 | import lib.params as prm 9 | 10 | class Chatbot: 11 | """Generic chatbot class 12 | """ 13 | 14 | def __init__(self): 15 | """Initializes chatbot class 16 | 17 | parametrs: 18 | aname - name of the agent ('robb' or 'agent') 19 | """ 20 | 21 | #read json file from ./static/data into a python dictionary object 22 | with open(prm.AGENT_INFO_PTH) as f: 23 | self.simulacra = json.load(f) 24 | 25 | 26 | def set_agent(self, aname): 27 | """Sets Agent's role for the chatbot 28 | """ 29 | 30 | self.agent = aname 31 | #choose agent for chatbot sys message 32 | self.sys_message = self.simulacra[self.agent] 33 | print("Agent set to: ", self.agent) 34 | 35 | 36 | 37 | def build_prompt(self, 38 | prev_usr, 39 | prev_asst, 40 | recall_src, 41 | recall_usr, 42 | recall_ast, 43 | question): 44 | """Builds prompt for the chatbot. 45 | Returns a list of dicts 46 | """ 47 | 48 | self.prev_user = {"role": "user", "content": f"{prev_usr}"} 49 | self.prev_assistant = {"role": "assistant", "content": f"{prev_asst}"} 50 | self.user_message = { 51 | "role": "user", 52 | "content": f"SRC: {recall_src}. QRY: {recall_usr}. RPL: {recall_ast}. INP: {question}" 53 | } 54 | 55 | return [self.sys_message, self.prev_user, self.prev_assistant, self.user_message] 56 | 57 | 58 | def chat_completion_response(self, msg): 59 | """Makes API call to OpenAI's chat completion endpoint. 60 | """ 61 | 62 | #TODO: user can choose variant of GPT model 63 | api_response = openai.ChatCompletion.create( 64 | model=prm.PROD_MODEL[0], 65 | messages=msg 66 | ) 67 | 68 | return api_response 69 | 70 | 71 | def retrieve_closest_idx(self, q, n, src, usr, ast): 72 | """Retrieves n closest samples from recall tables. 73 | Acts as a simple nearest neighbors search with cosine similarity. 74 | """ 75 | 76 | self.recall_source_idx = None 77 | self.recall_user_idx = None 78 | self.recall_assistant_idx = None 79 | 80 | ## Get SRC context 81 | if len(src) == 0: 82 | print('WARNING! No source material found.') 83 | self.recall_source_idx = [] 84 | else: 85 | # Get the context most relevant to user's question 86 | recall_source_id = ait.order_document_sections_by_query_similarity(q, src)[0:n] 87 | self.recall_source_idx = [x[1] for x in recall_source_id] 88 | 89 | ## GET QRY context 90 | # We get most relevant context from the user's previous messages here 91 | if len(usr) == 0: 92 | print('No context found in user chat history') 93 | self.recall_user_idx = [] 94 | else: 95 | self.recall_user_idx = ait.order_document_sections_by_query_similarity(q, usr)[0][1] 96 | 97 | ## GET RPL context 98 | # We get most relevant context from the agent's previous messages here 99 | if len(ast) == 0: 100 | print('No context found agent chat history') 101 | self.recall_assistant_idx = [] 102 | else: 103 | self.recall_assistant_idx = ait.order_document_sections_by_query_similarity(q, ast)[0][1] 104 | 105 | 106 | def retrieve_text(self, src, chat): 107 | self.src_text = 'No source text found' 108 | self.usr_text = 'No user text found' 109 | self.ast_text = 'No assistant text found' 110 | 111 | 112 | if self.recall_source_idx: 113 | if self.recall_source_idx != []: 114 | self.src_text = src.loc[self.recall_source_idx, 'text'].tolist() 115 | self.src_text = '| '.join(self.src_text) 116 | else: 117 | print('WARNING! No indexes for source material found.') 118 | 119 | 120 | if self.recall_user_idx: 121 | if self.recall_user_idx != []: 122 | self.usr_text = chat.loc[self.recall_user_idx]['text'] 123 | 124 | 125 | if self.recall_assistant_idx: 126 | if self.recall_assistant_idx != []: 127 | self.ast_text = chat.loc[self.recall_assistant_idx]['text'] 128 | 129 | return self.src_text, self.usr_text, self.ast_text 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /static/styles/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | padding: 0; 5 | font-family: 'Titillium Web', sans-serif; 6 | } 7 | 8 | body { 9 | background-image: radial-gradient(4px at center,rgb(0, 0, 0) 20%, transparent 20%), 10 | radial-gradient(4px at center,rgb(0, 0, 0) 20%, transparent 20%); 11 | background-position: 0px 0px, 4px 3px; 12 | background-size: 8px 6px; 13 | /* background-color: #575757; */ 14 | background-color: #aaa6dc; 15 | } 16 | 17 | 18 | .hero { 19 | min-height: 100vh; 20 | display: flex; 21 | justify-content: center; 22 | align-items: center; 23 | flex-direction: column; 24 | border-radius: 0; 25 | } 26 | 27 | .card { 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: center; 31 | align-items: center; 32 | min-width: 800px; 33 | border: 2px solid #000000; 34 | border-radius: 0; 35 | width: 50%; 36 | max-width: 1000px; 37 | box-shadow: #000000 10px 10px 0px -2px; 38 | } 39 | 40 | .card-header { 41 | font-weight: 800; 42 | display: flex; 43 | flex-direction: row; 44 | width: 100%; 45 | justify-content: flex-start; 46 | align-items: flex-start; 47 | background-color: rgba(0, 0, 0, 0.158); 48 | color: rgb(0, 0, 0); 49 | border-bottom: 2px solid #000000; 50 | border-radius: 0 !important; 51 | } 52 | .container-menu { 53 | display: flex; 54 | flex-direction: row; 55 | justify-content: center; 56 | align-items: flex-start; 57 | padding: 2em; 58 | width: 50%; 59 | min-width: 100%; 60 | min-height: 350px; 61 | background: rgb(246, 246, 246); 62 | border-radius: 0; 63 | } 64 | 65 | .content { 66 | padding: 0 4em 0 2em; 67 | width: 50%; 68 | height:100%; 69 | } 70 | 71 | .container-interface { 72 | display: flex; 73 | flex-direction: row; 74 | justify-content: center; 75 | align-items: center; 76 | width: 90%; 77 | height: 90vh; 78 | } 79 | 80 | 81 | .right-side { 82 | width: 50%; 83 | height: 100%; 84 | padding: 0 2em 0 2em; 85 | display: flex; 86 | flex-direction: column; 87 | align-items: flex-start; 88 | justify-content: flex-start; 89 | background-color: inherit; 90 | } 91 | 92 | .right-side-interface { 93 | width: 30%; 94 | padding: 0 2em 0 2em; 95 | max-width: 800px; 96 | min-height: 90vh; 97 | display: flex; 98 | flex-direction: column; 99 | justify-content: flex-start; 100 | align-items: flex-start; 101 | } 102 | 103 | a { 104 | color: #16b4a2; 105 | text-decoration: none; 106 | margin: 0 0 2em 0; 107 | } 108 | 109 | .chat-container { 110 | width: 70%; 111 | max-width: 800px; 112 | height: 100%; 113 | display: flex; 114 | flex-direction: column; 115 | justify-content: center; 116 | align-items: center; 117 | } 118 | 119 | .chat-box { 120 | flex-grow: 1; 121 | width: 100%; 122 | border-bottom: 2px solid #000000; 123 | border-radius: 0; 124 | padding: 1em 0em 2em 0em; 125 | overflow-y: hidden; 126 | } 127 | 128 | .response-container { 129 | width: 100%; 130 | height: 100%; 131 | /* height: calc(90vh - 6em); Set the height to 90% of the viewport height minus the space taken by the input container and other elements */ 132 | display: flex; 133 | flex-direction: column; 134 | justify-content: flex-start; 135 | overflow-y: auto; 136 | padding: 0em 1em 2em 1em; 137 | } 138 | 139 | .input-container { 140 | width: 100%; 141 | display: flex; 142 | justify-content: space-between; 143 | align-items: center; 144 | margin-top: 1rem; 145 | padding-bottom: 1rem; 146 | padding-left: 1em; 147 | padding-right: 1em; 148 | } 149 | 150 | .warning{ 151 | font-weight: 800; 152 | color: #e94b5d 153 | } 154 | 155 | textarea { 156 | width: 100%; 157 | height: 4em; 158 | resize: vertical; 159 | } 160 | 161 | .form-container { 162 | display: flex; 163 | flex-direction: column; 164 | align-items: center; 165 | width: 100%; 166 | } 167 | 168 | .form-group { 169 | display: flex; 170 | flex-direction: column; 171 | justify-content: right; 172 | align-items: left; 173 | width: 100%; 174 | margin-bottom: 1rem; 175 | } 176 | 177 | .button-container { 178 | display: flex; 179 | justify-content: space-between; 180 | align-items: center; 181 | width: 33%; 182 | /* margin-left: 1em; */ 183 | } 184 | 185 | #proceed { 186 | margin: 4em 0 0 0; 187 | } 188 | 189 | input[type="text"], input[type="file"] { 190 | width: 100%; 191 | } 192 | 193 | #ask-form { 194 | width: 100%; 195 | display: flex; 196 | justify-content: flex-start; 197 | align-items: center; 198 | } 199 | 200 | #ask-button { 201 | margin: 0 2em 0 1em; 202 | } 203 | 204 | 205 | input[type="text"], input[type="file"] { 206 | width: 400px; /* Set a fixed width for the input fields */ 207 | max-width: 100%; 208 | } 209 | input[type="file"] { 210 | text-overflow: ellipsis; /* Add ellipsis when the text overflows */ 211 | white-space: nowrap; /* Prevent text from wrapping to the next line */ 212 | overflow: hidden; /* Hide overflowing text */ 213 | } 214 | input[type="submit"] { 215 | max-width: 100px; 216 | } 217 | input[type="button"] { 218 | max-width: 100px; 219 | } 220 | .btn-l { 221 | margin-right: 1em !important; /* override bootstrap */ 222 | } 223 | .btn-r { 224 | margin-right: 1em !important; /* override bootstrap */ 225 | } 226 | 227 | /* Animate the spinner widget */ 228 | .spinner { 229 | width: 40px; 230 | height: 40px; 231 | position: relative; 232 | margin: 1.8em 0 0 0; 233 | } 234 | .cube1, 235 | .cube2 { 236 | width: 20px; 237 | height: 8px; 238 | background-color: #16b4a2; 239 | position: absolute; 240 | animation: bump 1.8s infinite ease-in-out; 241 | } 242 | .cube2 { 243 | animation-delay: -0.9s; 244 | } 245 | @keyframes bump { 246 | 0%, 80%, 100% { 247 | transform: translateY(0); 248 | } 249 | 40% { 250 | transform: translateY(-10px); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /lib/cont_proc.py: -------------------------------------------------------------------------------- 1 | """Content processing utilities 2 | """ 3 | 4 | import re 5 | import uuid 6 | import pickle 7 | import pandas as pd 8 | 9 | import lib.params as prm 10 | 11 | from lib.ai_tools import get_embedding_gpt, get_embedding_sbert, get_tokens, decode_tokens 12 | 13 | 14 | ## Various processors 15 | def process_name(name): 16 | """Process name string to make it suitable for db and further processing. 17 | Current support for pdfs. In the future will support other file types. 18 | """ 19 | 20 | # catch pdfs 21 | pdf_ = False 22 | if '.pdf' in name: 23 | # get name stem 24 | pdf_ = True 25 | name = name.split('.pdf')[0] 26 | 27 | name = name.strip() 28 | # exclude all signs that conflict with SQLite 29 | name = re.sub(r'[^\w]', '_', name) 30 | name = name.lower() 31 | 32 | # truncate if needed 33 | if len(name) > 100: 34 | name = name[:100] 35 | 36 | if pdf_: 37 | return name + '.pdf' 38 | else: 39 | return name 40 | 41 | 42 | ## Text processing functions 43 | def clean_text(text): 44 | text = text.replace('\t', ' ') 45 | text = text.strip().lower() 46 | text = text.replace('\n', '') 47 | text = text.replace('\t', '') 48 | text = re.sub(r'\s+', ' ', text) 49 | return text 50 | 51 | 52 | def long_date_to_short(date): 53 | """Convert long date to short date""" 54 | date = date.split(' ')[0] 55 | return date 56 | 57 | 58 | def create_uuid(): 59 | return str(uuid.uuid4()) 60 | 61 | 62 | def pages_to_dict(pages): 63 | """convert langchain.docstore.document.Document to dict""" 64 | pages_dict = {} 65 | for page in pages: 66 | pg_txt = page.page_content 67 | pg_txt = clean_text(pg_txt) 68 | pages_dict[page.metadata['page']] = pg_txt 69 | return pages_dict 70 | 71 | 72 | def pages_to_dataframe(pages): 73 | """Convert dictionary of pages to Pandas dataframe""" 74 | pages_dct = pages_to_dict(pages) 75 | # # Grab contents into a dataframe 76 | doc_contents_df = pd.DataFrame(pages_dct, index=['contents']).T 77 | 78 | return doc_contents_df 79 | 80 | 81 | def split_pages(pages_df, method): 82 | """Split pages that are too long for the model 83 | prepare the contents to be embedded 84 | """ 85 | 86 | pages_df['contents_tokenized'] = pages_df['contents'].apply( 87 | lambda x: get_tokens(x, method=method) 88 | ) 89 | 90 | # We want to split the contents_tokenized by counting the number of tokens and when it reaches the threshold, split it 91 | pages_df['contents_tokenized'] = pages_df['contents_tokenized'].apply( 92 | lambda x: [x[i:i+prm.TOKEN_THRES] for i in range(1, len(x), prm.TOKEN_THRES)] 93 | ) 94 | 95 | # At this point the tokenized text is split by set threshold into an n element list 96 | # We want to explode that list into rows and perserve index that tracks the src page 97 | pages_contents_long_df = pages_df.explode( 98 | column='contents_tokenized' 99 | )[['contents_tokenized']] 100 | 101 | # track # of tokens in each text snippet 102 | pages_contents_long_df['text_token_no'] = pages_contents_long_df['contents_tokenized'].apply( 103 | lambda x: len(x) 104 | ) 105 | 106 | # decode tokens back into text (SBERT default) 107 | pages_contents_long_df['contents'] = pages_contents_long_df['contents_tokenized'].apply( 108 | lambda x: decode_tokens(x, method=method) 109 | ) 110 | 111 | return pages_contents_long_df 112 | 113 | 114 | def prepare_for_embed(pages_df, collection_name, model): 115 | """Pre-process dataframe that holds src pages to be embedded by chosen model 116 | returns a dataframe with the following columns: 117 | name, interaction_type, text, text_token_no, page, timestamp 118 | """ 119 | 120 | return_cols=['name', 'interaction_type', 'text', 'text_token_no', 'page', 'timestamp', 'embedding_model'] 121 | 122 | # Further dataframe processing 123 | pages_df = ( 124 | pages_df 125 | .reset_index() # Reset index so page numbers get stored in column 126 | .rename(columns={'index': 'page'}) 127 | .assign(name=collection_name) 128 | .assign(interaction_type='source') 129 | .assign(timestamp=0) 130 | .assign(embedding_model=model) 131 | ) 132 | # Form text column for each fragment, we will later use it as the source text for embedding 133 | pages_df['text'] = "SRC:" + pages_df['name'] + " PAGE: " + pages_df['page'].astype(str) + " CONTENT: " + pages_df['contents'] 134 | 135 | return pages_df[return_cols] 136 | 137 | 138 | def embed_cost(pages_contents_long_df, price_per_k=0.0004): 139 | """Calculate the cost of running the Open AI model to get embeddings 140 | """ 141 | embed_cost = (pages_contents_long_df['text_token_no'].sum() / 1000) * price_per_k 142 | return embed_cost 143 | 144 | 145 | def embed_pages(pages_contents_long_df, method): 146 | """Get embeddings for each page""" 147 | 148 | # Get embeddings for each page 149 | if method == 'openai': 150 | print(f'!Sending pages to Open AI for embedding with {prm.OPENAI_MODEL}') 151 | pages_contents_long_df['embedding'] = pages_contents_long_df['text'].apply( 152 | lambda x: pickle.dumps(get_embedding_gpt(x)) 153 | ) 154 | elif method == 'SBERT': 155 | print(f'!Embedding pages with {prm.SENTENCE_TRANSFORMER_MODEL}') 156 | pages_contents_long_df['embedding'] = pages_contents_long_df['text'].apply( 157 | lambda x: pickle.dumps(get_embedding_sbert(x)) 158 | ) 159 | 160 | return pages_contents_long_df 161 | 162 | 163 | def convert_table_to_dct(table): 164 | """Converts table to dictionary of embeddings 165 | As Pandas df.to_dict() makes every value a string, 166 | we need to convert it to list of floats before passing it to the model 167 | """ 168 | table_dct = table[['embedding']].to_dict()['embedding'] 169 | for k, v in table_dct.items(): 170 | # table_dct[k] = ast.literal_eval(v) 171 | table_dct[k] = v 172 | return table_dct 173 | 174 | 175 | def prepare_chat_recall(chat_table): 176 | """Prepare chat recall table for chatbot memory build 177 | """ 178 | usr_f = (chat_table['interaction_type'] == 'user') 179 | ast_f = (chat_table['interaction_type'] == 'assistant') 180 | 181 | return chat_table[usr_f], chat_table[ast_f] 182 | 183 | def format_response(response): 184 | """Grab generator response and format it for display in the chatbot 185 | :param response: dict 186 | """ 187 | code_pattern = r'```(.*?)```' 188 | cont = response['choices'][0]['message']['content'] 189 | 190 | # Replace newlines with
tags 191 | cont_interim = cont.replace('\n', '
') 192 | 193 | # Use a lambda function to replace detected code with code wrapped in
 tags
194 |     new_cont = re.sub(code_pattern, lambda match: '
' + match.group(1) + '
', cont_interim, flags=re.DOTALL) 195 | 196 | return new_cont 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/rpast/ALP) 2 | 3 | # ALP 4 | 5 | 6 | ## Overview 7 | 8 | _ALP is an open-source, knowledge-based conversational AI system, crafted to produce responses that are rooted in relevant information from external sources._ 📖💫 9 | 10 | ALP allows you to build a large knowledge base that can be queried while interacting with a chatbot. Similarity based context constructions allows for better relevance of materials extracted from the database. The chatbot has unlimited conversational memory and the ability to export conversation and source embeddings to JSON format. 11 | 12 | ALP maintains conversation history and embeddings in a local SQLite database 🗄️. As a result, document uploading and embedding processes are required only once, enabling users to resume conversations seamlessly. 13 | 14 | ALP is intended to be run via localhost 💻. All you need is Python and few commands to setup environment. Feel free to fork, explore the code, and adjust to your needs 🔧. 15 | 16 | ## Table of Contents 17 | 18 | - [ALP](#alp) 19 | - [Table of Contents](#table-of-contents) 20 | - [Changelog](#changelog) 21 | - [Introduction](#introduction) 22 | - [Features](#features) 23 | - [Installation](#installation) 24 | - [Configuration](#configuration) 25 | - [Usage](#usage) 26 | - [Demo](#demo) 27 | - [Todo](#todo) 28 | 29 | ## Changelog 30 | - 20250628 - source-reader dev branch created. 31 | - 20240815 - model list updated to: ```gpt-4o'```, ```gpt-4o-mini``` 32 | - 20231226 - ```gpt-4-1106-preview``` added as default generative model. User can change it in ./lib/params.py in _PROD_MODEL_. Collection creation page count bug fix. 33 | - 20230911 - define custom agent behavior in ./static/data/simulacra.json and choose them in session manager from a drop-down menu. 34 | - 20230406 - bug-fix that prevented program to initialize database under /static/data/dbs/; requirements.txt fix 35 | - 20230705 - new data model, SBERT based embeddings 36 | - 20230411 - interface upgrade, UX improvements, bugfixes 37 | - 20230405 - program stores converastion history 38 | 39 | ## Introduction 40 | ALP enhances the accuracy of responses of GPT-based models relative to given PDF documents by using a retrieval augmentation method. This approach ensures that the most relevant context is always passed to the model. The intention behind ALP is to assist exploration of the overwhelming knowledge base of research papers, books and notes, making it easier to access and digest content. 41 | 42 | Currently ALP utilizes following models: 43 | 1. Text embedding: ```multi-qa-MiniLM-L6-cos-v1``` 44 | 2. Generation: ```gpt-4o'```, ```gpt-4o-mini``` 45 | 46 | ## Features 47 | - **Conversational research assistant**: Interact with and get information from collections of pdf files. 48 | - **Unlimited conversational memory**: Retain information from previous conversations for context-aware responses. 49 | - **Come back to past conversations**: Pick up your conversation right where you left. 50 | - **Flexible data model**: Allows for conversation with more than one document in one session. 51 | - **Uses open source models**: [Sentence-Transformers](https://www.sbert.net/) allow for costless and fast text embedding. 52 | - **Support for long documents**: upload books and other lengthy pdfs. 53 | - **Retrieval augmentation**: Utilize retrieval augmentation techniques for improved accuracy. Read more [here](https://arxiv.org/pdf/2104.07567.pdf). 54 | - **Define your own agent behavior**: set system message defining your agents in ./static/data/simulacra.json. You can then easily adjust html form to choose names from a drop-down menu. 55 | - **Local deployment**: Spin up ALP locally on your machine. 56 | - **JSON export**: Export conversation as a JSON file. 57 | - **Open source**: The code is yours to read, test and break and build upon. 58 | 59 | ## Installation 60 | To set up ALP on your local machine, follow these steps: 61 | 62 | 1. **Install Python:** 63 | 64 | Make sure you have Python installed on your machine. I recommend [Anaconda](https://www.anaconda.com/products/distribution) for an easy setup. 65 | 66 | __Important:__ ALP runs on Python 3.10 67 | 68 | 2. **Fork and clone the repository:** 69 | 70 | After forking the repo clone it in a command line: 71 | 72 | ```bash 73 | git clone https://github.com/yourusername/alp.git 74 | cd ALP 75 | ``` 76 | 77 | 3. **Create a virtual environment and activate it:** 78 | 79 | From within the ALP/ local directory invoke following commands 80 | 81 | For Linux users in Bash: 82 | 83 | ```bash 84 | python3 -m venv venv 85 | source venv/bin/activate 86 | ``` 87 | 88 | For Windows users in CMD: 89 | 90 | ``` 91 | python -m venv venv 92 | venv\Scripts\activate.bat 93 | ``` 94 | 95 | This should create ALP/venv/ directory and activate virtual environment. 96 | Naturally, you can use other programs to handle virtualenvs. 97 | 98 | 4. **Install the required dependencies to virtual environment:** 99 | 100 | ```bash 101 | pip install -r requirements.txt 102 | ``` 103 | 104 | ## Configuration 105 | By default, ALP runs in `localhost`. It requires API key to connect with GPT model via Open AI API. 106 | in the ALP/ directory create an api_key.txt and paste your API key there. Make sure api_key.txt is added to your .gitignore file so it doesnt leak to github. You can get your Open AI API key here https://platform.openai.com 🌐 107 | 108 | 109 | ## Usage 110 | 5. **Run the ALP application:** 111 | 112 | ```bash 113 | python alp.py 114 | ``` 115 | 116 | 6. **Access the application:** 117 | 118 | The app should open in your default web browser. If it doesn't, navigate to http://localhost:5000. 119 | First use involves creation of app.db file under ALP/static/data/dbs/. This is your SQLite database file that will hold conversation history and embeddings. Also, the script will download _'multi-qa-MiniLM-L6-cos-v1'_ (80 MB) to your PC from Hugging Face repositories. It will happen automatically on a first launch. 120 | 121 | 122 | 7. **Start using ALP:** 123 | 124 | ALP app interface consists of couple of sections: 125 | 126 | * HUB allows you to create new collection under 'Collections' or create/continue conversation session under 'Sessions'. 127 | * COLLECTIONS MANAGER: create a new collection by specifying its name and uploading a pdf file. 128 | * SESSION MANAGER: 129 | * continue existing session by selecting it from dropdown and hitting 'Start'. 130 | * create a new session there as well by defining its name and selecting collections linked to the session. 131 | * Select predefined agent role for the conversation. Works also for historical chats. 132 | * SESSION window: talk to GPT model over collections linked to the session. Each time a similarity score is calculated between user's query and collections' embeddings, a list of most similar datasources is printed in the program's console. These sources will be passed as a context with the user's question to the GPT model. 133 | 134 | ## Screenshots 135 | 136 |
137 | 138 |
139 | 140 | 141 | 142 | 143 | ## Todo 144 | - [x] Allow user to continue conversations on another sessions. 145 | - [x] Save chat history in separate table 146 | - [x] Upload document into context table 147 | - [x] Allow user to choose context for conversation, or continue the previous one 148 | - [x] Implement alternative embedding models (sentence-transformers) 149 | - [x] Allow user to upload more than one document 150 | - [x] Decouple conversation data from collection data 151 | - [x] Implement Sentence-Transformers for text embedding 152 | - [x] Display sources used by the agent for the answer 153 | - [x] Introduce various agents in the chatbot 154 | - [ ] User can delete unwanted converastions from database 155 | - [ ] Global settings menu (currently in params.py) 156 | - [ ] User is able to upload text from sources other than pdf 157 | - [ ] Improve GUI design 158 | - [ ] Add Whisper for audio-text 159 | -------------------------------------------------------------------------------- /lib/db_handler.py: -------------------------------------------------------------------------------- 1 | """Handles the database connection and queries. 2 | """ 3 | 4 | import os 5 | import sqlite3 6 | import pickle 7 | import pandas as pd 8 | 9 | # import params as prm 10 | from lib.ai_tools import get_embedding_sbert, get_tokens 11 | from lib.cont_proc import create_uuid 12 | 13 | 14 | class DatabaseHandler: 15 | """Handles the database connection and queries. One instance per database file. 16 | """ 17 | 18 | def __init__(self, db_file): 19 | """Create a new database file if it doesn't exist, otherwise connect to the existing database file.""" 20 | self.db_file = db_file 21 | if os.path.exists(db_file): 22 | print(f"Database file '{db_file}' already exists.") 23 | # self.create_connection(db_file) 24 | else: 25 | print(f"Creating a new database file '{db_file}'.") 26 | # self.create_connection(db_file) 27 | 28 | def __enter__(self): 29 | """Enter the context manager.""" 30 | self.create_connection() 31 | return self 32 | 33 | def __exit__(self, exc_type, exc_val, exc_tb): 34 | """Exit the context manager.""" 35 | self.close_connection() 36 | 37 | 38 | 39 | 40 | def create_connection(self, db_file=None): 41 | """Create a database connection to the SQLite database specified by db_file attribute.""" 42 | if db_file is None: 43 | db_file = self.db_file 44 | elif db_file != self.db_file: 45 | print('Warning: db_file argument does not match the db_file attribute. Using the db_file attribute instead.') 46 | db_file = self.db_file 47 | 48 | self.conn = None 49 | try: 50 | self.conn = sqlite3.connect(db_file) 51 | # print(f'Established connection to database \'{db_file}\'') 52 | except Exception as e: 53 | print(e) 54 | 55 | 56 | def close_connection(self): 57 | """Close the connection to the database. 58 | """ 59 | if self.conn is not None: 60 | self.conn.close() 61 | # print(f'Closed connection to database \'{self.db_file}\'.') 62 | else: 63 | print('No connection to close.') 64 | 65 | 66 | def parse_tables(self) -> list: 67 | """Parse the database and return a list of table names. 68 | :return: list of table names 69 | """ 70 | try: 71 | c = self.conn.cursor() 72 | c.execute("SELECT name FROM sqlite_master WHERE type='table';") 73 | return c.fetchall() 74 | except Exception as e: 75 | print(e) 76 | 77 | 78 | def query_db(self, query) -> list: 79 | """Query the database. 80 | :param query: SQL query 81 | :return: list of tuples 82 | """ 83 | try: 84 | c = self.conn.cursor() 85 | c.execute(query) 86 | return c.fetchall() 87 | except Exception as e: 88 | print(e) 89 | 90 | 91 | def write_db(self, query) -> None: 92 | """Write to the database. 93 | :param query: SQL query 94 | :return: None 95 | """ 96 | try: 97 | c = self.conn.cursor() 98 | c.execute(query) 99 | self.conn.commit() 100 | print(f'Query: \'{query}\' executed') 101 | except Exception as e: 102 | print(e) 103 | 104 | 105 | 106 | def insert_session(self, uuid, col_uuid, sname, sdate) -> bool: 107 | """Insert session data into the database's Sessions table. 108 | :param sname: session name 109 | :param sdate: session date 110 | :return: 111 | """ 112 | try: 113 | c = self.conn.cursor() 114 | 115 | c.execute(f""" 116 | INSERT INTO session 117 | VALUES ('{uuid}', '{col_uuid}', '{sname}', '{sdate}') 118 | """) 119 | self.conn.commit() 120 | 121 | print(f"Session: \"{sname}\" inserted into the database with collection: \"{col_uuid}\"") 122 | return True 123 | 124 | except Exception as e: 125 | print(e) 126 | 127 | 128 | def insert_context(self, context_df, table_name='collections', if_exist='append'): 129 | """Insert context data into the database 130 | :param context_df: context dataframe 131 | :return: 132 | """ 133 | try: 134 | context_df.to_sql( 135 | table_name, 136 | self.conn, 137 | if_exists=if_exist, 138 | index=False 139 | ) 140 | print (f"Context data inserted into the {table_name} table") 141 | return True 142 | except Exception as e: 143 | print(e) 144 | return False 145 | 146 | 147 | def insert_embeddings(self, embedding_df, table_name='embeddings', if_exist='append'): 148 | """Insert embeddings data into the database 149 | :param embedding_df: embeddings dataframe 150 | :return: 151 | """ 152 | try: 153 | embedding_df.to_sql( 154 | table_name, 155 | self.conn, 156 | if_exists=if_exist, 157 | index=False 158 | ) 159 | print (f"Embeddings inserted into the {table_name} table") 160 | return True 161 | except Exception as e: 162 | print(e) 163 | return False 164 | 165 | 166 | def insert_interaction( 167 | self, 168 | session_uuid, 169 | inter_type, 170 | message, 171 | page=None, 172 | timestamp=0) -> bool: 173 | """Insert interaction data into the database's Interaction and Embeddings table. 174 | :param inter_type: interaction type 175 | :param message: interaction message 176 | :param page: page number 177 | :param timestamp: timestamp 178 | :return: 179 | """ 180 | 181 | uuid = create_uuid() 182 | embedding = pickle.dumps( 183 | get_embedding_sbert(message) 184 | ) 185 | 186 | num_tokens_oai = len(get_tokens(message)) 187 | 188 | message = message.replace('"', '').replace("'", "") 189 | 190 | # TODO: base data inserting on this approach (?) 191 | query_message = """ 192 | INSERT INTO chat_history 193 | VALUES (?, ?, ?, ?, ?, ?, ?) 194 | """ 195 | query_embedding = """ 196 | INSERT INTO embeddings 197 | VALUES (?, ?) 198 | """ 199 | 200 | try: 201 | c = self.conn.cursor() 202 | c.execute(query_message, (session_uuid, uuid, inter_type, message, num_tokens_oai, page, timestamp)) 203 | c.execute(query_embedding, (uuid, embedding)) 204 | 205 | self.conn.commit() 206 | 207 | print(f"Interaction type: \"{inter_type}\" inserted into the database for session: \"{session_uuid}\"") 208 | 209 | return True 210 | 211 | except Exception as e: 212 | print(e) 213 | 214 | return False 215 | 216 | 217 | def load_context(self, session_uuid, table_name='collections') -> pd.DataFrame: 218 | """Load context data from the database to a dataframe 219 | :param table_name: table name 220 | :return: 221 | """ 222 | 223 | # if session_uuid is not a list, turn it into a list 224 | if not isinstance(session_uuid, list): 225 | session_uuid = [session_uuid] 226 | 227 | # Prepare placeholders for the query 228 | placeholders = ', '.join('?' for _ in session_uuid) 229 | 230 | try: 231 | c = self.conn.cursor() 232 | c.execute( 233 | f"SELECT * FROM {table_name} WHERE UUID in ({placeholders})", 234 | session_uuid 235 | ) 236 | data = c.fetchall() 237 | # get column names 238 | # print([desc[0] for desc in c.description]) 239 | colnames = [desc[0] for desc in c.description] 240 | context_df = pd.DataFrame(data, columns=colnames) 241 | context_df['timestamp'] = pd.to_numeric(context_df['timestamp']) 242 | return context_df 243 | 244 | except Exception as e: 245 | print(e) 246 | 247 | return None 248 | 249 | 250 | def load_embeddings(self, context_df) -> pd.DataFrame: 251 | """enchance context_df with embeddings using sql query 252 | """ 253 | uuids = context_df['doc_uuid'].tolist() 254 | placeholders = ', '.join(['?' for _ in uuids]) # Create placeholders for each UUID 255 | query = f""" 256 | SELECT * FROM embeddings 257 | WHERE UUID in ({placeholders}) 258 | """ 259 | try: 260 | c = self.conn.cursor() 261 | c.execute(query, uuids) # Pass the list of UUIDs as parameters 262 | data = c.fetchall() 263 | # get column names 264 | colnames = [desc[0] for desc in c.description] 265 | embeddings_df = pd.DataFrame(data, columns=colnames) 266 | embeddings_df['embedding'] = embeddings_df['embedding'].apply(lambda x: pickle.loads(x)) 267 | enriched_context_df = context_df.merge(embeddings_df, how='left', left_on='doc_uuid', right_on='uuid') 268 | return enriched_context_df 269 | 270 | except Exception as e: 271 | print(e) 272 | return None 273 | 274 | 275 | 276 | def load_collections(self, session_uuid) -> list: 277 | """Load collection ids associated with the given session id 278 | """ 279 | try: 280 | c = self.conn.cursor() 281 | c.execute(f"SELECT DISTINCT collection_uuid FROM session WHERE UUID = '{session_uuid}'") 282 | data = c.fetchall() 283 | 284 | return [d[0] for d in data] 285 | 286 | except Exception as e: 287 | print(e) 288 | 289 | return None 290 | 291 | 292 | def load_collections_all(self) -> list: 293 | """Load all collection names and uuids available in db.collections 294 | """ 295 | try: 296 | c = self.conn.cursor() 297 | c.execute(f"SELECT DISTINCT uuid, name FROM collections") 298 | data = c.fetchall() 299 | #if data is empty return [] 300 | # if not data: 301 | # return [] 302 | return data 303 | 304 | except Exception as e: 305 | print(e) 306 | 307 | return None 308 | 309 | 310 | # method to load all session names from context table in the database 311 | def load_session_names(self, table_name='session') -> list: 312 | """Load session names and dates from the database to a list 313 | :param table_name: table name 314 | :return: 315 | """ 316 | try: 317 | c = self.conn.cursor() 318 | c.execute(f"SELECT DISTINCT name, uuid, date FROM {table_name}") 319 | data = c.fetchall() 320 | return data 321 | 322 | except Exception as e: 323 | print(e) 324 | 325 | return None 326 | 327 | 328 | # method to delete a given session from the database 329 | def delete_session(self, session_name) -> bool: 330 | """Delete a session from the database 331 | :param session_name: session name 332 | :return: 333 | """ 334 | try: 335 | c = self.conn.cursor() 336 | c.execute(f"DELETE FROM collections WHERE SESSION_NAME = '{session_name}'") 337 | self.conn.commit() 338 | print(f"Session: \"{session_name}\" deleted from the database") 339 | return True 340 | 341 | except Exception as e: 342 | print(e) 343 | 344 | return False -------------------------------------------------------------------------------- /alp.py: -------------------------------------------------------------------------------- 1 | import os 2 | import io 3 | import time 4 | import openai 5 | import datetime 6 | import ast 7 | 8 | from flask import Flask, request, session, render_template, redirect, url_for, jsonify, send_file 9 | from langchain.document_loaders import PyPDFLoader 10 | 11 | ## Local modules import 12 | import lib.cont_proc as cproc 13 | import lib.params as prm 14 | from lib.chatbot import Chatbot 15 | from lib.db_handler import DatabaseHandler 16 | from lib.ai_tools import get_tokens 17 | 18 | # Serve to prod 19 | import webbrowser 20 | from waitress import serve 21 | from threading import Timer 22 | 23 | ########################################################################################## 24 | 25 | # Set up paths 26 | template_folder=os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") 27 | static_folder=os.path.join(os.path.dirname(os.path.abspath(__file__)),"static") 28 | 29 | 30 | # Initiate Flask app 31 | app = Flask( 32 | __name__, 33 | template_folder=template_folder, 34 | static_folder=static_folder 35 | ) 36 | 37 | app.secret_key = os.urandom(24) 38 | 39 | ########################################################################################### 40 | 41 | # Render home page 42 | @app.route('/') 43 | def home(): 44 | return render_template( 45 | 'home.html' 46 | ) 47 | 48 | @app.route('/collection_manager', methods=['GET', 'POST']) 49 | def collection_manager(): 50 | # TODO: fetch collections from database and pass them so they can be displayed 51 | return render_template( 52 | 'collection_manager.html' 53 | ) 54 | 55 | # create /process_collection route 56 | @app.route('/process_collection', methods=['POST']) 57 | def process_collection(): 58 | """Process collection 59 | Process the collection of documents. 60 | """ 61 | print("!Processing collection") 62 | 63 | # Get the data from the form 64 | collection_name = request.form['collection_name'] 65 | embed_method = request.form['embed_method'] 66 | collection_name = cproc.process_name(collection_name) 67 | 68 | # Process the collection 69 | file_ = request.files['pdf'] 70 | file_name = cproc.process_name(file_.filename) 71 | # collection_source = file_name 72 | 73 | # Save the file to the upload folder 74 | saved_fname = collection_name + '_' + file_name 75 | fpath = os.path.join(prm.UPLOAD_FOLDER, saved_fname) 76 | file_.save(fpath) 77 | print(f"!File saved to: {fpath}") 78 | 79 | # Load the pdf & process the text 80 | loader = PyPDFLoader(fpath) # langchain simple pdf loader 81 | pages = loader.load_and_split() # split by pages 82 | 83 | # Process text data further so it fits the context mechanism 84 | pages_df = cproc.pages_to_dataframe(pages) 85 | pages_refined_df = cproc.split_pages(pages_df, method=embed_method) 86 | pages_processed_df = cproc.prepare_for_embed(pages_refined_df, collection_name, embed_method) 87 | 88 | # Add UUIDs to the dataframe! 89 | pages_processed_df['uuid'] = cproc.create_uuid() 90 | pages_processed_df['doc_uuid'] = [cproc.create_uuid() for x in range(pages_processed_df.shape[0])] 91 | 92 | 93 | if embed_method == 'openai': 94 | # Get the embedding cost 95 | embedding_cost = round(cproc.embed_cost(pages_processed_df),4) 96 | # express embedding cost in dollars 97 | embedding_cost = f"${embedding_cost}" 98 | # doc_length = pages_processed_df.shape[0] 99 | # length_warning = doc_length / 600 > 1 100 | print(f"Embedding cost as per Open AI pricing .0004$ = {embedding_cost}") 101 | 102 | pages_embed_df = cproc.embed_pages(pages_processed_df, method=embed_method) 103 | 104 | with db as db_conn: 105 | db_conn.insert_context(pages_embed_df.drop(columns=['embedding'])) 106 | # rename from doc_uuid to uuid needed to fit table structure 107 | db_conn.insert_embeddings(pages_embed_df[['doc_uuid', 'embedding']].rename(columns={'doc_uuid':'uuid'})) 108 | 109 | return render_template( 110 | 'collection_manager.html' 111 | ) 112 | 113 | # Session_manager allows users to create new sessions or continue 114 | # ones already created. 115 | @app.route('/session_manager', methods=['GET', 'POST']) 116 | def session_manager(): 117 | """Session manager 118 | Manage sessions. 119 | """ 120 | 121 | # Load session names from the database 122 | with db as db_conn: 123 | # We want to see available sessions 124 | if db_conn.load_session_names() is not None: 125 | print('!Sessions found in database. Fetching sessions details') 126 | session_names = [x[0] for x in db_conn.load_session_names()] 127 | session_ids = [x[1] for x in db_conn.load_session_names()] 128 | 129 | # extract from session dates only the date YYYY-MM-DD 130 | # session_dates = [x.split()[0] for x in session_dates] 131 | 132 | sessions = list(zip(session_names, session_ids)) 133 | else: 134 | print('!No sessions found in database') 135 | sessions = [] 136 | 137 | # We want to see available collections 138 | collections = db_conn.load_collections_all() 139 | 140 | return render_template( 141 | 'session_manager.html', 142 | sessions=sessions, 143 | collections=collections 144 | ) 145 | 146 | #Create /process_session 147 | @app.route('/process_session', methods=['POST']) 148 | def process_session(): 149 | """Process session 150 | Set the API key, session name, connect sources for new session. 151 | """ 152 | 153 | session['UUID'] = cproc.create_uuid() 154 | session['SESSION_DATE'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 155 | 156 | ## Get the data from the form 157 | 158 | #determine if use clicked session_create or session_start 159 | session_action = request.form.get('session_action', 0) 160 | agent_name = request.form.get('agent_role', 0) 161 | 162 | # Define Agent's role 163 | session['AGENT_NAME'] = agent_name 164 | chatbot.set_agent(session['AGENT_NAME']) 165 | 166 | 167 | # Determine if we deal with new or existing session 168 | # And handle session variables accordingly 169 | 170 | if session_action == 'Start': 171 | name_grabbed = request.form.getlist('existing_session_name') 172 | sesion_id = [ast.literal_eval(x)[1] for x in name_grabbed][0] 173 | name = [ast.literal_eval(x)[0] for x in name_grabbed][0] 174 | print('!Starting existing session: ', name) 175 | session['SESSION_NAME'] = name 176 | session['UUID'] = sesion_id 177 | 178 | return redirect( 179 | url_for('index')) 180 | 181 | 182 | elif session_action == 'Create': 183 | session_name = request.form.get('new_session_name', 0) 184 | session['SESSION_NAME'] = cproc.process_name(session_name) 185 | print('!Creating new session: ', session['SESSION_NAME']) 186 | 187 | # grab collections from the form 188 | collections = request.form.getlist('collections') 189 | 190 | collection_ids = [ast.literal_eval(x)[0] for x in collections] 191 | for collection_uuid in collection_ids: 192 | with db as db_conn: 193 | db_conn.insert_session( 194 | session['UUID'], 195 | collection_uuid, 196 | session['SESSION_NAME'], 197 | session['SESSION_DATE'] 198 | ) 199 | 200 | return redirect( 201 | url_for('index')) 202 | 203 | 204 | @app.route('/interaction') 205 | def index(): 206 | """render interaction main page 207 | """ 208 | 209 | # Load chat history 210 | with db as db_conn: 211 | print('!Loading chat history for initial page load') 212 | chat_history = db_conn.load_context( 213 | session['UUID'], 214 | table_name='chat_history' 215 | ) 216 | 217 | # If chat history is empty it means this is the first interaction 218 | # we need to insert the baseline exchange 219 | if chat_history.empty: 220 | #insert baseline interaction 221 | with db as db_conn: 222 | print('!Inserting baseline interaction') 223 | db_conn.insert_interaction( 224 | session['UUID'], 225 | 'user', 226 | prm.SUMMARY_CTXT_USR 227 | ) 228 | db_conn.insert_interaction( 229 | session['UUID'], 230 | 'assistant', 231 | prm.SUMMARY_TXT_ASST 232 | ) 233 | else: 234 | # Remove seed interactions from the chat history 235 | chat_history = chat_history[chat_history['timestamp'] != 0] 236 | 237 | # Convert the DataFrame to a JSON object so we can pass it to the template 238 | chat_history_json = chat_history.to_dict(orient='records') 239 | 240 | return render_template( 241 | 'index.html', 242 | agent_name=session['AGENT_NAME'], 243 | session_name=session['SESSION_NAME'], 244 | session_date=session['SESSION_DATE'], 245 | session_uuid=session['UUID'], 246 | chat_history=chat_history_json 247 | ) 248 | 249 | 250 | @app.route('/ask', methods=['POST']) 251 | def ask(): 252 | """handle POST request from the form and return the response 253 | """ 254 | 255 | data = request.get_json() 256 | question = data['question'] 257 | 258 | 259 | # Handle chat memory and context 260 | with db as db_conn: 261 | print('!Loading chat history for interaction') 262 | # Form recall tables 263 | collections = db_conn.load_collections(session['UUID']) 264 | recall_table_context = db_conn.load_context(collections) 265 | recall_table_chat = db_conn.load_context(session['UUID'], table_name='chat_history') 266 | 267 | # fetch embeddings from embeddings table for given doc uuids 268 | recall_table_context = db_conn.load_embeddings(recall_table_context) 269 | recall_table_chat = db_conn.load_embeddings(recall_table_chat) 270 | 271 | # Prepare recall tables for user and assistant 272 | recall_table_source = recall_table_context 273 | recall_table_user, recall_table_assistant = cproc.prepare_chat_recall(recall_table_chat) 274 | 275 | recal_embed_source = cproc.convert_table_to_dct(recall_table_source) 276 | recal_embed_user = cproc.convert_table_to_dct(recall_table_user) 277 | recal_embed_assistant = cproc.convert_table_to_dct(recall_table_assistant) 278 | 279 | 280 | ## Get the context from recall table that is the most similar to user input 281 | num_samples = prm.NUM_SAMPLES 282 | if recall_table_source.shape[0] < prm.NUM_SAMPLES: 283 | # This should happen for short documents otherwise this suggests a bug (usually with session name) 284 | num_samples = recall_table_source.shape[0] 285 | print(""" 286 | !WARNING! Source material is shorter than number of samples you want to get. 287 | Setting number of samples to the number of source material sections. 288 | """) 289 | 290 | 291 | # Get the closest index - This will update index attributes of chatbot object 292 | # that are used later to retrieve text and page numbers 293 | 294 | chatbot.retrieve_closest_idx( 295 | question, 296 | num_samples, 297 | recal_embed_source, 298 | recal_embed_user, 299 | recal_embed_assistant 300 | ) 301 | 302 | recal_source, recal_user, recal_agent = chatbot.retrieve_text( 303 | recall_table_context, 304 | recall_table_chat, 305 | ) 306 | 307 | # Look for agent and user messages in the interaction table that have the latest timestamp 308 | # We will put them in the context too. 309 | last_usr_max = recall_table_user['timestamp'].astype(int).max() 310 | last_asst_max = recall_table_assistant['timestamp'].astype(int).max() 311 | if last_usr_max == 0: 312 | latest_user = 'No context found' 313 | else: 314 | latest_user = recall_table_user[recall_table_user['timestamp']==last_usr_max]['text'].values[0] 315 | 316 | if last_asst_max == 0: 317 | latest_assistant = 'No context found' 318 | else: 319 | latest_assistant = recall_table_assistant[recall_table_assistant['timestamp']==last_asst_max]['text'].values[0] 320 | 321 | print('!Done handling chat memory and context.') 322 | 323 | ## Grab the page number from the recall table 324 | ## It will become handy when user wants to know from which chapter the context was taken 325 | recall_source_pages = recall_table_context.loc[chatbot.recall_source_idx][['name','page','text_token_no','text']] 326 | print(f'I will base on the following context:') 327 | print(recall_source_pages) 328 | print('\n') 329 | 330 | 331 | # Build prompt 332 | message = chatbot.build_prompt( 333 | latest_user, 334 | latest_assistant, 335 | recal_source, 336 | recal_user, 337 | recal_agent, 338 | question 339 | ) 340 | print("!Prompt built") 341 | 342 | 343 | # Grab call user content from messages alias - debugging 344 | usr_message_content = message[0]['content'] 345 | # print(usr_message_content[:200]) 346 | 347 | # Count number of tokens in user message and display it to the user 348 | # TODO: flash it on the front-end 349 | 350 | #TODO: bug with message passed to encoder! 351 | 352 | text_passed = '; '.join([x['content'] for x in message]) 353 | token_passed = len(get_tokens(text_passed)) 354 | context_capacity = prm.PROD_MODEL[1] - token_passed 355 | print(f'# Tokens passed to the model: {token_passed}') 356 | print(f'# Tokens left in the context: {context_capacity}') 357 | 358 | 359 | # generate response 360 | response = chatbot.chat_completion_response(message) 361 | 362 | # Format the response so it can be displayed in the front-end 363 | # formatted_cont = response['choices'][0]['message']['content'].replace('\n', '
') 364 | response['choices'][0]['message']['content'] = cproc.format_response(response) 365 | print("!Response generated") 366 | 367 | 368 | # save it all to DB so the agent can remember the conversation 369 | session['SPOT_TIME'] = str(int(time.time())) 370 | with db as db_conn: 371 | print('!Inserting interaction into DB') 372 | # Insert user message into DB so we can use it for another user's input 373 | db_conn.insert_interaction( 374 | session['UUID'], 375 | 'user', 376 | question, 377 | timestamp=session['SPOT_TIME'] 378 | ) 379 | db_conn.insert_interaction( 380 | session['UUID'], 381 | 'assistant', 382 | response['choices'][0]['message']['content'], 383 | timestamp=response['created'] 384 | ) 385 | 386 | return jsonify({'response': response}) 387 | 388 | 389 | @app.route('/export_interactions', methods=['GET']) 390 | def export_interactions(): 391 | """Export the interaction table as a JSON file for download. 392 | """ 393 | 394 | # Connect to the database 395 | with db as db_conn: 396 | print('!Loading chat history for export') 397 | # Retrieve the interaction table 398 | recall_df = db_conn.load_context(session['UUID'], table_name='chat_history') 399 | 400 | # remove records that are user or assistant interaction type and have 401 | # time signature 0 - these were injected into the table as a seed to 402 | # improve performance of the model at the beginning of the conversation 403 | seed_f = ( 404 | (recall_df['interaction_type'].isin(['user','assistant'])) & (recall_df['timestamp'] == 0) 405 | ) 406 | recall_df = recall_df[~seed_f] 407 | 408 | # Convert the DataFrame to a JSON string 409 | interactions_json = recall_df.to_json(orient='records', indent=2) 410 | 411 | # Create a file-like buffer to hold the JSON string 412 | json_buffer = io.BytesIO() 413 | json_buffer.write(interactions_json.encode('utf-8')) 414 | json_buffer.seek(0) 415 | 416 | # Send the JSON file to the user for download 417 | return send_file( 418 | json_buffer, 419 | as_attachment=True, 420 | download_name=f"interactions_{session['SESSION_NAME']}.json", 421 | mimetype='application/json') 422 | 423 | 424 | def open_browser(): 425 | """Open default browser to display the app in PROD mode 426 | """ 427 | webbrowser.open_new('http://127.0.0.1:5000/') 428 | 429 | 430 | 431 | if __name__ == '__main__': 432 | 433 | ## Load key from api_key.txt 434 | with open('./api_key.txt') as f: 435 | key_ = f.read().replace('\n','') #remove trailing '\n' 436 | openai.api_key = key_ 437 | 438 | 439 | # Intitiate database if not exist 440 | db_exist = os.path.exists(prm.DB_PATH) 441 | print(f'!Database exists: {db_exist}') 442 | if not db_exist: 443 | # Initialize the database 444 | db = DatabaseHandler(prm.DB_PATH) 445 | with db as db_conn: 446 | db_conn.write_db(prm.SESSION_TABLE_SQL) 447 | db_conn.write_db(prm.COLLECTIONS_TABLE_SQL) 448 | db_conn.write_db(prm.CHAT_HIST_TABLE_SQL) 449 | db_conn.write_db(prm.EMBEDDINGS_TABLE_SQL) 450 | else: 451 | # Initialize the database 452 | db = DatabaseHandler(prm.DB_PATH) 453 | 454 | # Spin up chatbot instance 455 | chatbot = Chatbot() 456 | print("!Chatbot initialized") 457 | 458 | 459 | # Run DEV server 460 | # app.run(debug=True, host='0.0.0.0', port=5000) 461 | 462 | # # run PROD server 463 | Timer(1, open_browser).start() 464 | serve(app, host='0.0.0.0', port=5000) 465 | --------------------------------------------------------------------------------