from sqlalchemy.orm import Session from sqlalchemy.sql.expression import select import database as db MESSAGE_PAGE_SIZE = 256 class Message: def __init__(self, id, content, sender, time=None, kind='message', subkind=None): self.id = id self.content = content self.sender = sender self.time = time self.kind = kind self.subkind = subkind def from_db_record(record): return Message(record.id, record.content, record.sender, record.time) def __str__(self): return f"Message(#{self.id} ({self.time if self.time != None else 'no-time'}) {self.sender}: {self.content.replace("\n", "\\n")})" class Thread: def __init__(self, id, chat_id, name=None, description=None): self.id = id self.chat_id = chat_id self.name = name self.description = description self.messages = [] self.notes = [] self.memories = [] class ChatView: def __init__(self, app, chat_id): self.app = app self.chat_id = chat_id self.name = None self.description = None # Thread holding messages without a set thread id self.default_thread = Thread(None, chat_id) # Most recent thread objects, earliest to latest self.threads = [] # Map of threads by their id self.thread_map = {} # Long term summary of this chat self.summary = None # Separate list for note messages from any thread # Is it better to exclude them or include them from threads? atm they're included self.notes = [] # Id of the thread currently in focus, by default the last thread appearing in the conversation self.current_thread_id = None self._earliest_message_id = None self._load_basic_data() self._load_next_messages(MESSAGE_PAGE_SIZE) def _load_basic_data(self): with self.app.sql_session() as session: record = session.execute( select(db.Chat).where(db.Chat.id == self.chat_id) ).scalars().first() if not record: raise Exception(f"Chat not found with id {self.chat_id}") self.name = record.name self.description = record.description def _load_next_messages(self, load_count): with self.app.sql_session() as session: records_rev = session.execute( select(db.Message).where( db.Message.chat_id == self.chat_id, self._earliest_message_id == None or db.Message.id <= self._earliest_message_id ).order_by(db.Message.time.desc()) ).scalars().fetchmany(load_count) # Update the earliest id for the next load operation if records_rev: self._earliest_message_id = records_rev[0].id if not self.threads: # current thread is the latest known self.current_thread_id = records_rev[-1].thread_id # Go over messages from newest to oldest for record in records_rev: if record.thread_id == None: thread = self.default_thread else: thread = self.thread_map.get(record.thread_id) if not thread: thread = Thread(record.thread_id, self.chat_id) self.thread_map[thread.id] = thread self.threads.insert(0, thread) # This is the oldest message so far, add it to the start of the chat message = Message(record.id, record.content, record.sender, record.time, record.kind, record.subkind) if message.kind == 'note': thread.notes.insert(0, message) elif message.kind == 'memory': thread.memories.insert(0, message) else: thread.messages.insert(0, message) def get_thread(self, thread_id): if thread_id == None: return self.default_thread return self.thread_map.get(thread_id) def get_current_thread(self): # TODO: if !thread return self.get_thread(self.current_thread_id) def debug_string(self): result_lines = [] def _print(line): result_lines.append(line) def print_thread(thread, title=None): if title: _print(title + ":") elif thread.name: _print(thread.name + ":") else: _print(f"Thread {thread.id}:") for message in thread.messages: _print(" " + str(message)) print_thread(self.default_thread, "Default Thread") for thread in self.threads: print_thread(thread) _print("Current thread id: " + str(self.current_thread_id)) return "\n".join(result_lines)