diff --git a/lollms/app.py b/lollms/app.py index 6a85f43..387214a 100644 --- a/lollms/app.py +++ b/lollms/app.py @@ -7,13 +7,16 @@ from lollms.config import InstallOption from lollms.helpers import ASCIIColors, trace_exception from lollms.com import NotificationType, NotificationDisplayType, LoLLMsCom from lollms.terminal import MainMenu +from lollms.types import MSG_TYPE, SENDER_TYPES from lollms.utilities import PromptReshaper +from lollms.client_session import Client, Session from safe_store import TextVectorizer, VectorizationMethod, VisualizationMethod from typing import Callable from pathlib import Path from datetime import datetime from functools import partial from socketio import AsyncServer +from typing import Tuple, List import subprocess import importlib import sys, os @@ -55,6 +58,7 @@ class LollmsApplication(LoLLMsCom): self.long_term_memory = None self.tts = None + self.session = Session(lollms_paths) if not free_mode: try: @@ -432,3 +436,317 @@ class LollmsApplication(LoLLMsCom): if file_path.name!=f"{lollms_paths.tool_prefix}local_config.yaml" and file_path.suffix.lower()==".yaml": file_path.unlink() ASCIIColors.info(f"Deleted file: {file_path}") + + # -------------------------------------- Prompt preparing + def prepare_query(self, client_id: str, message_id: int = -1, is_continue: bool = False, n_tokens: int = 0, generation_type = None) -> Tuple[str, str, List[str]]: + """ + Prepares the query for the model. + + Args: + client_id (str): The client ID. + message_id (int): The message ID. Default is -1. + is_continue (bool): Whether the query is a continuation. Default is False. + n_tokens (int): The number of tokens. Default is 0. + + Returns: + Tuple[str, str, List[str]]: The prepared query, original message content, and tokenized query. + """ + if self.personality.callback is None: + self.personality.callback = partial(self.process_chunk, client_id=client_id) + # Get the list of messages + messages = self.session.get_client(client_id).discussion.get_messages() + + # Find the index of the message with the specified message_id + message_index = -1 + for i, message in enumerate(messages): + if message.id == message_id: + message_index = i + break + + # Define current message + current_message = messages[message_index] + + # Build the conditionning text block + conditionning = self.personality.personality_conditioning + + # Check if there are document files to add to the prompt + internet_search_results = "" + internet_search_infos = [] + documentation = "" + knowledge = "" + + + # boosting information + if self.config.positive_boost: + positive_boost="\n!@>important information: "+self.config.positive_boost+"\n" + n_positive_boost = len(self.model.tokenize(positive_boost)) + else: + positive_boost="" + n_positive_boost = 0 + + if self.config.negative_boost: + negative_boost="\n!@>important information: "+self.config.negative_boost+"\n" + n_negative_boost = len(self.model.tokenize(negative_boost)) + else: + negative_boost="" + n_negative_boost = 0 + + if self.config.force_output_language_to_be: + force_language="\n!@>important information: Answer the user in this language :"+self.config.force_output_language_to_be+"\n" + n_force_language = len(self.model.tokenize(force_language)) + else: + force_language="" + n_force_language = 0 + + if self.config.fun_mode: + fun_mode="\n!@>important information: Fun mode activated. In this mode you must answer in a funny playful way. Do not be serious in your answers. Each answer needs to make the user laugh.\n" + n_fun_mode = len(self.model.tokenize(positive_boost)) + else: + fun_mode="" + n_fun_mode = 0 + + discussion = None + if generation_type != "simple_question": + + if self.config.activate_internet_search: + if discussion is None: + discussion = self.recover_discussion(client_id) + if self.config.internet_activate_search_decision: + self.personality.step_start(f"Requesting if {self.personality.name} needs to search internet to answer the user") + need = not self.personality.yes_no(f"Do you have enough information to give a satisfactory answer to {self.config.user_name}'s request without internet search? (If you do not know or you can't answer 0 (no)", discussion) + self.personality.step_end(f"Requesting if {self.personality.name} needs to search internet to answer the user") + self.personality.step("Yes" if need else "No") + else: + need=True + if need: + self.personality.step_start("Crafting internet search query") + query = self.personality.fast_gen(f"!@>discussion:\n{discussion[-2048:]}\n!@>system: Read the discussion and craft a web search query suited to recover needed information to reply to last {self.config.user_name} message.\nDo not answer the prompt. Do not add explanations.\n!@>websearch query: ", max_generation_size=256, show_progress=True, callback=self.personality.sink) + self.personality.step_end("Crafting internet search query") + self.personality.step(f"web search query: {query}") + + self.personality.step_start("Performing Internet search") + + internet_search_results=f"!@>important information: Use the internet search results data to answer {self.config.user_name}'s last message. It is strictly forbidden to give the user an answer without having actual proof from the documentation.\n!@>Web search results:\n" + + docs, sorted_similarities, document_ids = self.personality.internet_search(query, self.config.internet_quick_search) + for doc, infos,document_id in zip(docs, sorted_similarities, document_ids): + internet_search_infos.append(document_id) + internet_search_results += f"search result chunk:\nchunk_infos:{document_id['url']}\nchunk_title:{document_id['title']}\ncontent:{doc}" + self.personality.step_end("Performing Internet search") + + if self.personality.persona_data_vectorizer: + if documentation=="": + documentation="\n!@>important information: Use the documentation data to answer the user questions. If the data is not present in the documentation, please tell the user that the information he is asking for does not exist in the documentation section. It is strictly forbidden to give the user an answer without having actual proof from the documentation.\n!@>Documentation:\n" + + if self.config.data_vectorization_build_keys_words: + if discussion is None: + discussion = self.recover_discussion(client_id) + query = self.personality.fast_gen(f"\n!@>instruction: Read the discussion and rewrite the last prompt for someone who didn't read the entire discussion.\nDo not answer the prompt. Do not add explanations.\n!@>discussion:\n{discussion[-2048:]}\n!@>enhanced query: ", max_generation_size=256, show_progress=True) + ASCIIColors.cyan(f"Query:{query}") + else: + query = current_message.content + try: + docs, sorted_similarities, document_ids = self.personality.persona_data_vectorizer.recover_text(query, top_k=self.config.data_vectorization_nb_chunks) + for doc, infos, doc_id in zip(docs, sorted_similarities, document_ids): + documentation += f"document chunk:\nchunk_infos:{infos}\ncontent:{doc}" + except: + self.warning("Couldn't add documentation to the context. Please verify the vector database") + + if len(self.personality.text_files) > 0 and self.personality.vectorizer: + if documentation=="": + documentation="\n!@>important information: Use the documentation data to answer the user questions. If the data is not present in the documentation, please tell the user that the information he is asking for does not exist in the documentation section. It is strictly forbidden to give the user an answer without having actual proof from the documentation.\n!@>Documentation:\n" + + if self.config.data_vectorization_build_keys_words: + discussion = self.recover_discussion(client_id) + query = self.personality.fast_gen(f"\n!@>instruction: Read the discussion and rewrite the last prompt for someone who didn't read the entire discussion.\nDo not answer the prompt. Do not add explanations.\n!@>discussion:\n{discussion[-2048:]}\n!@>enhanced query: ", max_generation_size=256, show_progress=True) + ASCIIColors.cyan(f"Query: {query}") + else: + query = current_message.content + + try: + docs, sorted_similarities, document_ids = self.personality.vectorizer.recover_text(query, top_k=self.config.data_vectorization_nb_chunks) + for doc, infos in zip(docs, sorted_similarities): + documentation += f"document chunk:\nchunk path: {infos[0]}\nchunk content:{doc}" + documentation += "\n!@>important information: Use the documentation data to answer the user questions. If the data is not present in the documentation, please tell the user that the information he is asking for does not exist in the documentation section. It is strictly forbidden to give the user an answer without having actual proof from the documentation." + except: + self.warning("Couldn't add documentation to the context. Please verify the vector database") + # Check if there is discussion knowledge to add to the prompt + if self.config.activate_ltm and self.long_term_memory is not None: + if knowledge=="": + knowledge="!@>knowledge:\n" + + try: + docs, sorted_similarities, document_ids = self.long_term_memory.recover_text(current_message.content, top_k=self.config.data_vectorization_nb_chunks) + for i,(doc, infos) in enumerate(zip(docs, sorted_similarities)): + knowledge += f"!@>knowledge {i}:\n!@>title:\n{infos[0]}\ncontent:\n{doc}" + except: + self.warning("Couldn't add long term memory information to the context. Please verify the vector database") # Add information about the user + user_description="" + if self.config.use_user_name_in_discussions: + user_description="!@>User description:\n"+self.config.user_description+"\n" + + + # Tokenize the conditionning text and calculate its number of tokens + tokens_conditionning = self.model.tokenize(conditionning) + n_cond_tk = len(tokens_conditionning) + + + # Tokenize the internet search results text and calculate its number of tokens + if len(internet_search_results)>0: + tokens_internet_search_results = self.model.tokenize(internet_search_results) + n_isearch_tk = len(tokens_internet_search_results) + else: + tokens_internet_search_results = [] + n_isearch_tk = 0 + + + # Tokenize the documentation text and calculate its number of tokens + if len(documentation)>0: + tokens_documentation = self.model.tokenize(documentation) + n_doc_tk = len(tokens_documentation) + else: + tokens_documentation = [] + n_doc_tk = 0 + + # Tokenize the knowledge text and calculate its number of tokens + if len(knowledge)>0: + tokens_history = self.model.tokenize(knowledge) + n_history_tk = len(tokens_history) + else: + tokens_history = [] + n_history_tk = 0 + + + # Tokenize user description + if len(user_description)>0: + tokens_user_description = self.model.tokenize(user_description) + n_user_description_tk = len(tokens_user_description) + else: + tokens_user_description = [] + n_user_description_tk = 0 + + + # Calculate the total number of tokens between conditionning, documentation, and knowledge + total_tokens = n_cond_tk + n_isearch_tk + n_doc_tk + n_history_tk + n_user_description_tk + n_positive_boost + n_negative_boost + n_force_language + n_fun_mode + + # Calculate the available space for the messages + available_space = self.config.ctx_size - n_tokens - total_tokens + + # if self.config.debug: + # self.info(f"Tokens summary:\nConditionning:{n_cond_tk}\nn_isearch_tk:{n_isearch_tk}\ndoc:{n_doc_tk}\nhistory:{n_history_tk}\nuser description:{n_user_description_tk}\nAvailable space:{available_space}",10) + + # Raise an error if the available space is 0 or less + if available_space<1: + self.error(f"Not enough space in context!!\nVerify that your vectorization settings for documents or internet search are realistic compared to your context size.\nYou are {available_space} short of context!") + raise Exception("Not enough space in context!!") + + # Accumulate messages until the cumulative number of tokens exceeds available_space + tokens_accumulated = 0 + + + # Initialize a list to store the full messages + full_message_list = [] + # If this is not a continue request, we add the AI prompt + if not is_continue: + message_tokenized = self.model.tokenize( + "\n" +self.personality.ai_message_prefix.strip() + ) + full_message_list.append(message_tokenized) + # Update the cumulative number of tokens + tokens_accumulated += len(message_tokenized) + + + if generation_type != "simple_question": + # Accumulate messages starting from message_index + for i in range(message_index, -1, -1): + message = messages[i] + + # Check if the message content is not empty and visible to the AI + if message.content != '' and ( + message.message_type <= MSG_TYPE.MSG_TYPE_FULL_INVISIBLE_TO_USER.value and message.message_type != MSG_TYPE.MSG_TYPE_FULL_INVISIBLE_TO_AI.value): + + # Tokenize the message content + message_tokenized = self.model.tokenize( + "\n" + self.config.discussion_prompt_separator + message.sender + ": " + message.content.strip()) + + # Check if adding the message will exceed the available space + if tokens_accumulated + len(message_tokenized) > available_space: + break + + # Add the tokenized message to the full_message_list + full_message_list.insert(0, message_tokenized) + + # Update the cumulative number of tokens + tokens_accumulated += len(message_tokenized) + else: + message = messages[message_index] + + # Check if the message content is not empty and visible to the AI + if message.content != '' and ( + message.message_type <= MSG_TYPE.MSG_TYPE_FULL_INVISIBLE_TO_USER.value and message.message_type != MSG_TYPE.MSG_TYPE_FULL_INVISIBLE_TO_AI.value): + + # Tokenize the message content + message_tokenized = self.model.tokenize( + "\n" + self.config.discussion_prompt_separator + message.sender + ": " + message.content.strip()) + + # Add the tokenized message to the full_message_list + full_message_list.insert(0, message_tokenized) + + # Update the cumulative number of tokens + tokens_accumulated += len(message_tokenized) + + # Build the final discussion messages by detokenizing the full_message_list + discussion_messages = "" + for i in range(len(full_message_list)-1): + message_tokens = full_message_list[i] + discussion_messages += self.model.detokenize(message_tokens) + + if len(full_message_list)>0: + ai_prefix = self.model.detokenize(full_message_list[-1]) + else: + ai_prefix = "" + # Build the final prompt by concatenating the conditionning and discussion messages + prompt_data = conditionning + internet_search_results + documentation + knowledge + user_description + discussion_messages + positive_boost + negative_boost + force_language + fun_mode + ai_prefix + + # Tokenize the prompt data + tokens = self.model.tokenize(prompt_data) + + # if this is a debug then show prompt construction details + if self.config["debug"]: + ASCIIColors.bold("CONDITIONNING") + ASCIIColors.yellow(conditionning) + ASCIIColors.bold("INTERNET SEARCH") + ASCIIColors.yellow(internet_search_results) + ASCIIColors.bold("DOC") + ASCIIColors.yellow(documentation) + ASCIIColors.bold("HISTORY") + ASCIIColors.yellow(knowledge) + ASCIIColors.bold("DISCUSSION") + ASCIIColors.hilight(discussion_messages,"!@>",ASCIIColors.color_yellow,ASCIIColors.color_bright_red,False) + ASCIIColors.bold("Final prompt") + ASCIIColors.hilight(prompt_data,"!@>",ASCIIColors.color_yellow,ASCIIColors.color_bright_red,False) + ASCIIColors.info(f"prompt size:{len(tokens)} tokens") + ASCIIColors.info(f"available space after doc and knowledge:{available_space} tokens") + + # self.info(f"Tokens summary:\nPrompt size:{len(tokens)}\nTo generate:{available_space}",10) + + # Details + context_details = { + "conditionning":conditionning, + "internet_search_infos":internet_search_infos, + "internet_search_results":internet_search_results, + "documentation":documentation, + "knowledge":knowledge, + "user_description":user_description, + "discussion_messages":discussion_messages, + "positive_boost":positive_boost, + "negative_boost":negative_boost, + "force_language":force_language, + "fun_mode":fun_mode, + "ai_prefix":ai_prefix + + } + + # Return the prepared query, original message content, and tokenized query + return prompt_data, current_message.content, tokens, context_details, internet_search_infos + diff --git a/lollms/client_session.py b/lollms/client_session.py new file mode 100644 index 0000000..53e00ae --- /dev/null +++ b/lollms/client_session.py @@ -0,0 +1,55 @@ +from lollms.generation import RECEPTION_MANAGER, ROLE_CHANGE_DECISION, ROLE_CHANGE_OURTPUT +from lollms.databases.discussions_database import Discussion, DiscussionsDB +from lollms.paths import LollmsPaths +from threading import Thread + +class Client: + def __init__(self, lollms_paths:LollmsPaths, client_id, discussion:Discussion, db:DiscussionsDB): + self.client_id = client_id + self.discussion = discussion + self.lollms_paths = lollms_paths + + if discussion: + self.discussion_path = lollms_paths.personal_discussions_path/db.discussion_db_name/f"{discussion.discussion_id}" + else: + self.discussion_path = None + self.db_name = db + self.rooms = set() + + self.generated_text = "" + self.cancel_generation = False + self.generation_thread:Thread = None + self.processing = False + self.schedule_for_deletion = False + self.continuing = False + self.first_chunk = True + self.reception_manager = RECEPTION_MANAGER() # Assuming RECEPTION_MANAGER is a global class + + def join_room(self, room_id:str): + self.rooms.add(room_id) + + def leave_room(self, room_id:str): + if room_id in self.rooms: + self.rooms.remove(room_id) + + +class Session: + def __init__(self, lollms_paths:LollmsPaths): + self.clients = {} + self.lollms_paths = lollms_paths + + def add_client(self, client_id, room_id:str, discussion:Discussion, db:DiscussionsDB): + if client_id not in self.clients: + self.clients[client_id] = Client(self.lollms_paths, client_id, discussion, db) + + self.clients[client_id].join_room(room_id) + + def get_client(self, client_id)->Client: + return self.clients.get(client_id) + + def remove_client(self, client_id, room_id): + client = self.get_client(client_id) + if client: + client.leave_room(room_id) + if not client.rooms: + del self.clients[client_id] diff --git a/lollms/databases/discussions_database.py b/lollms/databases/discussions_database.py new file mode 100644 index 0000000..67f63ac --- /dev/null +++ b/lollms/databases/discussions_database.py @@ -0,0 +1,821 @@ + +import sqlite3 +from pathlib import Path +from datetime import datetime +from lollms.helpers import ASCIIColors +from lollms.paths import LollmsPaths +import json + +__author__ = "parisneo" +__github__ = "https://github.com/ParisNeo/lollms-webui" +__copyright__ = "Copyright 2023, " +__license__ = "Apache 2.0" + + +# =================================== Database ================================================================== +class DiscussionsDB: + + def __init__(self, lollms_paths:LollmsPaths, discussion_db_name="default"): + self.lollms_paths = lollms_paths + + self.discussion_db_name = discussion_db_name + self.discussion_db_path = self.lollms_paths.personal_discussions_path/discussion_db_name + + self.discussion_db_path.mkdir(exist_ok=True, parents= True) + self.discussion_db_file_path = self.discussion_db_path/"database.db" + + + def create_tables(self): + db_version = 10 + with sqlite3.connect(self.discussion_db_file_path) as conn: + cursor = conn.cursor() + + cursor.execute(""" + CREATE TABLE IF NOT EXISTS schema_version ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + version INTEGER NOT NULL + ) + """) + + cursor.execute(""" + CREATE TABLE IF NOT EXISTS discussion ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + + cursor.execute(""" + CREATE TABLE IF NOT EXISTS message ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + binding TEXT, + model TEXT, + personality TEXT, + sender TEXT NOT NULL, + content TEXT NOT NULL, + message_type INT NOT NULL, + sender_type INT DEFAULT 0, + rank INT NOT NULL DEFAULT 0, + parent_message_id INT, + created_at TIMESTAMP, + finished_generating_at TIMESTAMP, + discussion_id INTEGER NOT NULL, + metadata TEXT, + ui TEXT, + FOREIGN KEY (discussion_id) REFERENCES discussion(id), + FOREIGN KEY (parent_message_id) REFERENCES message(id) + ) + """) + + cursor.execute("SELECT * FROM schema_version") + row = cursor.fetchone() + + if row is None: + cursor.execute("INSERT INTO schema_version (version) VALUES (?)", (db_version,)) + else: + cursor.execute("UPDATE schema_version SET version = ?", (db_version,)) + + conn.commit() + + def add_missing_columns(self): + with sqlite3.connect(self.discussion_db_file_path) as conn: + cursor = conn.cursor() + + table_columns = { + 'discussion': [ + 'id', + 'title', + 'created_at' + ], + 'message': [ + 'id', + 'binding', + 'model', + 'personality', + 'sender', + 'content', + 'message_type', + 'sender_type', + 'rank', + 'parent_message_id', + 'created_at', + 'metadata', + 'ui', + 'finished_generating_at', + 'discussion_id' + ] + } + + for table, columns in table_columns.items(): + cursor.execute(f"PRAGMA table_info({table})") + existing_columns = [column[1] for column in cursor.fetchall()] + + for column in columns: + if column not in existing_columns: + if column == 'id': + cursor.execute(f"ALTER TABLE {table} ADD COLUMN {column} INTEGER PRIMARY KEY AUTOINCREMENT") + elif column.endswith('_at'): + cursor.execute(f"ALTER TABLE {table} ADD COLUMN {column} TIMESTAMP") + elif column=='metadata': + cursor.execute(f"ALTER TABLE {table} ADD COLUMN {column} TEXT") + elif column=='message_type': + cursor.execute(f"ALTER TABLE {table} RENAME COLUMN type TO {column}") + elif column=='sender_type': + cursor.execute(f"ALTER TABLE {table} ADD COLUMN {column} INT DEFAULT 0") + elif column=='parent_message_id': + cursor.execute(f"ALTER TABLE {table} RENAME COLUMN parent TO {column}") + else: + cursor.execute(f"ALTER TABLE {table} ADD COLUMN {column} TEXT") + ASCIIColors.yellow(f"Added column :{column}") + conn.commit() + + + def select(self, query, params=None, fetch_all=True): + """ + Execute the specified SQL select query on the database, + with optional parameters. + Returns the cursor object for further processing. + """ + with sqlite3.connect(self.discussion_db_file_path) as conn: + if params is None: + cursor = conn.execute(query) + else: + cursor = conn.execute(query, params) + if fetch_all: + return cursor.fetchall() + else: + return cursor.fetchone() + + + def delete(self, query, params=None): + """ + Execute the specified SQL delete query on the database, + with optional parameters. + Returns the cursor object for further processing. + """ + with sqlite3.connect(self.discussion_db_file_path) as conn: + cursor = conn.cursor() + if params is None: + cursor.execute(query) + else: + cursor.execute(query, params) + conn.commit() + + def insert(self, query, params=None): + """ + Execute the specified INSERT SQL query on the database, + with optional parameters. + Returns the ID of the newly inserted row. + """ + + with sqlite3.connect(self.discussion_db_file_path) as conn: + cursor = conn.execute(query, params) + rowid = cursor.lastrowid + conn.commit() + self.conn = None + return rowid + + def update(self, query, params:tuple=None): + """ + Execute the specified Update SQL query on the database, + with optional parameters. + Returns the ID of the newly inserted row. + """ + + with sqlite3.connect(self.discussion_db_file_path) as conn: + conn.execute(query, params) + conn.commit() + + def load_last_discussion(self): + last_discussion_id = self.select("SELECT id FROM discussion ORDER BY id DESC LIMIT 1", fetch_all=False) + if last_discussion_id is None: + last_discussion = self.create_discussion() + last_discussion_id = last_discussion.discussion_id + else: + last_discussion_id = last_discussion_id[0] + self.current_message_id = self.select("SELECT id FROM message WHERE discussion_id=? ORDER BY id DESC LIMIT 1", (last_discussion_id,), fetch_all=False) + return Discussion(last_discussion_id, self) + + def create_discussion(self, title="untitled"): + """Creates a new discussion + + Args: + title (str, optional): The title of the discussion. Defaults to "untitled". + + Returns: + Discussion: A Discussion instance + """ + discussion_id = self.insert(f"INSERT INTO discussion (title) VALUES (?)",(title,)) + return Discussion(discussion_id, self) + + def build_discussion(self, discussion_id=0): + return Discussion(discussion_id, self) + + def get_discussions(self): + rows = self.select("SELECT * FROM discussion") + return [{"id": row[0], "title": row[1]} for row in rows] + + def does_last_discussion_have_messages(self): + last_discussion_id = self.select("SELECT id FROM discussion ORDER BY id DESC LIMIT 1", fetch_all=False) + if last_discussion_id is None: + last_discussion = self.create_discussion() + last_discussion_id = last_discussion.discussion_id + else: + last_discussion_id = last_discussion_id[0] + last_message = self.select("SELECT * FROM message WHERE discussion_id=?", (last_discussion_id,), fetch_all=False) + return last_message is not None + + def remove_discussions(self): + self.delete("DELETE FROM message") + self.delete("DELETE FROM discussion") + + + def export_to_json(self): + """ + Export all discussions and their messages from the database to a JSON format. + + Returns: + list: A list of dictionaries representing discussions and their messages. + Each dictionary contains the discussion ID, title, and a list of messages. + Each message dictionary contains the sender, content, message type, rank, + parent message ID, binding, model, personality, created at, and finished + generating at fields. + """ + db_discussions = self.select("SELECT * FROM discussion") + discussions = [] + for row in db_discussions: + discussion_id = row[0] + discussion_title = row[1] + discussion = {"id": discussion_id, "title":discussion_title, "messages": []} + rows = self.select(f"SELECT sender, content, message_type, rank, parent_message_id, binding, model, personality, created_at, finished_generating_at FROM message WHERE discussion_id=?",(discussion_id,)) + for message_row in rows: + sender = message_row[0] + content = message_row[1] + content_type = message_row[2] + rank = message_row[3] + parent_message_id = message_row[4] + binding = message_row[5] + model = message_row[6] + personality = message_row[7] + created_at = message_row[8] + finished_generating_at = message_row[9] + + discussion["messages"].append( + {"sender": sender, "content": content, "message_type": content_type, "rank": rank, "parent_message_id": parent_message_id, "binding": binding, "model":model, "personality":personality, "created_at":created_at, "finished_generating_at":finished_generating_at} + ) + discussions.append(discussion) + return discussions + + def export_all_as_markdown_list_for_vectorization(self): + """ + Export all discussions and their messages from the database to a Markdown list format. + + Returns: + list: A list of lists representing discussions and their messages in a Markdown format. + Each inner list contains the discussion title and a string representing all + messages in the discussion in a Markdown format. + """ + data = self.export_all_discussions_to_json() + # Initialize an empty result string + discussions = [] + # Iterate through discussions in the JSON data + for discussion in data: + # Extract the title + title = discussion['title'] + messages = "" + # Iterate through messages in the discussion + for message in discussion['messages']: + sender = message['sender'] + content = message['content'] + # Append the sender and content in a Markdown format + messages += f'{sender}: {content}\n' + discussions.append([title, messages]) + return discussions + + def export_all_as_markdown(self): + """ + Export all discussions and their messages from the database to a Markdown format. + + Returns: + str: A string representing all discussions and their messages in a Markdown format. + Each discussion is represented as a Markdown heading, and each message is + represented with the sender and content in a Markdown format. + """ + data = self.export_all_discussions_to_json() + + # Initialize an empty result string + result = '' + + # Iterate through discussions in the JSON data + for discussion in data: + # Extract the title + title = discussion['title'] + # Append the title with '#' as Markdown heading + result += f'#{title}\n' + + # Iterate through messages in the discussion + for message in discussion['messages']: + sender = message['sender'] + content = message['content'] + # Append the sender and content in a Markdown format + result += f'{sender}: {content}\n' + + return result + + def export_all_discussions_to_json(self): + # Convert the list of discussion IDs to a tuple + db_discussions = self.select( + f"SELECT * FROM discussion" + ) + discussions = [] + for row in db_discussions: + discussion_id = row[0] + discussion_title = row[1] + discussion = {"id": discussion_id, "title":discussion_title, "messages": []} + rows = self.select(f"SELECT sender, content, message_type, rank, parent_message_id, binding, model, personality, created_at, finished_generating_at FROM message WHERE discussion_id=?",(discussion_id,)) + for message_row in rows: + sender = message_row[0] + content = message_row[1] + content_type = message_row[2] + rank = message_row[3] + parent_message_id = message_row[4] + binding = message_row[5] + model = message_row[6] + personality = message_row[7] + created_at = message_row[8] + finished_generating_at = message_row[9] + + discussion["messages"].append( + {"sender": sender, "content": content, "message_type": content_type, "rank": rank, "parent_message_id": parent_message_id, "binding": binding, "model":model, "personality":personality, "created_at":created_at, "finished_generating_at": finished_generating_at} + ) + discussions.append(discussion) + return discussions + + def export_discussions_to_json(self, discussions_ids:list): + # Convert the list of discussion IDs to a tuple + discussions_ids_tuple = tuple(discussions_ids) + txt = ','.join(['?'] * len(discussions_ids_tuple)) + db_discussions = self.select( + f"SELECT * FROM discussion WHERE id IN ({txt})", + discussions_ids_tuple + ) + discussions = [] + for row in db_discussions: + discussion_id = row[0] + discussion_title = row[1] + discussion = {"id": discussion_id, "title":discussion_title, "messages": []} + rows = self.select(f"SELECT sender, content, message_type, rank, parent_message_id, binding, model, personality, created_at, finished_generating_at FROM message WHERE discussion_id=?",(discussion_id,)) + for message_row in rows: + sender = message_row[0] + content = message_row[1] + content_type = message_row[2] + rank = message_row[3] + parent_message_id = message_row[4] + binding = message_row[5] + model = message_row[6] + personality = message_row[7] + created_at = message_row[8] + finished_generating_at = message_row[9] + + discussion["messages"].append( + {"sender": sender, "content": content, "message_type": content_type, "rank": rank, "parent_message_id": parent_message_id, "binding": binding, "model":model, "personality":personality, "created_at":created_at, "finished_generating_at": finished_generating_at} + ) + discussions.append(discussion) + return discussions + + def import_from_json(self, json_data): + discussions = [] + data = json_data + for discussion_data in data: + discussion_id = discussion_data.get("id") + discussion_title = discussion_data.get("title") + messages_data = discussion_data.get("messages", []) + discussion = {"id": discussion_id, "title": discussion_title, "messages": []} + + # Insert discussion into the database + discussion_id = self.insert("INSERT INTO discussion (title) VALUES (?)", (discussion_title,)) + + for message_data in messages_data: + sender = message_data.get("sender") + content = message_data.get("content") + content_type = message_data.get("message_type",message_data.get("type")) + rank = message_data.get("rank") + parent_message_id = message_data.get("parent_message_id") + binding = message_data.get("binding","") + model = message_data.get("model","") + personality = message_data.get("personality","") + created_at = message_data.get("created_at",datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + finished_generating_at = message_data.get("finished_generating_at",datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + discussion["messages"].append( + {"sender": sender, "content": content, "message_type": content_type, "rank": rank, "binding": binding, "model": model, "personality": personality, "created_at": created_at, "finished_generating_at": finished_generating_at} + ) + + # Insert message into the database + self.insert("INSERT INTO message (sender, content, message_type, rank, parent_message_id, binding, model, personality, created_at, finished_generating_at, discussion_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + (sender, content, content_type, rank, parent_message_id, binding, model, personality, created_at, finished_generating_at, discussion_id)) + + discussions.append(discussion) + + return discussions + + def export_discussions_to_markdown(self, discussions_ids:list, title = ""): + # Convert the list of discussion IDs to a tuple + discussions_ids_tuple = tuple(discussions_ids) + txt = ','.join(['?'] * len(discussions_ids_tuple)) + db_discussions = self.select( + f"SELECT * FROM discussion WHERE id IN ({txt})", + discussions_ids_tuple + ) + discussions = f"# {title}" if title!="" else "" + for row in db_discussions: + discussion_id = row[0] + discussion_title = row[1] + discussions += f"## {discussion_title}\n" + rows = self.select(f"SELECT sender, content, message_type, rank, parent_message_id, binding, model, personality, created_at, finished_generating_at FROM message WHERE discussion_id=?",(discussion_id,)) + for message_row in rows: + sender = message_row[0] + content = message_row[1] + content_type = message_row[2] + rank = message_row[3] + parent_message_id = message_row[4] + binding = message_row[5] + model = message_row[6] + personality = message_row[7] + created_at = message_row[8] + finished_generating_at = message_row[9] + + discussions +=f"### {sender}:\n{content}\n" + discussions +=f"\n" + return discussions + + +class Message: + def __init__( + self, + discussion_id, + discussions_db, + message_type, + sender_type, + sender, + content, + metadata = None, + ui = None, + rank = 0, + parent_message_id = 0, + binding = "", + model = "", + personality = "", + created_at = None, + finished_generating_at = None, + id = None, + insert_into_db = False + ): + + self.discussion_id = discussion_id + self.discussions_db = discussions_db + self.self = self + self.sender = sender + self.sender_type = sender_type + self.content = content + self.message_type = message_type + self.rank = rank + self.parent_message_id = parent_message_id + self.binding = binding + self.model = model + self.metadata = json.dumps(metadata, indent=4) if metadata is not None and type(metadata)== dict else metadata + self.ui = ui + self.personality = personality + self.created_at = created_at + self.finished_generating_at = finished_generating_at + + if insert_into_db: + self.id = self.discussions_db.insert( + "INSERT INTO message (sender, message_type, sender_type, sender, content, metadata, ui, rank, parent_message_id, binding, model, personality, created_at, finished_generating_at, discussion_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + (sender, message_type, sender_type, sender, content, metadata, ui, rank, parent_message_id, binding, model, personality, created_at, finished_generating_at, discussion_id) + ) + else: + self.id = id + + + @staticmethod + def get_fields(): + return [ + "id", + "message_type", + "sender_type", + "sender", + "content", + "metadata", + "ui", + "rank", + "parent_message_id", + "binding", + "model", + "personality", + "created_at", + "finished_generating_at", + "discussion_id" + ] + + @staticmethod + def from_db(discussions_db, message_id): + columns = Message.get_fields() + rows = discussions_db.select( + f"SELECT {','.join(columns)} FROM message WHERE id=?", (message_id,) + ) + data_dict={ + col:rows[0][i] + for i,col in enumerate(columns) + } + data_dict["discussions_db"]=discussions_db + return Message( + **data_dict + ) + + @staticmethod + def from_dict(discussions_db,data_dict): + data_dict["discussions_db"]=discussions_db + return Message( + **data_dict + ) + + def insert_into_db(self): + self.message_id = self.discussions_db.insert( + "INSERT INTO message (sender, content, metadata, ui, message_type, rank, parent_message_id, binding, model, personality, created_at, finished_generating_at, discussion_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + (self.sender, self.content, self.metadata, self.ui, self.message_type, self.rank, self.parent_message_id, self.binding, self.model, self.personality, self.created_at, self.finished_generating_at, self.discussion_id) + ) + + def update_db(self): + self.message_id = self.discussions_db.insert( + "INSERT INTO message (sender, content, metadata, ui, message_type, rank, parent_message_id, binding, model, personality, created_at, finished_generating_at, discussion_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + (self.sender, self.content, self.metadata, self.ui, self.message_type, self.rank, self.parent_message_id, self.binding, self.model, self.personality, self.created_at, self.finished_generating_at, self.discussion_id) + ) + def update(self, new_content, new_metadata=None, new_ui=None, commit=True): + self.finished_generating_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + text = f"UPDATE message SET content = ?" + params = [new_content] + if new_metadata is not None: + text+=", metadata = ?" + params.append(new_metadata) + if new_ui is not None: + text+=", ui = ?" + params.append(new_ui) + + text +=", finished_generating_at = ? WHERE id = ?" + params.append(self.finished_generating_at) + params.append(self.id) + self.discussions_db.update( + text, tuple(params) + ) + + def to_json(self): + attributes = Message.get_fields() + msgJson = {} + for attribute_name in attributes: + attribute_value = getattr(self, attribute_name, None) + if attribute_name=="metadata": + if type(attribute_value) == str: + msgJson[attribute_name] = json.loads(attribute_value) + else: + msgJson[attribute_name] = attribute_value + else: + msgJson[attribute_name] = attribute_value + return msgJson + +class Discussion: + def __init__(self, discussion_id, discussions_db:DiscussionsDB): + self.discussion_id = discussion_id + self.discussions_db = discussions_db + self.discussion_folder = self.discussions_db.discussion_db_path/f"{discussion_id}" + self.discussion_audio_folder = self.discussion_folder / "audio" + self.discussion_images_folder = self.discussion_folder / "images" + self.discussion_text_folder = self.discussion_folder / "text_data" + self.discussion_skills_folder = self.discussion_folder / "skills" + self.discussion_rag_folder = self.discussion_folder / "rag" + self.discussion_folder.mkdir(exist_ok=True) + self.discussion_images_folder.mkdir(exist_ok=True) + self.discussion_text_folder.mkdir(exist_ok=True) + self.discussion_skills_folder.mkdir(exist_ok=True) + self.discussion_rag_folder.mkdir(exist_ok=True) + self.messages = self.get_messages() + if len(self.messages)>0: + self.current_message = self.messages[-1] + + def add_file(self, file_name): + # TODO : add file + pass + + def load_message(self, id): + """Gets a list of messages information + + Returns: + list: List of entries in the format {"id":message id, "sender":sender name, "content":message content, "message_type":message type, "rank": message rank} + """ + self.current_message = Message.from_db(self.discussions_db, id) + return self.current_message + + def add_message( + self, + message_type, + sender_type, + sender, + content, + metadata=None, + ui=None, + rank=0, + parent_message_id=0, + binding="", + model ="", + personality="", + created_at=None, + finished_generating_at=None + ): + """Adds a new message to the discussion + + Args: + sender (str): The sender name + content (str): The text sent by the sender + + Returns: + int: The added message id + """ + if created_at is None: + created_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + if finished_generating_at is None: + finished_generating_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + self.current_message = Message( + self.discussion_id, + self.discussions_db, + message_type, + sender_type, + sender, + content, + metadata, + ui, + rank, + parent_message_id, + binding, + model, + personality, + created_at, + finished_generating_at, + insert_into_db=True + ) + + self.messages.append(self.current_message) + return self.current_message + + def rename(self, new_title): + """Renames the discussion + + Args: + new_title (str): The nex discussion name + """ + self.discussions_db.update( + f"UPDATE discussion SET title=? WHERE id=?",(new_title,self.discussion_id) + ) + + def title(self): + """Renames the discussion + + Args: + new_title (str): The nex discussion name + """ + rows = self.discussions_db.select( + f"Select title from discussion WHERE id={self.discussion_id}" + ) + return rows[0][0] + + def delete_discussion(self): + """Deletes the discussion + """ + self.discussions_db.delete( + f"DELETE FROM message WHERE discussion_id={self.discussion_id}" + ) + self.discussions_db.delete( + f"DELETE FROM discussion WHERE id={self.discussion_id}" + ) + + def get_messages(self): + """Gets a list of messages information + + Returns: + list: List of entries in the format {"id":message id, "sender":sender name, "content":message content, "message_type":message type, "rank": message rank} + """ + columns = Message.get_fields() + + rows = self.discussions_db.select( + f"SELECT {','.join(columns)} FROM message WHERE discussion_id=?", (self.discussion_id,) + ) + msg_dict = [{ c:row[i] for i,c in enumerate(columns)} for row in rows] + self.messages=[] + for msg in msg_dict: + self.messages.append(Message.from_dict(self.discussions_db, msg)) + + if len(self.messages)>0: + self.current_message = self.messages[-1] + + return self.messages + + def get_message(self, message_id): + for message in self.messages: + if message.id == int(message_id): + self.current_message = message + return message + return None + + def select_message(self, message_id): + msg = self.get_message(message_id) + if msg is not None: + self.current_message = msg + return True + else: + return False + + def update_message(self, new_content, new_metadata=None, new_ui=None): + """Updates the content of a message + + Args: + message_id (int): The id of the message to be changed + new_content (str): The nex message content + """ + self.current_message.update(new_content, new_metadata, new_ui) + + def edit_message(self, message_id, new_content, new_metadata=None, new_ui=None): + """Edits the content of a message + + Args: + message_id (int): The id of the message to be changed + new_content (str): The nex message content + """ + msg = self.get_message(message_id) + if msg: + msg.update(new_content, new_metadata, new_ui) + return True + else: + return False + + + def message_rank_up(self, message_id): + """Increments the rank of the message + + Args: + message_id (int): The id of the message to be changed + """ + # Retrieve current rank value for message_id + current_rank = self.discussions_db.select("SELECT rank FROM message WHERE id=?", (message_id,),False)[0] + + # Increment current rank value by 1 + new_rank = current_rank + 1 + self.discussions_db.update( + f"UPDATE message SET rank = ? WHERE id = ?",(new_rank,message_id) + ) + return new_rank + + def message_rank_down(self, message_id): + """Increments the rank of the message + + Args: + message_id (int): The id of the message to be changed + """ + # Retrieve current rank value for message_id + current_rank = self.discussions_db.select("SELECT rank FROM message WHERE id=?", (message_id,),False)[0] + + # Increment current rank value by 1 + new_rank = current_rank - 1 + self.discussions_db.update( + f"UPDATE message SET rank = ? WHERE id = ?",(new_rank,message_id) + ) + return new_rank + + def delete_message(self, message_id): + """Delete the message + + Args: + message_id (int): The id of the message to be deleted + """ + # Retrieve current rank value for message_id + self.discussions_db.delete("DELETE FROM message WHERE id=?", (message_id,)) + + def export_for_vectorization(self): + """ + Export all discussions and their messages from the database to a Markdown list format. + + Returns: + list: A list of lists representing discussions and their messages in a Markdown format. + Each inner list contains the discussion title and a string representing all + messages in the discussion in a Markdown format. + """ + # Extract the title + title = self.title() + messages = "" + # Iterate through messages in the discussion + for message in self.messages: + sender = message.sender + content = message.content + # Append the sender and content in a Markdown format + messages += f'{sender}: {content}\n' + return title, messages +# ======================================================================================================================== diff --git a/lollms/generation.py b/lollms/generation.py index 7b1efeb..ddd4d6a 100644 --- a/lollms/generation.py +++ b/lollms/generation.py @@ -22,7 +22,7 @@ class ROLE_CHANGE_OURTPUT: self.status = status self.value = value -class RECPTION_MANAGER: +class RECEPTION_MANAGER: done:bool=False chunk:str="" new_role:str="" diff --git a/lollms/personality.py b/lollms/personality.py index 4f0f83d..1a1cb17 100644 --- a/lollms/personality.py +++ b/lollms/personality.py @@ -15,7 +15,7 @@ from lollms.paths import LollmsPaths from lollms.binding import LLMBinding, BindingType from lollms.utilities import PromptReshaper, PackageManager, discussion_path_to_url from lollms.com import NotificationType, NotificationDisplayType - +from lollms.client_session import Session, Client import pkg_resources from pathlib import Path from PIL import Image @@ -928,11 +928,11 @@ class AIPersonality: self.vectorizer = None return True - def add_file(self, path, callback=None): + def add_file(self, path, client:Client, callback=None): + if not self.callback: self.callback = callback - discussion_db_name = self.lollms_paths.personal_discussions_path / "personalities" / self.name / "db.json" - discussion_db_name.parent.mkdir(parents=True, exist_ok=True) + path = Path(path) if path.suffix in [".wav",".mp3"]: self.new_message("") @@ -955,10 +955,11 @@ class AIPersonality: transcription_fn = str(path)+".txt" with open(transcription_fn, "w", encoding="utf-8") as f: f.write(result["text"]) + self.info(f"File saved to {transcription_fn}") self.full(result["text"]) self.step_end("Transcribing ... ") - elif path.suffix in [".png",".jpg",".gif",".bmp",".webp"]: + elif path.suffix in [".png",".jpg",".jpeg",".gif",".bmp",".svg",".webp"]: if self.callback: try: pth = str(path).replace("\\","/").split('/') @@ -1001,7 +1002,7 @@ class AIPersonality: self.vectorizer = TextVectorizer( self.config.data_vectorization_method, # supported "model_embedding" or "tfidf_vectorizer" model=self.model, #needed in case of using model_embedding - database_path=discussion_db_name, + database_path=client.discussion.discussion_rag_folder/"db.json", save_db=self.config.data_vectorization_save_db, data_visualization_method=VisualizationMethod.PCA, database_dict=None) @@ -1013,6 +1014,7 @@ class AIPersonality: self.HideBlockingMessage("Adding file to vector store.\nPlease stand by") return True except Exception as e: + trace_exception(e) self.HideBlockingMessage("Adding file to vector store.\nPlease stand by") self.InfoMessage(f"Unsupported file format or empty file.\nSupported formats are {GenericDataLoader.get_supported_file_types()}") return False diff --git a/lollms/server/elf_server.py b/lollms/server/elf_server.py index 3e85ef7..1255102 100644 --- a/lollms/server/elf_server.py +++ b/lollms/server/elf_server.py @@ -86,10 +86,10 @@ class LOLLMSElfServer(LollmsApplication): return None def prepare_reception(self, client_id): - if not self.connections[client_id]["continuing"]: - self.connections[client_id]["generated_text"] = "" + if not self.session.get_client(client_id).continuing: + self.session.get_client(client_id).generated_text = "" - self.connections[client_id]["first_chunk"]=True + self.session.get_client(client_id).first_chunk=True self.nb_received_tokens = 0 self.start_time = datetime.now() diff --git a/lollms/server/endpoints/lollms_discussion.py b/lollms/server/endpoints/lollms_discussion.py new file mode 100644 index 0000000..55b3c6f --- /dev/null +++ b/lollms/server/endpoints/lollms_discussion.py @@ -0,0 +1,229 @@ +""" +project: lollms_webui +file: lollms_discussion.py +author: ParisNeo +description: + This module contains a set of FastAPI routes that provide information about the Lord of Large Language and Multimodal Systems (LoLLMs) Web UI + application. These routes allow users to manipulate the discussion elements. + +""" +from fastapi import APIRouter, Request +from lollms_webui import LOLLMSWebUI +from pydantic import BaseModel +from starlette.responses import StreamingResponse +from lollms.types import MSG_TYPE +from lollms.utilities import detect_antiprompt, remove_text_from_string, trace_exception +from lollms.security import sanitize_path +from ascii_colors import ASCIIColors +from lollms.databases.discussions_database import DiscussionsDB, Discussion +from typing import List + +from safe_store.text_vectorizer import TextVectorizer, VectorizationMethod, VisualizationMethod +import tqdm +from pathlib import Path +class GenerateRequest(BaseModel): + text: str + +class DatabaseSelectionParameters(BaseModel): + name: str + +class EditTitleParameters(BaseModel): + client_id: str + title: str + id: int + +class MakeTitleParameters(BaseModel): + id: int + +class DeleteDiscussionParameters(BaseModel): + client_id: str + id: int + +# ----------------------- Defining router and main class ------------------------------ + +router = APIRouter() +lollmsElfServer:LOLLMSWebUI = LOLLMSWebUI.get_instance() + + +@router.get("/list_discussions") +def list_discussions(): + discussions = lollmsElfServer.db.get_discussions() + return discussions + + +@router.get("/list_databases") +async def list_databases(): + """List all the personal databases in the LoLLMs server.""" + # Retrieve the list of database names + databases = [f.name for f in lollmsElfServer.lollms_paths.personal_discussions_path.iterdir() if f.is_dir() and (f/"database.db").exists()] + # Return the list of database names + return databases + + +@router.post("/select_database") +def select_database(data:DatabaseSelectionParameters): + sanitize_path(data.name) + print(f'Selecting database {data.name}') + # Create database object + lollmsElfServer.db = DiscussionsDB(lollmsElfServer.lollms_paths, data.name) + ASCIIColors.info("Checking discussions database... ",end="") + lollmsElfServer.db.create_tables() + lollmsElfServer.db.add_missing_columns() + lollmsElfServer.config.discussion_db_name = data.name + ASCIIColors.success("ok") + + if lollmsElfServer.config.auto_save: + lollmsElfServer.config.save_config() + + if lollmsElfServer.config.data_vectorization_activate and lollmsElfServer.config.activate_ltm: + try: + ASCIIColors.yellow("0- Detected discussion vectorization request") + folder = lollmsElfServer.lollms_paths.personal_discussions_path/"vectorized_dbs" + folder.mkdir(parents=True, exist_ok=True) + lollmsElfServer.long_term_memory = TextVectorizer( + vectorization_method=VectorizationMethod.TFIDF_VECTORIZER,#=VectorizationMethod.BM25_VECTORIZER, + database_path=folder/lollmsElfServer.config.discussion_db_name, + data_visualization_method=VisualizationMethod.PCA,#VisualizationMethod.PCA, + save_db=True + ) + ASCIIColors.yellow("1- Exporting discussions") + lollmsElfServer.info("Exporting discussions") + discussions = lollmsElfServer.db.export_all_as_markdown_list_for_vectorization() + ASCIIColors.yellow("2- Adding discussions to vectorizer") + lollmsElfServer.info("Adding discussions to vectorizer") + index = 0 + nb_discussions = len(discussions) + + for (title,discussion) in tqdm(discussions): + lollmsElfServer.sio.emit('update_progress',{'value':int(100*(index/nb_discussions))}) + index += 1 + if discussion!='': + skill = lollmsElfServer.learn_from_discussion(title, discussion) + lollmsElfServer.long_term_memory.add_document(title, skill, chunk_size=lollmsElfServer.config.data_vectorization_chunk_size, overlap_size=lollmsElfServer.config.data_vectorization_overlap_size, force_vectorize=False, add_as_a_bloc=False) + ASCIIColors.yellow("3- Indexing database") + lollmsElfServer.info("Indexing database",True, None) + lollmsElfServer.long_term_memory.index() + ASCIIColors.yellow("Ready") + except Exception as ex: + lollmsElfServer.error(f"Couldn't vectorize the database:{ex}") + return {"status":False} + + return {"status":True} + + +@router.post("/export_discussion") +def export_discussion(): + return {"discussion_text":lollmsElfServer.get_discussion_to()} + + +class DiscussionEditTitle(BaseModel): + client_id: str + title: str + id: int + +@router.post("/edit_title") +async def edit_title(discussion_edit_title: DiscussionEditTitle): + try: + client_id = discussion_edit_title.client_id + title = discussion_edit_title.title + discussion_id = discussion_edit_title.id + lollmsElfServer.session.get_client(client_id).discussion = Discussion(discussion_id, lollmsElfServer.db) + lollmsElfServer.session.get_client(client_id).discussion.rename(title) + return {'status':True} + except Exception as ex: + trace_exception(ex) + lollmsElfServer.error(ex) + return {"status":False,"error":str(ex)} + +class DiscussionTitle(BaseModel): + id: int + +@router.post("/make_title") +async def make_title(discussion_title: DiscussionTitle): + try: + ASCIIColors.info("Making title") + discussion_id = discussion_title.id + discussion = Discussion(discussion_id, lollmsElfServer.db) + title = lollmsElfServer.make_discussion_title(discussion) + discussion.rename(title) + return {'status':True, 'title':title} + except Exception as ex: + trace_exception(ex) + lollmsElfServer.error(ex) + return {"status":False,"error":str(ex)} + + +@router.get("/export") +def export(): + return lollmsElfServer.db.export_to_json() + + + +class DiscussionDelete(BaseModel): + client_id: str + id: int + +@router.post("/delete_discussion") +async def delete_discussion(discussion: DiscussionDelete): + """ + Executes Python code and returns the output. + + :param request: The HTTP request object. + :return: A JSON response with the status of the operation. + """ + + try: + + client_id = discussion.client_id + discussion_id = discussion.id + lollmsElfServer.session.get_client(client_id).discussion = Discussion(discussion_id, lollmsElfServer.db) + lollmsElfServer.session.get_client(client_id).discussion.delete_discussion() + lollmsElfServer.session.get_client(client_id).discussion = None + return {'status':True} + except Exception as ex: + trace_exception(ex) + lollmsElfServer.error(ex) + return {"status":False,"error":str(ex)} + + +# ----------------------------- import/export -------------------- +class DiscussionExport(BaseModel): + discussion_ids: List[int] + export_format: str + +@router.post("/export_multiple_discussions") +async def export_multiple_discussions(discussion_export: DiscussionExport): + try: + discussion_ids = discussion_export.discussion_ids + export_format = discussion_export.export_format + + if export_format=="json": + discussions = lollmsElfServer.db.export_discussions_to_json(discussion_ids) + elif export_format=="markdown": + discussions = lollmsElfServer.db.export_discussions_to_markdown(discussion_ids) + else: + discussions = lollmsElfServer.db.export_discussions_to_markdown(discussion_ids) + return discussions + except Exception as ex: + trace_exception(ex) + lollmsElfServer.error(ex) + return {"status":False,"error":str(ex)} + + +class DiscussionInfo(BaseModel): + id: int + content: str + +class DiscussionImport(BaseModel): + jArray: List[DiscussionInfo] + +@router.post("/import_multiple_discussions") +async def import_multiple_discussions(discussion_import: DiscussionImport): + try: + discussions = discussion_import.jArray + lollmsElfServer.db.import_from_json(discussions) + return discussions + except Exception as ex: + trace_exception(ex) + lollmsElfServer.error(ex) + return {"status":False,"error":str(ex)} diff --git a/lollms/server/endpoints/lollms_generator.py b/lollms/server/endpoints/lollms_generator.py index ed9f094..1631cbb 100644 --- a/lollms/server/endpoints/lollms_generator.py +++ b/lollms/server/endpoints/lollms_generator.py @@ -14,7 +14,7 @@ from pydantic import BaseModel from starlette.responses import StreamingResponse from lollms.types import MSG_TYPE from lollms.utilities import detect_antiprompt, remove_text_from_string, trace_exception -from lollms.generation import RECPTION_MANAGER, ROLE_CHANGE_DECISION, ROLE_CHANGE_OURTPUT +from lollms.generation import RECEPTION_MANAGER, ROLE_CHANGE_DECISION, ROLE_CHANGE_OURTPUT from ascii_colors import ASCIIColors import time import threading @@ -87,7 +87,7 @@ async def lollms_generate(request: LollmsGenerateRequest): """ try: - reception_manager=RECPTION_MANAGER() + reception_manager=RECEPTION_MANAGER() prompt = request.prompt n_predict = request.n_predict if request.n_predict>0 else 1024 stream = request.stream @@ -274,7 +274,7 @@ class GenerationRequest(BaseModel): @router.post("/v1/chat/completions") async def v1_chat_completions(request: GenerationRequest): try: - reception_manager=RECPTION_MANAGER() + reception_manager=RECEPTION_MANAGER() messages = request.messages prompt = "" roles= False diff --git a/lollms/server/endpoints/lollms_user.py b/lollms/server/endpoints/lollms_user.py index 5f5bd99..dbcddf1 100644 --- a/lollms/server/endpoints/lollms_user.py +++ b/lollms/server/endpoints/lollms_user.py @@ -15,7 +15,7 @@ from lollms.types import MSG_TYPE from lollms.main_config import BaseConfig from lollms.utilities import detect_antiprompt, remove_text_from_string from ascii_colors import ASCIIColors -from api.db import DiscussionsDB +from lollms.databases.discussions_database import DiscussionsDB from pathlib import Path from safe_store.text_vectorizer import TextVectorizer, VectorizationMethod, VisualizationMethod import tqdm diff --git a/lollms/server/events/lollms_files_events.py b/lollms/server/events/lollms_files_events.py index 132a8e7..8700479 100644 --- a/lollms/server/events/lollms_files_events.py +++ b/lollms/server/events/lollms_files_events.py @@ -26,7 +26,7 @@ import socketio import threading import os from functools import partial -from api.db import Discussion +from lollms.databases.discussions_database import Discussion from datetime import datetime router = APIRouter() diff --git a/lollms/server/events/lollms_generation_events.py b/lollms/server/events/lollms_generation_events.py index 87bba45..ec6b2ab 100644 --- a/lollms/server/events/lollms_generation_events.py +++ b/lollms/server/events/lollms_generation_events.py @@ -31,10 +31,11 @@ def add_events(sio:socketio): @sio.on('cancel_generation') def cancel_generation(sid): client_id = sid + client = lollmsElfServer.session.get_client(client_id) lollmsElfServer.cancel_gen = True #kill thread ASCIIColors.error(f'Client {sid} requested cancelling generation') - terminate_thread(lollmsElfServer.connections[client_id]['generation_thread']) + terminate_thread(client.generation_thread) ASCIIColors.error(f'Client {sid} canceled generation') lollmsElfServer.busy=False @@ -42,7 +43,8 @@ def add_events(sio:socketio): @sio.on('cancel_text_generation') def cancel_text_generation(sid, data): client_id = sid - lollmsElfServer.connections[client_id]["requested_stop"]=True + client = lollmsElfServer.session.get_client(client_id) + client.requested_stop=True print(f"Client {client_id} requested canceling generation") run_async(partial(lollmsElfServer.sio.emit,"generation_canceled", {"message":"Generation is canceled."}, to=client_id)) lollmsElfServer.busy = False @@ -52,6 +54,7 @@ def add_events(sio:socketio): @sio.on('generate_text') def handle_generate_text(sid, data): client_id = sid + client = lollmsElfServer.session.get_client(client_id) lollmsElfServer.cancel_gen = False ASCIIColors.info(f"Text generation requested by client: {client_id}") if lollmsElfServer.busy: @@ -61,8 +64,8 @@ def add_events(sio:socketio): lollmsElfServer.busy = True try: model = lollmsElfServer.model - lollmsElfServer.connections[client_id]["is_generating"]=True - lollmsElfServer.connections[client_id]["requested_stop"]=False + client.is_generating=True + client.requested_stop=False prompt = data['prompt'] tokenized = model.tokenize(prompt) personality_id = data.get('personality', -1) @@ -90,8 +93,8 @@ def add_events(sio:socketio): if text is not None: lollmsElfServer.answer["full_text"] = lollmsElfServer.answer["full_text"] + text run_async(partial(lollmsElfServer.sio.emit,'text_chunk', {'chunk': text, 'type':MSG_TYPE.MSG_TYPE_CHUNK.value}, to=client_id)) - if client_id in lollmsElfServer.connections:# Client disconnected - if lollmsElfServer.connections[client_id]["requested_stop"]: + if client_id in lollmsElfServer.session.clients.keys():# Client disconnected + if client.requested_stop: return False else: return True @@ -117,8 +120,8 @@ def add_events(sio:socketio): ) ASCIIColors.success(f"\ndone") - if client_id in lollmsElfServer.connections: - if not lollmsElfServer.connections[client_id]["requested_stop"]: + if client_id in lollmsElfServer.session.clients.keys(): + if not client.requested_stop: # Emit the generated text to the client run_async(partial(lollmsElfServer.sio.emit,'text_generated', {'text': generated_text}, to=client_id)) except Exception as ex: @@ -137,7 +140,7 @@ def add_events(sio:socketio): print(f"Text generation requested by client: {client_id}") lollmsElfServer.answer["full_text"] = '' - full_discussion_blocks = lollmsElfServer.connections[client_id]["full_discussion_blocks"] + full_discussion_blocks = client.full_discussion_blocks if prompt != '': if personality.processor is not None and personality.processor_cfg["process_model_input"]: @@ -163,7 +166,7 @@ def add_events(sio:socketio): lollmsElfServer.answer["full_text"] = lollmsElfServer.answer["full_text"] + text run_async(partial(lollmsElfServer.sio.emit,'text_chunk', {'chunk': text}, to=client_id)) try: - if lollmsElfServer.connections[client_id]["requested_stop"]: + if client.requested_stop: return False else: return True @@ -197,8 +200,8 @@ def add_events(sio:socketio): ASCIIColors.error(f"\ndone") lollmsElfServer.busy = False - lollmsElfServer.connections[client_id]['generation_thread'] = threading.Thread(target=do_generation) - lollmsElfServer.connections[client_id]['generation_thread'].start() + client.generation_thread = threading.Thread(target=do_generation) + client.generation_thread.start() ASCIIColors.info("Started generation task") lollmsElfServer.busy=True @@ -214,11 +217,12 @@ def add_events(sio:socketio): @sio.on('generate_msg') def generate_msg(sid, data): client_id = sid + client = lollmsElfServer.session.get_client(client_id) lollmsElfServer.cancel_gen = False - lollmsElfServer.connections[client_id]["generated_text"]="" - lollmsElfServer.connections[client_id]["cancel_generation"]=False - lollmsElfServer.connections[client_id]["continuing"]=False - lollmsElfServer.connections[client_id]["first_chunk"]=True + client.generated_text="" + client.cancel_generation=False + client.continuing=False + client.first_chunk=True @@ -228,15 +232,15 @@ def add_events(sio:socketio): return if not lollmsElfServer.busy: - if lollmsElfServer.connections[client_id]["current_discussion"] is None: + if lollmsElfServer.session.get_client(client_id).discussion is None: if lollmsElfServer.db.does_last_discussion_have_messages(): - lollmsElfServer.connections[client_id]["current_discussion"] = lollmsElfServer.db.create_discussion() + lollmsElfServer.session.get_client(client_id).discussion = lollmsElfServer.db.create_discussion() else: - lollmsElfServer.connections[client_id]["current_discussion"] = lollmsElfServer.db.load_last_discussion() + lollmsElfServer.session.get_client(client_id).discussion = lollmsElfServer.db.load_last_discussion() prompt = data["prompt"] ump = lollmsElfServer.config.discussion_prompt_separator +lollmsElfServer.config.user_name.strip() if lollmsElfServer.config.use_user_name_in_discussions else lollmsElfServer.personality.user_message_prefix - message = lollmsElfServer.connections[client_id]["current_discussion"].add_message( + message = lollmsElfServer.session.get_client(client_id).discussion.add_message( message_type = MSG_TYPE.MSG_TYPE_FULL.value, sender_type = SENDER_TYPES.SENDER_TYPES_USER.value, sender = ump.replace(lollmsElfServer.config.discussion_prompt_separator,"").replace(":",""), @@ -246,8 +250,8 @@ def add_events(sio:socketio): ) ASCIIColors.green("Starting message generation by "+lollmsElfServer.personality.name) - lollmsElfServer.connections[client_id]['generation_thread'] = threading.Thread(target=lollmsElfServer.start_message_generation, args=(message, message.id, client_id)) - lollmsElfServer.connections[client_id]['generation_thread'].start() + client.generation_thread = threading.Thread(target=lollmsElfServer.start_message_generation, args=(message, message.id, client_id)) + client.generation_thread.start() ASCIIColors.info("Started generation task") lollmsElfServer.busy=True #tpe = threading.Thread(target=lollmsElfServer.start_message_generation, args=(message, message_id, client_id)) @@ -258,42 +262,44 @@ def add_events(sio:socketio): @sio.on('generate_msg_from') def generate_msg_from(sid, data): client_id = sid + client = lollmsElfServer.session.get_client(client_id) lollmsElfServer.cancel_gen = False - lollmsElfServer.connections[client_id]["continuing"]=False - lollmsElfServer.connections[client_id]["first_chunk"]=True + client.continuing=False + client.first_chunk=True - if lollmsElfServer.connections[client_id]["current_discussion"] is None: + if lollmsElfServer.session.get_client(client_id).discussion is None: ASCIIColors.warning("Please select a discussion") lollmsElfServer.error("Please select a discussion first", client_id=client_id) return id_ = data['id'] generation_type = data.get('msg_type',None) if id_==-1: - message = lollmsElfServer.connections[client_id]["current_discussion"].current_message + message = lollmsElfServer.session.get_client(client_id).discussion.current_message else: - message = lollmsElfServer.connections[client_id]["current_discussion"].load_message(id_) + message = lollmsElfServer.session.get_client(client_id).discussion.load_message(id_) if message is None: return - lollmsElfServer.connections[client_id]['generation_thread'] = threading.Thread(target=lollmsElfServer.start_message_generation, args=(message, message.id, client_id, False, generation_type)) - lollmsElfServer.connections[client_id]['generation_thread'].start() + client.generation_thread = threading.Thread(target=lollmsElfServer.start_message_generation, args=(message, message.id, client_id, False, generation_type)) + client.generation_thread.start() @sio.on('continue_generate_msg_from') def handle_connection(sid, data): client_id = sid + client = lollmsElfServer.session.get_client(client_id) lollmsElfServer.cancel_gen = False - lollmsElfServer.connections[client_id]["continuing"]=True - lollmsElfServer.connections[client_id]["first_chunk"]=True + client.continuing=True + client.first_chunk=True - if lollmsElfServer.connections[client_id]["current_discussion"] is None: + if lollmsElfServer.session.get_client(client_id).discussion is None: ASCIIColors.yellow("Please select a discussion") lollmsElfServer.error("Please select a discussion", client_id=client_id) return id_ = data['id'] if id_==-1: - message = lollmsElfServer.connections[client_id]["current_discussion"].current_message + message = lollmsElfServer.session.get_client(client_id).discussion.current_message else: - message = lollmsElfServer.connections[client_id]["current_discussion"].load_message(id_) + message = lollmsElfServer.session.get_client(client_id).discussion.load_message(id_) - lollmsElfServer.connections[client_id]["generated_text"]=message.content - lollmsElfServer.connections[client_id]['generation_thread'] = threading.Thread(target=lollmsElfServer.start_message_generation, args=(message, message.id, client_id, True)) - lollmsElfServer.connections[client_id]['generation_thread'].start() + client.generated_text=message.content + client.generation_thread = threading.Thread(target=lollmsElfServer.start_message_generation, args=(message, message.id, client_id, True)) + client.generation_thread.start() diff --git a/lollms/server/events/lollms_personality_events.py b/lollms/server/events/lollms_personality_events.py index fd50a86..899af66 100644 --- a/lollms/server/events/lollms_personality_events.py +++ b/lollms/server/events/lollms_personality_events.py @@ -34,8 +34,10 @@ def add_events(sio:socketio): @sio.on('get_personality_files') def get_personality_files(sid, data): client_id = sid - lollmsElfServer.connections[client_id]["generated_text"] = "" - lollmsElfServer.connections[client_id]["cancel_generation"] = False + client = lollmsElfServer.session.get_client(client_id) + + client.generated_text = "" + client.cancel_generation = False try: lollmsElfServer.personality.setCallback(partial(lollmsElfServer.process_chunk,client_id = client_id)) @@ -58,7 +60,9 @@ def add_events(sio:socketio): @sio.on('send_file_chunk') def send_file_chunk(sid, data): client_id = sid - filename = os.path.basename(data['filename']) # sanitize filename + client = lollmsElfServer.session.get_client(client_id) + + filename:str = os.path.basename(data['filename']) # sanitize filename filename = filename.lower() chunk = data['chunk'] offset = data['offset'] @@ -68,8 +72,14 @@ def add_events(sio:socketio): if not allowed_file(filename): print(f"Invalid file type: {filename}") return + ext = filename.split(".")[-1].lower() + if ext in ["wav", "mp3"]: + path:Path = client.discussion.discussion_audio_folder + elif ext in [".png",".jpg",".jpeg",".gif",".bmp",".svg",".webp"]: + path:Path = client.discussion.discussion_images_folder + else: + path:Path = client.discussion.discussion_text_folder - path:Path = lollmsElfServer.get_uploads_path(client_id) / lollmsElfServer.personality.personality_folder_name path.mkdir(parents=True, exist_ok=True) file_path = path / filename @@ -89,7 +99,7 @@ def add_events(sio:socketio): if lollmsElfServer.personality.processor: result = lollmsElfServer.personality.processor.add_file(file_path, partial(lollmsElfServer.process_chunk, client_id=client_id)) else: - result = lollmsElfServer.personality.add_file(file_path, partial(lollmsElfServer.process_chunk, client_id=client_id)) + result = lollmsElfServer.personality.add_file(file_path, client, partial(lollmsElfServer.process_chunk, client_id=client_id)) ASCIIColors.success('File processed successfully') run_async(partial(sio.emit,'file_received', {'status': True, 'filename': filename})) @@ -100,11 +110,13 @@ def add_events(sio:socketio): @sio.on('execute_command') def execute_command(sid, data): client_id = sid + client = lollmsElfServer.session.get_client(client_id) + lollmsElfServer.cancel_gen = False - lollmsElfServer.connections[client_id]["generated_text"]="" - lollmsElfServer.connections[client_id]["cancel_generation"]=False - lollmsElfServer.connections[client_id]["continuing"]=False - lollmsElfServer.connections[client_id]["first_chunk"]=True + client.generated_text="" + client.cancel_generation=False + client.continuing=False + client.first_chunk=True if not lollmsElfServer.model: ASCIIColors.error("Model not selected. Please select a model") @@ -112,14 +124,14 @@ def add_events(sio:socketio): return if not lollmsElfServer.busy: - if lollmsElfServer.connections[client_id]["current_discussion"] is None: + if lollmsElfServer.session.get_client(client_id).discussion is None: if lollmsElfServer.db.does_last_discussion_have_messages(): - lollmsElfServer.connections[client_id]["current_discussion"] = lollmsElfServer.db.create_discussion() + lollmsElfServer.session.get_client(client_id).discussion = lollmsElfServer.db.create_discussion() else: - lollmsElfServer.connections[client_id]["current_discussion"] = lollmsElfServer.db.load_last_discussion() + lollmsElfServer.session.get_client(client_id).discussion = lollmsElfServer.db.load_last_discussion() ump = lollmsElfServer.config.discussion_prompt_separator +lollmsElfServer.config.user_name.strip() if lollmsElfServer.config.use_user_name_in_discussions else lollmsElfServer.personality.user_message_prefix - message = lollmsElfServer.connections[client_id]["current_discussion"].add_message( + message = lollmsElfServer.session.get_client(client_id).discussion.add_message( message_type = MSG_TYPE.MSG_TYPE_FULL.value, sender_type = SENDER_TYPES.SENDER_TYPES_USER.value, sender = ump.replace(lollmsElfServer.config.discussion_prompt_separator,"").replace(":",""), @@ -130,6 +142,8 @@ def add_events(sio:socketio): lollmsElfServer.busy=True client_id = sid + client = lollmsElfServer.session.get_client(client_id) + command = data["command"] parameters = data["parameters"] lollmsElfServer.prepare_reception(client_id) diff --git a/lollms/services/motion_ctrl/lollms_motion_ctrl.py b/lollms/services/motion_ctrl/lollms_motion_ctrl.py index 458a05b..5a8bbc3 100644 --- a/lollms/services/motion_ctrl/lollms_motion_ctrl.py +++ b/lollms/services/motion_ctrl/lollms_motion_ctrl.py @@ -166,7 +166,7 @@ class LollmsMotionCtrl: self.default_sampler = sampler self.default_steps = steps - self.session = requests.Session() + self.session = requests.Session(lollms_paths) if username and password: self.set_auth(username, password) diff --git a/lollms/services/sd/lollms_sd.py b/lollms/services/sd/lollms_sd.py index ea19eff..5854272 100644 --- a/lollms/services/sd/lollms_sd.py +++ b/lollms/services/sd/lollms_sd.py @@ -273,7 +273,7 @@ class LollmsSD: self.default_sampler = sampler self.default_steps = steps - self.session = requests.Session() + self.session = requests.Session(lollms_paths) if username and password: self.set_auth(username, password)