From 5bdd911cf464b62bd94899d38a78ea3c2a16692b Mon Sep 17 00:00:00 2001 From: milo Date: Tue, 6 May 2025 11:13:15 -0400 Subject: [PATCH] First push from FORK client --- .vscode/settings.json | 18 ++ Obsolete/bert_subject_summariser.py | 87 ++++++++++ Obsolete/cleaner.py | 97 +++++++++++ Obsolete/compose.yml | 19 +++ Obsolete/credentials.json | 1 + Obsolete/database.py | 32 ++++ Obsolete/gmail_to_db_test.py | 135 +++++++++++++++ Obsolete/initialize_db.py | 93 +++++++++++ Obsolete/insight,py | 58 +++++++ Obsolete/labeler.py | 115 +++++++++++++ Obsolete/migrations.py | 54 ++++++ Obsolete/nlp_summary.py | 89 ++++++++++ Obsolete/requirements.txt | 6 + Obsolete/smart_labler.py | 135 +++++++++++++++ Obsolete/subject_summariser.py | 95 +++++++++++ Obsolete/test.py | 41 +++++ Obsolete/test_gmail.py | 67 ++++++++ __pycache__/nlp_summary.cpython-310.pyc | Bin 0 -> 1806 bytes config/accounts.yml | 4 + config/labels.yml | 63 +++++++ config/settings.yml | 0 .../database_manager.cpython-310.pyc | Bin 0 -> 5646 bytes src/db/database_manager.py | 155 ++++++++++++++++++ .../__pycache__/gmail_client.cpython-310.pyc | Bin 0 -> 863 bytes src/gmail/gmail_client.py | 20 +++ src/gmail/gmail_parser.py | 22 +++ src/main.py | 23 +++ src/models/llm_engine.py | 19 +++ .../__pycache__/assistant.cpython-310.pyc | Bin 0 -> 1861 bytes src/orchestrator/assistant.py | 54 ++++++ .../__pycache__/cleaner.cpython-310.pyc | Bin 0 -> 886 bytes .../__pycache__/labeler.cpython-310.pyc | Bin 0 -> 690 bytes .../__pycache__/summarizer.cpython-310.pyc | Bin 0 -> 732 bytes src/processor/cleaner.py | 17 ++ src/processor/labeler.py | 14 ++ src/processor/summarizer.py | 14 ++ src/utils/__pycache__/logger.cpython-310.pyc | Bin 0 -> 1765 bytes src/utils/logger.py | 44 +++++ src/utils/scheduler.py | 14 ++ ui/streamlit_app/Home.py | 19 +++ ui/streamlit_app/__init__.py | 0 .../__pycache__/utils.cpython-310.pyc | Bin 0 -> 1042 bytes ui/streamlit_app/pages/EmailViewer.py | 45 +++++ ui/streamlit_app/pages/LabelManager.py | 18 ++ ui/streamlit_app/pages/Settings.py | 21 +++ ui/streamlit_app/utils.py | 24 +++ 46 files changed, 1732 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 Obsolete/bert_subject_summariser.py create mode 100644 Obsolete/cleaner.py create mode 100644 Obsolete/compose.yml create mode 100644 Obsolete/credentials.json create mode 100644 Obsolete/database.py create mode 100644 Obsolete/gmail_to_db_test.py create mode 100644 Obsolete/initialize_db.py create mode 100644 Obsolete/insight,py create mode 100644 Obsolete/labeler.py create mode 100644 Obsolete/migrations.py create mode 100644 Obsolete/nlp_summary.py create mode 100644 Obsolete/requirements.txt create mode 100644 Obsolete/smart_labler.py create mode 100644 Obsolete/subject_summariser.py create mode 100644 Obsolete/test.py create mode 100644 Obsolete/test_gmail.py create mode 100644 __pycache__/nlp_summary.cpython-310.pyc create mode 100644 config/accounts.yml create mode 100644 config/labels.yml create mode 100644 config/settings.yml create mode 100644 src/db/__pycache__/database_manager.cpython-310.pyc create mode 100644 src/db/database_manager.py create mode 100644 src/gmail/__pycache__/gmail_client.cpython-310.pyc create mode 100644 src/gmail/gmail_client.py create mode 100644 src/gmail/gmail_parser.py create mode 100644 src/main.py create mode 100644 src/models/llm_engine.py create mode 100644 src/orchestrator/__pycache__/assistant.cpython-310.pyc create mode 100644 src/orchestrator/assistant.py create mode 100644 src/processor/__pycache__/cleaner.cpython-310.pyc create mode 100644 src/processor/__pycache__/labeler.cpython-310.pyc create mode 100644 src/processor/__pycache__/summarizer.cpython-310.pyc create mode 100644 src/processor/cleaner.py create mode 100644 src/processor/labeler.py create mode 100644 src/processor/summarizer.py create mode 100644 src/utils/__pycache__/logger.cpython-310.pyc create mode 100644 src/utils/logger.py create mode 100644 src/utils/scheduler.py create mode 100644 ui/streamlit_app/Home.py create mode 100644 ui/streamlit_app/__init__.py create mode 100644 ui/streamlit_app/__pycache__/utils.cpython-310.pyc create mode 100644 ui/streamlit_app/pages/EmailViewer.py create mode 100644 ui/streamlit_app/pages/LabelManager.py create mode 100644 ui/streamlit_app/pages/Settings.py create mode 100644 ui/streamlit_app/utils.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1a56bde --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "sqltools.connections": [ + { + "mysqlOptions": { + "authProtocol": "default", + "enableSsl": "Disabled" + }, + "previewLimit": 50, + "server": "localhost", + "port": 3306, + "driver": "MariaDB", + "name": "emailassistant", + "database": "emailassistant", + "username": "emailuser", + "password": "miguel33020" + } + ] +} \ No newline at end of file diff --git a/Obsolete/bert_subject_summariser.py b/Obsolete/bert_subject_summariser.py new file mode 100644 index 0000000..719b8dc --- /dev/null +++ b/Obsolete/bert_subject_summariser.py @@ -0,0 +1,87 @@ +from keybert import KeyBERT +from sentence_transformers import SentenceTransformer +import mysql.connector +import os + +# === Load multilingual KeyBERT model === +model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2') +kw_model = KeyBERT(model) + +# === DB Credentials === +DB_HOST = os.getenv("DB_HOST", "localhost") +DB_PORT = int(os.getenv("DB_PORT", 3306)) +DB_USER = os.getenv("DB_USER", "emailuser") +DB_PASSWORD = os.getenv("DB_PASSWORD", "miguel33020") +DB_NAME = os.getenv("DB_NAME", "emailassistant") + +# === Connect to DB === +conn = mysql.connector.connect( + host=DB_HOST, + port=DB_PORT, + user=DB_USER, + password=DB_PASSWORD, + database=DB_NAME +) +cursor = conn.cursor(dictionary=True) + +# === Logging Helper === +def log_event(cursor, level, source, message): + try: + cursor.execute( + "INSERT INTO logs (level, source, message) VALUES (%s, %s, %s)", + (level, source, message) + ) + except Exception as e: + print(f"[LOG ERROR] Failed to log event: {e}") + +# === Subject-Based Summarization Using KeyBERT === +def summarize_subject(subject): + keywords = kw_model.extract_keywords( + subject, + keyphrase_ngram_range=(1, 2), + stop_words='english', + top_n=1 + ) + + summary = keywords[0][0] if keywords else subject + confidence = round(len(summary.split()) / max(1, len(subject.split())), 2) + + if len(summary.split()) < 1 or confidence < 0.2: + return subject, 1.0 + + return summary.strip(), confidence + +# === Fetch emails === +cursor.execute("SELECT id, subject FROM emails") +emails = cursor.fetchall() + +# === Main Processing Loop === +for email in emails: + email_id = email["id"] + subject = email["subject"] + + if not subject or not subject.strip(): + log_event(cursor, "WARNING", "subject_summarizer", f"Skipped empty subject for email ID {email_id}") + continue + + try: + summary, confidence = summarize_subject(subject) + + cursor.execute(""" + UPDATE emails + SET ai_summary = %s, + ai_confidence = %s + WHERE id = %s + """, (summary, confidence, email_id)) + + log_event(cursor, "INFO", "subject_summarizer", f"Subject summarized for email ID {email_id}") + print(f"✅ Subject summarized for email {email_id} (confidence: {confidence})") + + except Exception as e: + log_event(cursor, "ERROR", "subject_summarizer", f"Error on email ID {email_id}: {str(e)}") + print(f"❌ Error summarizing subject for email {email_id}: {e}") + +# === Commit & Close === +conn.commit() +cursor.close() +conn.close() diff --git a/Obsolete/cleaner.py b/Obsolete/cleaner.py new file mode 100644 index 0000000..53ca9d1 --- /dev/null +++ b/Obsolete/cleaner.py @@ -0,0 +1,97 @@ +import mysql.connector +import json +import re +import spacy +from bs4 import BeautifulSoup +from datetime import datetime + +# === Load spaCy model === +nlp = spacy.load("en_core_web_sm") + +# === Logging helper === +def log_event(cursor, level, source, message): + cursor.execute( + "INSERT INTO logs (level, source, message) VALUES (%s, %s, %s)", + (level, source, message) + ) + +# === Extract all links from body === +def extract_links(text): + return re.findall(r'https?://[^\s<>()"]+', text) + +# === Extract unsubscribe links === +def extract_unsubscribe_link(text): + # Match links that contain the word "unsubscribe" + matches = re.findall(r'(https?://[^\s()"]*unsubscribe[^\s()"]*)', text, re.IGNORECASE) + if matches: + return matches[0] # Return the first match + return None + +# === Clean email body === +def clean_body(body): + soup = BeautifulSoup(body, "html.parser") + return soup.get_text(separator=' ', strip=True) + +# === Main cleaning logic === +def clean_emails(): + conn = mysql.connector.connect( + host="localhost", + user="emailuser", + password="miguel33020", + database="emailassistant" + ) + cursor = conn.cursor(dictionary=True) + + cursor.execute("SELECT * FROM emails WHERE body IS NOT NULL") + emails = cursor.fetchall() + + for email in emails: + email_id = email["id"] + body = email["body"] + + cleaned_body = clean_body(body) + links = extract_links(cleaned_body) + unsubscribe_link = extract_unsubscribe_link(cleaned_body) + + # Attempt to parse attachments + attachments_data = None + if email.get("attachments"): + try: + attachments_data = json.loads(email["attachments"]) + except json.JSONDecodeError: + try: + # Quick fix: replace single quotes with double quotes + attachments_data = json.loads(email["attachments"].replace("'", '"')) + log_event(cursor, "WARNING", "cleaner", f"Auto-corrected JSON in attachments (email ID {email_id})") + except Exception as e2: + log_event(cursor, "ERROR", "cleaner", f"Attachment parse failed (ID {email_id}): {str(e2)}") + attachments_data = None + + # Update database + try: + cursor.execute(""" + UPDATE emails + SET body = %s, + links = %s, + unsubscribe_data = %s, + attachments = %s + WHERE id = %s + """, ( + cleaned_body, + json.dumps(links), + unsubscribe_link, + json.dumps(attachments_data) if attachments_data else None, + email_id + )) + conn.commit() + print(f"✅ Cleaned email {email_id}") + log_event(cursor, "INFO", "cleaner", f"Successfully cleaned email ID {email_id}") + except Exception as e: + print(f"❌ Error updating email {email_id}: {e}") + log_event(cursor, "ERROR", "cleaner", f"DB update failed for email ID {email_id}: {str(e)}") + + cursor.close() + conn.close() + +if __name__ == "__main__": + clean_emails() diff --git a/Obsolete/compose.yml b/Obsolete/compose.yml new file mode 100644 index 0000000..6e4fb25 --- /dev/null +++ b/Obsolete/compose.yml @@ -0,0 +1,19 @@ +version: '3.8' + +services: + mariadb: + image: lscr.io/linuxserver/mariadb:latest + container_name: mariadb + environment: + - PUID=1000 + - PGID=1000 + - TZ=Etc/UTC + - MYSQL_ROOT_PASSWORD=miguel33020 + - MYSQL_DATABASE=emailassistant + - MYSQL_USER=emailuser + - MYSQL_PASSWORD=miguel33020 + volumes: + - C:/Users/migue/mariadb_config:/config + ports: + - 3306:3306 + restart: unless-stopped diff --git a/Obsolete/credentials.json b/Obsolete/credentials.json new file mode 100644 index 0000000..696d59b --- /dev/null +++ b/Obsolete/credentials.json @@ -0,0 +1 @@ +{"installed":{"client_id":"712638107230-am5njg9pf0aj9plh1kbtv2h085dveo1q.apps.googleusercontent.com","project_id":"ez-email-agent","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"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","redirect_uris":["http://localhost"]}} \ No newline at end of file diff --git a/Obsolete/database.py b/Obsolete/database.py new file mode 100644 index 0000000..421a353 --- /dev/null +++ b/Obsolete/database.py @@ -0,0 +1,32 @@ +import mysql.connector +import os + +# Load database credentials from environment variables +DB_HOST = os.getenv("DB_HOST", "localhost") # Your server's IP +DB_PORT = int(os.getenv("DB_PORT", "3306")) # Convert port to integer +DB_USER = os.getenv("DB_USER", "emailuser") +DB_PASSWORD = os.getenv("DB_PASSWORD", "miguel33020") +DB_NAME = os.getenv("DB_NAME", "emailassistant") + +def connect_db(): + try: + conn = mysql.connector.connect( + host=DB_HOST, + port=DB_PORT, # Now it's an integer + user=DB_USER, + password=DB_PASSWORD, + database=DB_NAME + ) + print("✅ Connected to MariaDB successfully!") + return conn + except mysql.connector.Error as err: + print(f"❌ Error: {err}") + return None + +# Test connection +if __name__ == "__main__": + conn = connect_db() + if conn: + conn.close() + + diff --git a/Obsolete/gmail_to_db_test.py b/Obsolete/gmail_to_db_test.py new file mode 100644 index 0000000..d10402a --- /dev/null +++ b/Obsolete/gmail_to_db_test.py @@ -0,0 +1,135 @@ +from google_auth_oauthlib.flow import InstalledAppFlow +from googleapiclient.discovery import build +import base64 +import mysql.connector +import os +import yaml +import datetime +from initialize_db import initialize_database + +SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"] + +# === Load DB credentials === +DB_HOST = os.getenv("DB_HOST", "localhost") +DB_PORT = int(os.getenv("DB_PORT", 3306)) +DB_USER = os.getenv("DB_USER", "emailuser") +DB_PASSWORD = os.getenv("DB_PASSWORD", "miguel33020") +DB_NAME = os.getenv("DB_NAME", "emailassistant") + +def authenticate_gmail(): + flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES) + creds = flow.run_local_server(port=0) + return build("gmail", "v1", credentials=creds) + +def get_header(headers, name): + for h in headers: + if h["name"].lower() == name.lower(): + return h["value"] + return None + +def decode_body(payload): + if "data" in payload.get("body", {}): + return base64.urlsafe_b64decode(payload["body"]["data"]).decode("utf-8", errors="ignore") + elif "parts" in payload: + for part in payload["parts"]: + if part.get("mimeType") == "text/plain" and "data" in part.get("body", {}): + return base64.urlsafe_b64decode(part["body"]["data"]).decode("utf-8", errors="ignore") + return "" + +def insert_into_db(email_data): + conn = mysql.connector.connect( + host=DB_HOST, + port=DB_PORT, + user=DB_USER, + password=DB_PASSWORD, + database=DB_NAME + ) + cursor = conn.cursor() + + query = """ + INSERT IGNORE INTO emails ( + user, account, message_id, thread_id, account_id, sender, cc, subject, body, links, + received_at, folder, attachments, is_read, labels, + ai_category, ai_confidence, ai_summary, + processing_status, sync_status, attachment_path, downloaded + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, + %s, %s, %s, %s, %s, + %s, %s, %s, + %s, %s, %s, %s) + """ + + try: + cursor.execute(query, ( + email_data.get("user", "default_user"), + email_data.get("account", "main_account"), + email_data["message_id"], + email_data["thread_id"], + email_data.get("account_id", ""), + email_data["sender"], + email_data["cc"], + email_data["subject"], + email_data["body"], + email_data.get("links", ""), + email_data["received_at"], + email_data.get("folder", "inbox"), + email_data["attachments"], + False, + email_data["labels"], + None, None, None, + "unprocessed", + "synced", + None, + False + )) + print(f"✅ Stored: {email_data['subject'][:60]}...") + except Exception as e: + print("❌ Error inserting into DB:", e) + + conn.commit() + cursor.close() + conn.close() + +def fetch_and_store_emails(service): + results = service.users().messages().list(userId="me", maxResults=500).execute() + messages = results.get("messages", []) + + for msg in messages: + msg_data = service.users().messages().get(userId="me", id=msg["id"]).execute() + + payload = msg_data.get("payload", {}) + headers = payload.get("headers", []) + + sender = get_header(headers, "From") + cc = get_header(headers, "Cc") + subject = get_header(headers, "Subject") + date_str = get_header(headers, "Date") + body = decode_body(payload) + + try: + received_at = datetime.datetime.strptime(date_str, "%a, %d %b %Y %H:%M:%S %z") + except: + received_at = datetime.datetime.utcnow() + + email_data = { + "user": "default_user", + "account": "main_account", + "message_id": msg_data["id"], + "thread_id": msg_data.get("threadId"), + "account_id": "", + "sender": sender, + "cc": cc, + "subject": subject, + "body": body, + "links": "", # Placeholder, will be populated by AI + "received_at": received_at, + "folder": "inbox", + "attachments": str(payload.get("parts", [])), + "labels": str(msg_data.get("labelIds", [])) + } + + insert_into_db(email_data) + +if __name__ == "__main__": + initialize_database() + gmail_service = authenticate_gmail() + fetch_and_store_emails(gmail_service) diff --git a/Obsolete/initialize_db.py b/Obsolete/initialize_db.py new file mode 100644 index 0000000..6d6dc6b --- /dev/null +++ b/Obsolete/initialize_db.py @@ -0,0 +1,93 @@ +import mysql.connector +import yaml +import os + +def initialize_database(): + # === Load config file === + with open("config.yml", "r") as file: + config = yaml.safe_load(file) + + # === DB Connection === + conn = mysql.connector.connect( + host=os.getenv("DB_HOST", "localhost"), + port=os.getenv("DB_PORT", 3306), + user=os.getenv("DB_USER", "emailuser"), + password=os.getenv("DB_PASSWORD", "miguel33020"), + database=os.getenv("DB_NAME", "emailassistant") + ) + cursor = conn.cursor() + + # === Table: metadata (previously main_account) === + cursor.execute(""" + CREATE TABLE IF NOT EXISTS metadata ( + id INT AUTO_INCREMENT PRIMARY KEY, + user VARCHAR(255), + email VARCHAR(255) UNIQUE NOT NULL, + token TEXT + ); + """) + print("✅ Table ready: metadata") + +# === Table: emails === + cursor.execute(""" + CREATE TABLE IF NOT EXISTS emails ( + id INT AUTO_INCREMENT PRIMARY KEY, + user VARCHAR(255), + account VARCHAR(255), + message_id VARCHAR(255) UNIQUE, + thread_id VARCHAR(255), + account_id VARCHAR(255), + sender VARCHAR(255), + cc TEXT, + subject TEXT, + body LONGTEXT, + links LONGTEXT, + unsubscribe_data TEXT, + received_at DATETIME, + folder VARCHAR(50), + attachments LONGTEXT, + is_read BOOLEAN DEFAULT FALSE, + labels LONGTEXT, + + -- 🔍 AI-Generated Fields + ai_category VARCHAR(100), -- Top-level (e.g. 'promo') + ai_confidence FLOAT, -- Confidence score + ai_summary TEXT, -- Summary of subject/body + ai_keywords TEXT, -- Comma-separated extracted keywords + ai_label_source VARCHAR(100), -- 'subject', 'body', 'combined', 'llm' + summary_source VARCHAR(100), -- Similar to above + ai_model_version VARCHAR(100), -- Versioning helps long-term debugging + is_ai_reviewed BOOLEAN DEFAULT FALSE, -- Was this fully processed by AI? + processing_notes TEXT, -- Optional notes about fallback, etc. + + -- 🔄 Sync and Processing Status + processing_status VARCHAR(50), + sync_status VARCHAR(50), + attachment_path TEXT, + downloaded BOOLEAN DEFAULT FALSE + ); + """) + print("✅ Table ready: emails") + + + # === Table: logs === + cursor.execute(""" + CREATE TABLE IF NOT EXISTS logs ( + id INT AUTO_INCREMENT PRIMARY KEY, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + level VARCHAR(20), + source VARCHAR(255), + message TEXT + ); + """) + print("✅ Table ready: logs") + + cursor.close() + conn.close() + +# if __name__ == "__main__": +# initialize_database() + + +#if __name__ == "__main__": +# initialize_database() diff --git a/Obsolete/insight,py b/Obsolete/insight,py new file mode 100644 index 0000000..9540fe7 --- /dev/null +++ b/Obsolete/insight,py @@ -0,0 +1,58 @@ +import os +import mysql.connector +from keybert import KeyBERT +from sentence_transformers import SentenceTransformer +from collections import Counter + +# === Load multilingual model for KeyBERT === +model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2") +kw_model = KeyBERT(model) + +# === DB Credentials === +DB_HOST = os.getenv("DB_HOST", "localhost") +DB_PORT = int(os.getenv("DB_PORT", 3306)) +DB_USER = os.getenv("DB_USER", "emailuser") +DB_PASSWORD = os.getenv("DB_PASSWORD", "miguel33020") +DB_NAME = os.getenv("DB_NAME", "emailassistant") + +# === Connect to DB === +conn = mysql.connector.connect( + host=DB_HOST, + port=DB_PORT, + user=DB_USER, + password=DB_PASSWORD, + database=DB_NAME +) +cursor = conn.cursor(dictionary=True) + +# === Fetch only unlabeled emails === +cursor.execute("SELECT id, subject FROM emails WHERE ai_category = 'unlabeled'") +emails = cursor.fetchall() + +print(f"🔍 Analyzing {len(emails)} unlabeled emails...") + +keyword_counter = Counter() + +for email in emails: + subject = email["subject"] + if not subject: + continue + + try: + keywords = kw_model.extract_keywords( + subject, + keyphrase_ngram_range=(1, 2), + stop_words="english", + top_n=5 + ) + keyword_counter.update([kw[0].lower() for kw in keywords]) + except Exception as e: + print(f"❌ Error processing email ID {email['id']}: {e}") + +# === Output top missing keywords === +print("\n📊 Top keywords in unlabeled emails:") +for word, count in keyword_counter.most_common(30): + print(f"{word}: {count}") + +cursor.close() +conn.close() diff --git a/Obsolete/labeler.py b/Obsolete/labeler.py new file mode 100644 index 0000000..0540943 --- /dev/null +++ b/Obsolete/labeler.py @@ -0,0 +1,115 @@ +import os +import yaml +import mysql.connector +from keybert import KeyBERT +from sentence_transformers import SentenceTransformer + + +# === Load multilingual model for KeyBERT === +model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2") +kw_model = KeyBERT(model) + +# === Load label hierarchy from YAML === +LABEL_FILE = os.getenv("LABEL_CONFIG_PATH", "labels.yml") +with open(LABEL_FILE, "r", encoding="utf-8") as f: + label_config = yaml.safe_load(f) + +print(f"📂 Using label config: {LABEL_FILE}") +print(label_config) + + +# === DB Credentials === +DB_HOST = os.getenv("DB_HOST", "localhost") +DB_PORT = int(os.getenv("DB_PORT", 3306)) +DB_USER = os.getenv("DB_USER", "emailuser") +DB_PASSWORD = os.getenv("DB_PASSWORD", "miguel33020") +DB_NAME = os.getenv("DB_NAME", "emailassistant") + +# === Connect to DB === +conn = mysql.connector.connect( + host=DB_HOST, + port=DB_PORT, + user=DB_USER, + password=DB_PASSWORD, + database=DB_NAME +) +cursor = conn.cursor(dictionary=True) + +# === Logging Helper === +def log_event(cursor, level, source, message): + cursor.execute( + "INSERT INTO logs (level, source, message) VALUES (%s, %s, %s)", + (level, source, message) + ) + +# === Recursive label matcher === +def match_labels(keywords, label_tree, prefix=""): + for label, data in label_tree.items(): + full_label = f"{prefix}/{label}".strip("/") + label_keywords = [kw.lower() for kw in data.get("keywords", [])] + + # First check children + children = data.get("children", {}) + child_match = match_labels(keywords, children, prefix=full_label) + if child_match: + return child_match + + # Then check this level (so children take priority) + if any(kw in keywords for kw in label_keywords): + return full_label + + return None + + + +# === Fetch emails that haven't been labeled === +cursor.execute("SELECT id, subject, ai_category FROM emails") +emails = cursor.fetchall() + +# === Main Labeling Loop === +for email in emails: + email_id = email["id"] + subject = email["subject"] + current_label = email["ai_category"] + +# if current_label not in [None, "None", ""]: +# print(f"ℹ️ Email {email_id} already has label '{current_label}'") +# continue + + if not subject or not subject.strip(): + log_event(cursor, "WARNING", "labeler", f"Skipped empty subject for email ID {email_id}") + continue + + try: + keywords = kw_model.extract_keywords( + subject, + keyphrase_ngram_range=(1, 2), + stop_words="english", + top_n=5 + ) + keyword_set = set(k[0].lower() for k in keywords) + label = match_labels(keyword_set, label_config) or "unlabeled" + + cursor.execute(""" + UPDATE emails + SET ai_category = %s, + ai_keywords = %s, + ai_label_source = %s, + ai_confidence = %s, + is_ai_reviewed = FALSE + WHERE id = %s + """, (label, ", ".join(keyword_set), "labeler_v1.0", 1.0, email_id)) + + + log_event(cursor, "INFO", "labeler", f"Labeled email {email_id} as '{label}'") + print(f"🏷️ Email {email_id} labeled as: {label}") + + + except Exception as e: + log_event(cursor, "ERROR", "labeler", f"Error labeling email ID {email_id}: {str(e)}") + print(f"❌ Error labeling email {email_id}: {e}") + +# === Commit & Close === +conn.commit() +cursor.close() +conn.close() diff --git a/Obsolete/migrations.py b/Obsolete/migrations.py new file mode 100644 index 0000000..450a6cc --- /dev/null +++ b/Obsolete/migrations.py @@ -0,0 +1,54 @@ +import os +import mysql.connector +from datetime import datetime + +# === DB Credentials === +DB_HOST = os.getenv("DB_HOST", "localhost") +DB_PORT = int(os.getenv("DB_PORT", 3306)) +DB_USER = os.getenv("DB_USER", "emailuser") +DB_PASSWORD = os.getenv("DB_PASSWORD", "miguel33020") +DB_NAME = os.getenv("DB_NAME", "emailassistant") + +# === Connect to DB === +conn = mysql.connector.connect( + host=DB_HOST, + port=DB_PORT, + user=DB_USER, + password=DB_PASSWORD, + database=DB_NAME +) +cursor = conn.cursor() + +# === Logging Helper === +def log_event(cursor, level, source, message): + cursor.execute( + "INSERT INTO logs (level, source, message, timestamp) VALUES (%s, %s, %s, %s)", + (level, source, message, datetime.now()) + ) + +# === Migration Commands === +migration_commands = [ + "ALTER TABLE emails ADD COLUMN IF NOT EXISTS ai_keywords TEXT;", + "ALTER TABLE emails ADD COLUMN IF NOT EXISTS ai_label_source VARCHAR(100);", + "ALTER TABLE emails ADD COLUMN IF NOT EXISTS summary_source VARCHAR(100);", + "ALTER TABLE emails ADD COLUMN IF NOT EXISTS ai_model_version VARCHAR(100);", + "ALTER TABLE emails ADD COLUMN IF NOT EXISTS is_ai_reviewed BOOLEAN DEFAULT FALSE;", + "ALTER TABLE emails ADD COLUMN IF NOT EXISTS processing_notes TEXT;", +] + +# === Apply Migrations === +print("🚀 Starting migrations...") +for cmd in migration_commands: + try: + cursor.execute(cmd) + log_event(cursor, "INFO", "migrations", f"Executed: {cmd}") + print(f"✅ Executed: {cmd}") + except mysql.connector.Error as err: + log_event(cursor, "WARNING", "migrations", f"Skipped or failed: {cmd} -> {err}") + print(f"⚠️ Skipped or failed: {cmd} -> {err}") + +# === Commit & Close === +conn.commit() +cursor.close() +conn.close() +print("✅ Migration complete.") diff --git a/Obsolete/nlp_summary.py b/Obsolete/nlp_summary.py new file mode 100644 index 0000000..33e3f88 --- /dev/null +++ b/Obsolete/nlp_summary.py @@ -0,0 +1,89 @@ +import spacy +import mysql.connector +import os +import sys +from collections import Counter +from string import punctuation + +# === Load spaCy model === +nlp = spacy.load("en_core_web_sm") + +# === DB Credentials === +DB_HOST = os.getenv("DB_HOST", "localhost") +DB_PORT = int(os.getenv("DB_PORT", 3306)) +DB_USER = os.getenv("DB_USER", "emailuser") +DB_PASSWORD = os.getenv("DB_PASSWORD", "miguel33020") +DB_NAME = os.getenv("DB_NAME", "emailassistant") + +# === Connect to DB === +conn = mysql.connector.connect( + host=DB_HOST, + port=DB_PORT, + user=DB_USER, + password=DB_PASSWORD, + database=DB_NAME +) +cursor = conn.cursor(dictionary=True) + +# === Logging Helper === +def log_event(cursor, level, source, message): + cursor.execute( + "INSERT INTO logs (level, source, message) VALUES (%s, %s, %s)", + (level, source, message) + ) + +# === Summarization Logic === +def summarize(text, max_sentences=3): + doc = nlp(text) + words = [token.text.lower() for token in doc if token.is_alpha and not token.is_stop] + word_freq = Counter(words) + + sentence_scores = {} + for sent in doc.sents: + for word in sent: + if word.text.lower() in word_freq: + sentence_scores[sent] = sentence_scores.get(sent, 0) + word_freq[word.text.lower()] + + summarized = sorted(sentence_scores, key=sentence_scores.get, reverse=True)[:max_sentences] + return " ".join(str(s) for s in summarized) + +# === Fetch All Emails with Missing Summaries === +cursor.execute("SELECT id, body FROM emails WHERE ai_summary IS NULL") +emails = cursor.fetchall() + +# === Main Processing Loop === +for email in emails: + email_id = email["id"] + body = email["body"] + + if not body or not body.strip(): + log_event(cursor, "WARNING", "summarizer", f"Skipped empty body for email ID {email_id}") + continue + + try: + summary = summarize(body) + if not summary.strip(): + summary = "No meaningful summary could be generated." + + # Optional confidence (ratio of summary length to original body) + confidence = round(len(summary.split()) / max(1, len(body.split())), 2) + + # Update email + cursor.execute(""" + UPDATE emails + SET ai_summary = %s, + ai_confidence = %s + WHERE id = %s + """, (summary, confidence, email_id)) + + log_event(cursor, "INFO", "summarizer", f"Summarized email ID {email_id}") + print(f"✅ Summarized email {email_id} (confidence: {confidence})") + + except Exception as e: + log_event(cursor, "ERROR", "summarizer", f"Error summarizing email ID {email_id}: {str(e)}") + print(f"❌ Error summarizing email {email_id}: {e}") + +# === Commit & Close === +conn.commit() +cursor.close() +conn.close() diff --git a/Obsolete/requirements.txt b/Obsolete/requirements.txt new file mode 100644 index 0000000..76320e1 --- /dev/null +++ b/Obsolete/requirements.txt @@ -0,0 +1,6 @@ +google-auth +google-auth-oauthlib +google-auth-httplib2 +google-api-python-client +openai +transformers diff --git a/Obsolete/smart_labler.py b/Obsolete/smart_labler.py new file mode 100644 index 0000000..6cd8601 --- /dev/null +++ b/Obsolete/smart_labler.py @@ -0,0 +1,135 @@ +import os +import ast +import yaml +import mysql.connector +from keybert import KeyBERT +from sentence_transformers import SentenceTransformer +from collections import Counter + +# === Load multilingual model for KeyBERT === +model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2") +kw_model = KeyBERT(model) + +# === Load label hierarchy from YAML === +LABEL_FILE = os.getenv("LABEL_CONFIG_PATH", "labels.yml") +with open(LABEL_FILE, "r", encoding="utf-8") as f: + label_config = yaml.safe_load(f) + +# === DB Credentials === +DB_HOST = os.getenv("DB_HOST", "localhost") +DB_PORT = int(os.getenv("DB_PORT", 3306)) +DB_USER = os.getenv("DB_USER", "emailuser") +DB_PASSWORD = os.getenv("DB_PASSWORD", "miguel33020") +DB_NAME = os.getenv("DB_NAME", "emailassistant") + +# === Connect to DB === +conn = mysql.connector.connect( + host=DB_HOST, + port=DB_PORT, + user=DB_USER, + password=DB_PASSWORD, + database=DB_NAME +) +cursor = conn.cursor(dictionary=True) + +# === Logging Helper === +def log_event(cursor, level, source, message): + try: + cursor.execute( + "INSERT INTO logs (level, source, message) VALUES (%s, %s, %s)", + (level, source, message) + ) + except: + print(f"[LOG ERROR] {level} from {source}: {message}") + +# === Recursive label matcher === +def match_labels(keywords, label_tree, prefix=""): + for label, data in label_tree.items(): + full_label = f"{prefix}/{label}".strip("/") + label_keywords = [kw.lower() for kw in data.get("keywords", [])] + if any(kw in keywords for kw in label_keywords): + children = data.get("children", {}) + child_match = match_labels(keywords, children, prefix=full_label) + return child_match if child_match else full_label + return None + +# === Smart Label Aggregator === +def smart_label(email): + votes = [] + + # 1. FROM address rules + from_addr = email.get("sender", "").lower() + if any(x in from_addr for x in ["paypal", "bankofamerica", "chase"]): + votes.append("bank") + if "indeed" in from_addr or "hiring" in from_addr: + votes.append("job") + + # 2. Subject keyword analysis + subject = email.get("subject", "") + if subject: + keywords = kw_model.extract_keywords( + subject, keyphrase_ngram_range=(1, 2), stop_words="english", top_n=5 + ) + keyword_set = set(k[0].lower() for k in keywords) + label_from_subject = match_labels(keyword_set, label_config) + if label_from_subject: + votes.append(label_from_subject) + + # 3. AI summary matching + summary = email.get("ai_summary", "").lower() + if "payment" in summary or "transaction" in summary: + votes.append("bank") + if "your order" in summary or "delivered" in summary: + votes.append("promo") + + # 4. Gmail label logic (from "labels" column) + raw_label = email.get("labels", "") + try: + gmail_labels = ast.literal_eval(raw_label) if raw_label else [] + gmail_labels = [label.upper() for label in gmail_labels] + except (ValueError, SyntaxError): + gmail_labels = [] + + if "CATEGORY_PROMOTIONS" in gmail_labels: + votes.append("promo") + elif "CATEGORY_SOCIAL" in gmail_labels: + votes.append("social") + elif "CATEGORY_UPDATES" in gmail_labels: + votes.append("work") + elif "IMPORTANT" in gmail_labels: + votes.append("work") + + # 5. Count votes + label_counts = Counter(votes) + return label_counts.most_common(1)[0][0] if label_counts else "unlabeled" + +# === Fetch unlabeled emails === +cursor.execute("SELECT id, sender, subject, ai_summary, labels, ai_category FROM emails") + +emails = cursor.fetchall() +print(f"📬 Found {len(emails)} total emails for re-labeling") + +# === Main Labeling Loop === +for email in emails: + email_id = email["id"] + try: + label = smart_label(email) + cursor.execute(""" + UPDATE emails + SET ai_category = %s, + ai_label_source = %s, + is_ai_reviewed = FALSE + WHERE id = %s + """, (label, "smart_labeler", email_id)) + + log_event(cursor, "INFO", "smart_labeler", f"Labeled email {email_id} as '{label}'") + print(f"🏷️ Email {email_id} labeled as: {label}") + + except Exception as e: + log_event(cursor, "ERROR", "smart_labeler", f"Error labeling email {email_id}: {str(e)}") + print(f"❌ Error labeling email {email_id}: {e}") + +# === Commit & Close === +conn.commit() +cursor.close() +conn.close() diff --git a/Obsolete/subject_summariser.py b/Obsolete/subject_summariser.py new file mode 100644 index 0000000..d3019d5 --- /dev/null +++ b/Obsolete/subject_summariser.py @@ -0,0 +1,95 @@ +import spacy +import mysql.connector +import os +import sys +from collections import Counter + +# === Load spaCy model === +nlp = spacy.load("en_core_web_sm") + +# === DB Credentials === +DB_HOST = os.getenv("DB_HOST", "localhost") +DB_PORT = int(os.getenv("DB_PORT", 3306)) +DB_USER = os.getenv("DB_USER", "emailuser") +DB_PASSWORD = os.getenv("DB_PASSWORD", "miguel33020") +DB_NAME = os.getenv("DB_NAME", "emailassistant") + +# === Connect to DB === +conn = mysql.connector.connect( + host=DB_HOST, + port=DB_PORT, + user=DB_USER, + password=DB_PASSWORD, + database=DB_NAME +) +cursor = conn.cursor(dictionary=True) + +# === Logging Helper === +def log_event(cursor, level, source, message): + cursor.execute( + "INSERT INTO logs (level, source, message) VALUES (%s, %s, %s)", + (level, source, message) + ) + +# === Subject-Based Summarization === +def summarize_subject(subject): + doc = nlp(subject) + keywords = [token.text for token in doc if token.is_alpha and not token.is_stop] + if not keywords: + return subject, 1.0 # fallback to raw subject + + # Prioritize noun chunks that include keywords + noun_chunks = list(doc.noun_chunks) + chunks = [chunk.text for chunk in noun_chunks if any(tok.text in keywords for tok in chunk)] + + # Combine and limit summary length + compressed = " ".join(chunks or keywords) + compressed_words = compressed.split() + subject_word_count = len(subject.split()) + summary = " ".join(compressed_words[:max(1, subject_word_count - 1)]).strip() + + # Confidence is relative to subject word count + confidence = round(len(summary.split()) / max(1, subject_word_count), 2) + + # Fallback if summary is too short or confidence too low + if len(summary.split()) < 2 or confidence < 0.3: + return subject, 1.0 + + return summary, confidence + +# === Fetch emails with NULL ai_summary === +cursor.execute("SELECT id, subject FROM emails") +emails = cursor.fetchall() + +# === Main Processing Loop === +# === Main Processing Loop === +for email in emails: + email_id = email["id"] + subject = email["subject"] + + if not subject or not subject.strip(): + log_event(cursor, "WARNING", "subject_summarizer", f"Skipped empty subject for email ID {email_id}") + continue + + try: + summary, confidence = summarize_subject(subject) + + cursor.execute(""" + UPDATE emails + SET ai_summary = %s, + ai_confidence = %s + WHERE id = %s + """, (summary, confidence, email_id)) + + log_event(cursor, "INFO", "subject_summarizer", f"Subject summarized for email ID {email_id}") + print(f"✅ Subject summarized for email {email_id} (confidence: {confidence})") + + except Exception as e: + log_event(cursor, "ERROR", "subject_summarizer", f"Error on email ID {email_id}: {str(e)}") + print(f"❌ Error summarizing subject for email {email_id}: {e}") + + +# === Commit & Close === +conn.commit() +cursor.close() +conn.close() diff --git a/Obsolete/test.py b/Obsolete/test.py new file mode 100644 index 0000000..fbd7179 --- /dev/null +++ b/Obsolete/test.py @@ -0,0 +1,41 @@ +import requests +import time + +API_URL = "http://192.168.1.100:11434/api/generate" +MODEL = "tinyllama:1.1b" + +prompt_text = ( + "You are a professional AI assistant. Read the following email and briefly explain what it's about " + "as if you were summarizing it for your busy boss.\n\n" + "Be concise, clear, and include names, requests, deadlines, and project names if mentioned.\n\n" + "Email:\n" + "\"Hi there, just checking in to see if you received my last message about the invoice due next week. " + "Please let me know when you get a chance.\"" +) + +payload = { + "model": MODEL, + "prompt": prompt_text, + "stream": False +} + +def run_summary_pass(pass_label): + print(f"\n🔁 {pass_label} run for model: {MODEL}") + start_time = time.time() + response = requests.post(API_URL, json=payload) + end_time = time.time() + + if response.status_code == 200: + result = response.json().get("response") + else: + result = f"❌ Error: {response.status_code} - {response.text}" + + elapsed = end_time - start_time + print(f"🧠 Summary: {result}") + print(f"⏱️ Time taken: {elapsed:.2f} seconds") + +# Warm-up run (model loading) +run_summary_pass("Warm-up") + +# Second run (real performance) +run_summary_pass("Performance") diff --git a/Obsolete/test_gmail.py b/Obsolete/test_gmail.py new file mode 100644 index 0000000..74ba665 --- /dev/null +++ b/Obsolete/test_gmail.py @@ -0,0 +1,67 @@ +from google_auth_oauthlib.flow import InstalledAppFlow +from googleapiclient.discovery import build +import nlp_summary + +SCOPES = ["https://www.googleapis.com/auth/gmail.modify"] +nlp = nlp_summary.load("en_core_web_sm") + +# Define keyword-based categories +CATEGORIES = { + "Work": ["meeting", "deadline", "project", "report"], + "Finance": ["invoice", "bill", "receipt", "payment", "tax"], + "Security": ["verification", "sign in attempt", "password"], + "Promotions": ["sale", "deal", "offer", "discount", "promotion"], + "Events": ["webinar", "conference", "event", "invitation"] +} + +def authenticate_gmail(): + flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES) + creds = flow.run_local_server(port=0) + return build("gmail", "v1", credentials=creds) + +def categorize_email(subject): + doc = nlp(subject.lower()) + for category, keywords in CATEGORIES.items(): + if any(word in doc.text for word in keywords): + return category + return "Uncategorized" + +def list_and_categorize_emails(service): + results = service.users().messages().list(userId="me", maxResults=10).execute() + messages = results.get("messages", []) + + for msg in messages: + msg_data = service.users().messages().get(userId="me", id=msg["id"]).execute() + subject = msg_data.get("snippet", "No Subject") + category = categorize_email(subject) + + print(f"📩 Subject: {subject}") + print(f" 🏷️ Category: {category}\n") + + # Apply the category label in Gmail + label_email(service, msg["id"], category) + +def label_email(service, message_id, category): + label_id = get_or_create_label(service, category) + service.users().messages().modify( + userId="me", + id=message_id, + body={"addLabelIds": [label_id]} + ).execute() + +def get_or_create_label(service, label_name): + labels = service.users().labels().list(userId="me").execute().get("labels", []) + for label in labels: + if label["name"].lower() == label_name.lower(): + return label["id"] + + # Create a new label if not found + label = service.users().labels().create( + userId="me", + body={"name": label_name, "labelListVisibility": "labelShow"} + ).execute() + return label["id"] + +if __name__ == "__main__": + gmail_service = authenticate_gmail() + list_and_categorize_emails(gmail_service) diff --git a/__pycache__/nlp_summary.cpython-310.pyc b/__pycache__/nlp_summary.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9ebfc60d5f629c87a82c54580c2d4394ee568418 GIT binary patch literal 1806 zcmZt{OK%fLcxLwLR}zRJc7S$^+Jn_94dsSH)D*i4Qbh*!qO+RsJu}~YkJ+SBDIyrVf4)9CGZFerF&_>K zm~UZ zl)%a>v_9j1MZbe@7!7}9%af~9l<#^i$IFu(Q`|<4D{oY+b{kk)cal+@G7e;EFpB+j z-dU4t?q@HiC4-%eA zJ>SM2fc|O1skEt=dWW819dNiv17A7o@zb;0?bc4~PR9x8SI%KVPo3}g_wG6Q-okmX z-P&(CUeFVxC<1HeZrj;C*x7M*?%uoGac-=?(I6Z?Q9*e(3V@-m&>fiKX8kSOu)6Vm05jlqEAt#A0xh=851KjsWv5{ec>~v;)!e!og!tT0n_38Av^4C#f{ToyaA6tOrJ-R&e|&W?B3ZMjZM zCE3|J0!23t2lGU%4NhfoBZSoZNi^8}O!=-Zavhyro8>AvV{03E$%#!M=9XMty#vl^ zPuQunI6DSc*jwy;6cd$Mp=J%hPbwzX7(+7gbNmCe#}FO^2qsp&potXj%U8irV)6d6 z{Hl@^Ho$v!GT@tEs_j&)$+OGo?CSqHIyY_DQiJ<)bt!2G6&J1?;dLd@j#w&90fo+} z)cZ*gHww90RwD1Dr_5RGGa)sa_|i}vAY?(I-XqS2a&d0a6RKB*)RjQ$d8-u9{h}XT zV%A!R{)#8sB19wr{;pDWUY$g=pD8;a3_IOXbxz-f?tNOsFk&)oGN2eBY6Iq|CFtwX5%8U7t6Q` JpH0+?{{V@X>+1jj literal 0 HcmV?d00001 diff --git a/config/accounts.yml b/config/accounts.yml new file mode 100644 index 0000000..8cc8300 --- /dev/null +++ b/config/accounts.yml @@ -0,0 +1,4 @@ +accounts: + - name: main_account + email: miguelloy97@gmail.com + diff --git a/config/labels.yml b/config/labels.yml new file mode 100644 index 0000000..476f8e2 --- /dev/null +++ b/config/labels.yml @@ -0,0 +1,63 @@ +promo: + keywords: ["sale", "deal", "discount", "offer", "clearance", "gift", "free", "promo", "savings", "save", "perk", "alert", "50", "10"] + children: + stores: + keywords: ["walmart", "target", "amazon", "bestbuy", "shein", "temu"] + newsletters: + keywords: ["weekly roundup", "newsletter", "digest", "perk alert", "alert 10", "week 03", "spring", "new"] + coupons: + keywords: ["coupon", "voucher", "redeem"] + electronics: + keywords: ["raspberry pi", "digi key", "digikey", "hardware", "component", "order"] + gaming: + keywords: ["steam wishlist", "game", "bonus", "classic", "dlc", "gaming", "wishlist"] + seasonal: + keywords: ["fishing sale", "flavor", "spring", "classic fishing"] + +job: + keywords: ["hiring", "interview", "career", "position", "job", "resume", "software engineer", "engineer", "developer"] + children: + offers: + keywords: ["job offer", "contract", "start date", "accept"] + applications: + keywords: ["application", "applied", "submitted", "review"] + +bank: + keywords: ["account", "transaction", "balance", "deposit", "withdrawal", "bank"] + children: + alerts: + keywords: ["alert", "fraud", "security", "unauthorized"] + credit_offers: + keywords: ["approved", "pre-approved", "credit cards", "selected pre", "approved credit", "payment", "changed"] + +school: + keywords: ["course", "assignment", "professor", "exam", "lecture", "university"] + children: + grades: + keywords: ["grade", "result", "score", "transcript"] + schedule: + keywords: ["calendar", "timetable", "class schedule"] + +social: + keywords: ["friend", "follow", "message", "mention", "notification"] + children: + networks: + keywords: ["twitter", "facebook", "instagram", "tiktok", "discord"] + invites: + keywords: ["invite", "joined", "group", "event"] + +travel: + keywords: ["flight", "booking", "hotel", "trip", "reservation", "itinerary"] + children: + airlines: + keywords: ["delta", "united", "american airlines", "southwest"] + deals: + keywords: ["travel deal", "fare", "cheap flights"] + +work: + keywords: ["meeting", "weekly meeting", "time card", "2025"] + children: + projects: + keywords: ["project", "task", "deadline", "milestone"] + team: + keywords: ["team", "colleague", "manager", "supervisor"] diff --git a/config/settings.yml b/config/settings.yml new file mode 100644 index 0000000..e69de29 diff --git a/src/db/__pycache__/database_manager.cpython-310.pyc b/src/db/__pycache__/database_manager.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f293e8d644e8180a6df2e718245f7f80557067a GIT binary patch literal 5646 zcmc&2-*4N-nMY9+Ez5S|woW`W15;#MG<2?;E`4x~V2Z6I#%w8-ecIv+xXr8JGKjx3_>NqJ|Qp>BSq6xeJmLY z%}@>AX*x!;@K)(K&A~e(cA1Q(maV|rvV68?xfX0wie$QFBkt}G`!YOB@c2K3CXhPv zBhryf(jj}2qy8{k12WB71(M)>!~*j8Y9Z$PElq*k3ly=xX=HxR`y4mvg9b+tk_PCKry25{W8W2V)@I+Y4 zxySPhU8uVeXFfsB)UGE&@!IrI-%%=uH9U$bJX+(Sj=GtaU#XXzq&KR9>OcY zD{8ak4Jh>4!{6z)<+@&=W8i66PNU6q-`)ZDoa3{wjH4x3;}BZ1`oM3gQY_SpdaZDM zwWzP$&`af-Ui^5aTC3_!7Fg)B`sEX=5J`jTE2WxVSg)1Ml@j1tE5hgP%F0@ya!3Dp z@y^>rmtj=UKPps~ZWSt*uUx&F9}Xr$%P}~8y|nV-dQnhVT3=lqzAo_YGFPt^KdznF zJ^#V6p%5j5E>yF&9Hw(-(bhtbX)6R{KL(FHzeHMsVE_LjE6cXM&<&1BYq)fp%=h7h zHbHm&Do8{-d`+-}q8VH=95$ikbih7y>8WhNwuM56Z|R3yyKv~g1Y*mhEq%3Ix`|dX z9EsC#@A|KR30>IAw|QfWnZhkbqvOnGkVjAx0`W4qS8ZjjIDF%6&*_)J)%g+bUvuOd=(ElQ|ti>fmNK{Yq0xpOn(Qx>~YTqhi$lZ6M{SIvOZKheXx*>1)<-g zY@|*3E!Q?r3;ee2D!@rI3@=T z2%>@2-0OLE&&8J3D-}2cO?3Bat*~}`xX+7htEZkTqdaK9h({llNl+Imd8KgQ7$#hl zQuvc(rkhzO8AM-BL3Ka0(IU4)$7x**FX9TwNN$LX5);r9`Nx8u*EkkJB6s8$pa%t8 z$bFAT8TNqLVZaQi;F`@w5RDZd*laHVw~ErXr9c*e5w88Ruqh3V#hQIh9kwtwV`4^Jx52`ep7MZBpf$D4#@;s`|LOx34ToMZOc3ots3S_8q zM~2Emtw+=A@i^|X|DiiU--uV!8Ne&Wco7!3h;!$2 zI6o^PE|D2`s~)ZON63ORQ1R%=j^{fWvEvA71Y@1dBhtm8#`RF<(~PKw;4P}5*;14V z-McPCmb@IPamav(H8;%ndSl{tgya$iPljD_xwSkvM^0v!A@ zH0^V+0?&86(CDk1=Gt1dxLRDQ>F+`TtH$xjP|$S@t(z*hbJbhrjX0aGesIig@D@YZ z47~mG(EJi)`AG(>1};YT|;Hf1>{Frbo=%ehS15@Dcd)Ge49Z)FerS|+ON$xE+ zG|K86&zKM{&!lI+Km6yPf2*%G#9^t6Z5MPSvr(v&R!TS9QzvE;wGZXJij*N*!Mo59 zRQh1)G3R*_qJ(rfqGHb8hOYtT@ONPBOLzo_Gg7bCIA)@FHG-9RJUhyO^Vt&t)Y{vF z+*yQ0jzQSawpn2BSdJ5=d=~JxU<`Tzhv18`DzX<1E2EFHF6q%FoYUqk2iQ7f9PksL}w{x4c{F+KnQ literal 0 HcmV?d00001 diff --git a/src/db/database_manager.py b/src/db/database_manager.py new file mode 100644 index 0000000..50ddb99 --- /dev/null +++ b/src/db/database_manager.py @@ -0,0 +1,155 @@ +# src/db/database_manager.py + +import os +import mysql.connector +from utils.logger import Logger +import yaml + +class DatabaseManager: + def __init__(self, config=None, source="db"): + self.logger = Logger(source) + self.config = config or self._load_env_config() + self.connection = self._connect() + + def _load_env_config(self): + return { + "host": os.getenv("DB_HOST", "localhost"), + "port": int(os.getenv("DB_PORT", "3306")), + "user": os.getenv("DB_USER", "emailuser"), + "password": os.getenv("DB_PASSWORD", "miguel33020"), + "database": os.getenv("DB_NAME", "emailassistant") + } + + def _connect(self): + try: + conn = mysql.connector.connect( + host=self.config["host"], + port=self.config["port"], + user=self.config["user"], + password=self.config["password"], + database=self.config["database"] + ) + self.logger.log(f"✅ Connected to MariaDB at {self.config['host']}:{self.config['port']}") + return conn + except mysql.connector.Error as err: + self.logger.log(f"❌ DB connection failed: {err}", level="ERROR") + return None + + def initialize_schema(self): + if not self.connection: + self.logger.log("❌ No DB connection — cannot initialize schema.", level="ERROR") + return + + cursor = self.connection.cursor() + + try: + # metadata + cursor.execute(""" + CREATE TABLE IF NOT EXISTS metadata ( + id INT AUTO_INCREMENT PRIMARY KEY, + user VARCHAR(255), + email VARCHAR(255) UNIQUE NOT NULL, + token TEXT + ); + """) + self.logger.log("✅ Table ready: metadata") + + # emails + cursor.execute(""" + CREATE TABLE IF NOT EXISTS emails ( + id INT AUTO_INCREMENT PRIMARY KEY, + user VARCHAR(255), + account VARCHAR(255), + message_id VARCHAR(255) UNIQUE, + thread_id VARCHAR(255), + account_id VARCHAR(255), + sender VARCHAR(255), + cc TEXT, + subject TEXT, + body LONGTEXT, + links LONGTEXT, + unsubscribe_data TEXT, + received_at DATETIME, + folder VARCHAR(50), + attachments LONGTEXT, + is_read BOOLEAN DEFAULT FALSE, + labels LONGTEXT, + + ai_category VARCHAR(100), + ai_confidence FLOAT, + ai_summary TEXT, + ai_keywords TEXT, + ai_label_source VARCHAR(100), + summary_source VARCHAR(100), + ai_model_version VARCHAR(100), + is_ai_reviewed BOOLEAN DEFAULT FALSE, + processing_notes TEXT, + + processing_status VARCHAR(50), + sync_status VARCHAR(50), + attachment_path TEXT, + downloaded BOOLEAN DEFAULT FALSE + ); + """) + self.logger.log("✅ Table ready: emails") + + # logs + cursor.execute(""" + CREATE TABLE IF NOT EXISTS logs ( + id INT AUTO_INCREMENT PRIMARY KEY, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + level VARCHAR(20), + source VARCHAR(255), + message TEXT + ); + """) + self.logger.log("✅ Table ready: logs") + + self.connection.commit() + self.logger.log("✅ Database schema initialized successfully!") + + except Exception as e: + self.logger.log(f"❌ Failed to initialize schema: {e}", level="ERROR") + + finally: + cursor.close() + + def check_health(self): + status = {"status": "unknown", "details": []} + + if not self.connection: + self.logger.log("❌ Health check failed: No DB connection.", level="ERROR") + status["status"] = "unhealthy" + status["details"].append("No database connection.") + return status + + try: + # Ping the DB + cursor = self.connection.cursor() + cursor.execute("SELECT 1") + _ = cursor.fetchall() + + # Check core tables + required_tables = ["emails", "logs", "metadata"] + cursor.execute("SHOW TABLES;") + existing_tables = set(row[0] for row in cursor.fetchall()) + + missing_tables = [table for table in required_tables if table not in existing_tables] + if missing_tables: + status["status"] = "degraded" + for table in missing_tables: + self.logger.log(f"⚠️ Missing table: {table}", level="WARNING") + status["details"].append(f"Missing table: {table}") + else: + status["status"] = "healthy" + status["details"] = [f"{table} ✅" for table in required_tables] + + self.logger.log(f"✅ Health check passed: {status['status']}") + return status + + except Exception as e: + self.logger.log(f"❌ Health check failed: {e}", level="ERROR") + status["status"] = "unhealthy" + status["details"].append(str(e)) + return status + diff --git a/src/gmail/__pycache__/gmail_client.cpython-310.pyc b/src/gmail/__pycache__/gmail_client.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fdd3f580382ab4689be586509b6795d54a45c575 GIT binary patch literal 863 zcmZuv&2H2%5Vn)-CS9_XTJ-=oAIW7O03k$as}`wRkXCSrf~;VBQ%m^)J6Va<-spqS zV_zv(#1n8s9GLNTQC2Y0jK|{{e>0!mOs5k9^85Xd;)6%Xciarchs{e+dkp51NFwPi z=vOZ#(p!+k7ZBeQxMf@M6dXlTDoN@|n)=d{{w+xZ8ORZ=BRQ5KtYaC$JqP`x%PQC9 zS*casu3t?U$W>m;QdyRF?F~e=$Xngi45Cb7GFz%v+~~SsE1fe}&raXIp1ERyjqC({ z)H6_f0%k)ugp&>VM(@CKPm%?>r}M<`;{vbdqN$g<=%;)DuF}vdv(iE(ULROhE)DW? zPs|vWUkmXpdj|`krCjb5qjyb)am1K8}UsT8};KV}yxyKp0%7I2VRBQl4( zwOlwFW}m+F5$C$rE$0!o9)KCsL%Kd0ur=GI2nxqLK_AtPjCDAoK@wcI2{ogg?*b;o zEhfgGKQS@z*H89vhFtu^?;cpO59T?r^e`8q>FV}@-7PM1=fL~ey(f2XYmYE|0A~Go z*uyYoo`lFxomYzUe!_Xx$gV^`=KOP)mk%c)=dux;8|2W9Xziy09K dXcR<2JaX|lkcQ{~E9|4^Tl5#0VUfOL?+@iOz}Nr) literal 0 HcmV?d00001 diff --git a/src/gmail/gmail_client.py b/src/gmail/gmail_client.py new file mode 100644 index 0000000..bb433d7 --- /dev/null +++ b/src/gmail/gmail_client.py @@ -0,0 +1,20 @@ +# src/gmail/gmail_client.py + +class GmailClient: + """ + Handles authentication and email fetching via Gmail API. + """ + + def __init__(self, gmail_config): + self.gmail_config = gmail_config + self.service = self._authenticate() + + def _authenticate(self): + # TODO: Implement OAuth2 flow using token + credentials + # Return authenticated Gmail API service + pass + + def fetch_emails(self, account_config): + # TODO: Fetch emails for a specific account + # Return a list of raw emails (with subject, body, etc.) + return [] diff --git a/src/gmail/gmail_parser.py b/src/gmail/gmail_parser.py new file mode 100644 index 0000000..263f6fa --- /dev/null +++ b/src/gmail/gmail_parser.py @@ -0,0 +1,22 @@ +# src/gmail/gmail_parser.py + +class GmailParser: + """ + Parses raw Gmail messages into structured format. + """ + + def __init__(self): + pass + + def parse_message(self, raw_message): + # TODO: Extract subject, sender, body, date, attachments, etc. + parsed = { + "subject": "", + "from": "", + "to": "", + "body": "", + "attachments": [], + "date": "", + "message_id": "" + } + return parsed diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..5227d49 --- /dev/null +++ b/src/main.py @@ -0,0 +1,23 @@ +# src/main.py +import yaml +from orchestrator.assistant import EmailAssistant +import sys, os +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "."))) +from db.database_manager import DatabaseManager + +def main(): + print("📦 EZ Email Assistant: Backend Bootstrap\n") + + # 1. Initialize DB Schema + db = DatabaseManager() + db.initialize_schema() + + # 2. Run Health Check + print("\n🔍 Checking DB Health...") + health = db.check_health() + print("Health Status:", health["status"]) + for detail in health["details"]: + print(" -", detail) + +if __name__ == "__main__": + main() diff --git a/src/models/llm_engine.py b/src/models/llm_engine.py new file mode 100644 index 0000000..f1c9eeb --- /dev/null +++ b/src/models/llm_engine.py @@ -0,0 +1,19 @@ +# src/models/llm_engine.py + +class LLMEngine: + """ + Handles summarization or classification via LLMs (local or API). + """ + + def __init__(self, mode="api", config=None): + self.mode = mode + self.config = config or {} + # TODO: Initialize model or API client + + def summarize(self, text): + # TODO: Send to local LLM or API + return "LLM summary" + + def classify(self, text): + # (optional) Use LLM for category prediction + return ["promo", "job"] diff --git a/src/orchestrator/__pycache__/assistant.cpython-310.pyc b/src/orchestrator/__pycache__/assistant.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..61783f876693294c7b326eee5519a0cc25756d78 GIT binary patch literal 1861 zcmZuy&2QX96d#X2*XvEwq@oStU?DDBiTwj2RJBUlawsWM_>e}6EU#y>b@4|sGX@el zreQrIOgan@NKKpa|lTXMW zC|rJQ5N^XyF(909t01MtEDL#B(9&kM;qAgHUFI6zDZJ8WzTw>>C`W8$c&`Y{F&kUt zKH)wOo)R8Bp)BH~LoyBjg8RwT>c;oWG%xmwTvQrFf3FZ}B_y!nVN;f=%+HMEKS&p% zFzl86RHutniI37MT^g15pk4x(;iZ=!3c_vpsThbL%;JPmZZVrvZa*c=;SP6Ub-4$i z{BGkeqPe3~u5?=Iv+u(go`*7fB$Sq^7An?{L=0GSDdMA|{(9F~ydU2a8jWIs7pS$; zIHTgU;;|Z>QE^sR8g3YD;MQf0ZVsmobuHnhyo&kSQDd5+GsA;C=4uaq>K>5RYDq%0 z^@PAmwcT3W+96L+>u47wx|Tfc8=F9n%o^&kStCv$1olBk`J!`|=*D!?c>wk(UrMMI z(s_l6vh+ZZBP0kzVns;=)A?DSJ?WqnUid7fYqpDZ4r%8oQ5?xJXkQX}6_!_@XS?%H zm5^#)=F3LRA68;t=3j~Vew{TX#7oUTtV5#zL z@o5(%NnYhTNw(oLY6FO81=a?<0lblQcKywy&j$O<0^8w1cM+}$_;0|p`Sr)A@u5zo&Z}i#&hbS&W7$-**^C8gmemd9qv%A4{h4c^mDiEHjr*Z-(>N$^pmqldc1KUPfa`2X!8^dHn?NFKgT4hd9MDaA4c09y zu&G7=r4*=n!&f&(^zY5>vn>;ri_tcNYYwI!c1M+#B1yV1Ny?fx1@cjnoHS{%-tm)! z*IANC>>!CBzXZy_Lu4`lg0ny&6A;G=f@oy42h$1LG9zVXXp)(EZ^j3^dfex*ma}1e z;4op=Uh7{6+Dw+qN7f^yA28c|F&k!PQuhB2W?HeR(J+>0>j8$-%mDj8Kf2(9J`T6A P|INUNXavFS-!1wdKe^{o literal 0 HcmV?d00001 diff --git a/src/orchestrator/assistant.py b/src/orchestrator/assistant.py new file mode 100644 index 0000000..204e38b --- /dev/null +++ b/src/orchestrator/assistant.py @@ -0,0 +1,54 @@ +# src/orchestrator/assistant.py + +from gmail.gmail_client import GmailClient +from processor.cleaner import Cleaner +from processor.summarizer import Summarizer +from processor.labeler import Labeler +from db.database_manager import DatabaseManager +from utils.logger import Logger + +class EmailAssistant: + """ + Orchestrates the entire flow: + - Fetches emails + - Cleans and summarizes content + - Categorizes + - Stores in database + """ + + def __init__(self, config): + self.config = config + self.logger = Logger() + self.db = DatabaseManager(config["db"]) + self.gmail = GmailClient(config["gmail"]) + self.cleaner = Cleaner() + self.summarizer = Summarizer() + self.labeler = Labeler() + + def run(self): + self.logger.log("🔄 Starting email assistant run...") + + for account in self.config["accounts"]: + self.logger.log(f"📩 Fetching emails for account: {account['email']}") + emails = self.gmail.fetch_emails(account) + + for email in emails: + # TODO: Add ID check to avoid duplicate inserts + cleaned_body = self.cleaner.clean_body(email["body"]) + links, unsub_data = self.cleaner.extract_links(email["body"]) + summary = self.summarizer.summarize(cleaned_body) + labels = self.labeler.label(email["subject"], summary) + + # TODO: Insert or update email in DB + self.db.insert_email({ + **email, + "cleaned_body": cleaned_body, + "summary": summary, + "labels": labels, + "links": links, + "unsubscribe_data": unsub_data + }) + + self.logger.log(f"✅ Processed: {email['subject']}") + + self.logger.log("✅ Email assistant run completed.") diff --git a/src/processor/__pycache__/cleaner.cpython-310.pyc b/src/processor/__pycache__/cleaner.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16d9e75effaca4a854fd86e51c55b09765db5fb8 GIT binary patch literal 886 zcmZuv&2AGh5VpOWWE&cRB5_4Nl1qB85CXLo9I7A{RfyF}mSa2Vs{0e{-O}!rig)QN z?G^D7oS5+@bOVeujc+}u`Hs><=&nN^)}Ei1{h^xA+auARSA7p`79Wz(hMmlx-5 z?sF>ILyA7?38)LfXaYFs;sHz>I+Lv|w&KmN3w4}aI%8dur|a4z9}077(;JhVma5K8 zQMu%OnHzpCm&sYo&phI{YX~IHs$^qJWt=N*qPiyMo4Ym=A}!KNhzJp2t&ld)wk*td zL4|>xpudlq0%o>ord?hj=MPsN_ye;z!T8{+$}@o!J~m~s>2~a2`p^^DcQ{xx-Qjo) z|APe4c=qrMX}YGGt^A(=JXckTigX?Zkzj z@6#R1F>9XhQ10DW%tlCVA#)={I~F1@b)BJqD8!dqX4}X}2wf^6>;Q|7(BLR~<*`Fg mlSS5%Nr$~gFsUevg2T|CFJ{!Q|9jX*ue$25F@tA!%68z<~Vdv`pjK9=Z z*`L^xRdGgF@Ih57RjJgg7gno(@`{HH?#`J-50R?bA>QCi03Lq%6Z?!+o!t*!Kpa*fSdFeZ}bNov%Mt4f}9gH zKimEwHx&r|=g8EPdw6f3>5m8zweQ*y^xFZhV)qU7Rj{LnVUPNC@SuphzOL0ZQ&g8N z6*NYxZ(^{37@|&VK-}EiPL(m;(Cv+>m;ui!0J$|TO>?}w(A-59U2eDObcUt zaHK26OJlx{O@FM+jd8&m6HEFkGQ&rclAJs)vhGX_`9&p)tSXj8%5K+LI{();RnLEq LeZl>S=$ZTl%^91J literal 0 HcmV?d00001 diff --git a/src/processor/__pycache__/summarizer.cpython-310.pyc b/src/processor/__pycache__/summarizer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91556e92e05f8e819e2dd2a2bf4a6d61f8832972 GIT binary patch literal 732 zcmZWm-D=c86rRb>?qEAbN=T0zL=w@8qKZVGqR~Y8YNFVIModlB>_DQan!|=g8(dX+p4ha}wz&>C z*pP5sW;!Xf;yO>#jFH78h!}TdQmTxxi`BQ0%ZMv&$a+~RJ z)var%&@5}|jez1A{S4fp26yz12p|ZM=tW4|S+2`ZMulF>oX)ludHo=E3ZMHM`7FM1 z+PXN0r*wQ-=yRKX(eb&FRj!ND#cxfn`6juI*AZWL*kA3S5IZa5y){xhXKd^S=Bs_( z`a+~dS_~=J6ngUHz^9#{_;|&xFB2QQ+WRG>`i2H8n+a literal 0 HcmV?d00001 diff --git a/src/processor/cleaner.py b/src/processor/cleaner.py new file mode 100644 index 0000000..0ec82bf --- /dev/null +++ b/src/processor/cleaner.py @@ -0,0 +1,17 @@ +# src/processor/cleaner.py + +class Cleaner: + """ + Cleans raw email body and extracts useful data like links and unsubscribe URLs. + """ + + def __init__(self): + pass + + def clean_body(self, html_body): + # TODO: Strip HTML, remove tracking pixels, normalize text + return "cleaned email body" + + def extract_links(self, html_body): + # TODO: Find all URLs and unsubscribe links in the body + return ["http://example.com"], "http://unsubscribe.example.com" diff --git a/src/processor/labeler.py b/src/processor/labeler.py new file mode 100644 index 0000000..c75d09a --- /dev/null +++ b/src/processor/labeler.py @@ -0,0 +1,14 @@ +# src/processor/labeler.py + +class Labeler: + """ + Assigns labels to emails using subject, sender, or summary-based rules. + """ + + def __init__(self): + # TODO: Load rules or ML model + pass + + def label(self, subject, summary): + # TODO: Return a list of categories or labels + return ["promo", "gaming"] diff --git a/src/processor/summarizer.py b/src/processor/summarizer.py new file mode 100644 index 0000000..ab0840a --- /dev/null +++ b/src/processor/summarizer.py @@ -0,0 +1,14 @@ +# src/processor/summarizer.py + +class Summarizer: + """ + Summarizes cleaned email text using spaCy, KeyBERT, or LLM (configurable). + """ + + def __init__(self, method="spacy"): + self.method = method + # TODO: Load model(s) depending on config + + def summarize(self, text): + # TODO: Return a short summary or key phrases + return "summary of email" diff --git a/src/utils/__pycache__/logger.cpython-310.pyc b/src/utils/__pycache__/logger.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d655e467b8ee903554c870056b4cbd3b2a83bb49 GIT binary patch literal 1765 zcmZux-ESL35Z~SVw4Jz+z93Om$A?i)O{0iPJVXeQHZF;j*j1bo)p2xkZa2o!ozLCf z%ZKGuNX;V%iASnRHgEg~yz+0X;0443Pdo#HGP`y{RJzv8&Cbk@XJ>x1+3ai)!TRZk zUpqfK2>qfrSC<8wkAcJ@Fp4NnP>PxeYnddtNzB)3TAv_lQTrTG`zdMKlr&K2{03)G zh?P&vR}b zt~eEvr7^h@RLh#~0f{$&4e<~~kUMao5gD8OAr7f+hZe-havn3~9WlP238qR>ob`Gv zmJ!_#iLwPt4meZ?eTR4fK7Ve;%dLlkanVYin_h7j3QrW>;iMKi)ZlQ+T@mt7xox~ z?PBRG>gJhiehtFlmPCj6kVtD}Q%uR2NXN)f#xZ${jqFet^x6XoI+Y5vI3~38r2_?W zhv?89d1C~!w*zf%AR9^lbPc03jM3nAsx7W8(3v5AhMq%QjCRl?@I1p$pnZ+0bc~Nt zI9pTR>fLCq-q==!B#Yb0XPJ;j_n^MDt?WB@mOe1shmGo%DllEC94f0yaO}ZKqp?%p zT2&=e<^-JIUozgcmCdRu8}GIdT_M{&89K@~(d~Z5rLs*JzYq4~j8g-`_I_Ki{1((| zcT(BC;OFOG1oyH$p+PT`!L;vz%mQeS2v+Yd2Vqe;>8W^}sKV5oapg@#MKYn>4wI~R zq^xca{#RR+8|PerU!i&uv%c(RJ>~Q{*h9i~mlb9PN)x8r$UR`QK;i~40stuDGLZiH zf6aMi(A>cO=j5Nk+yyMANdQchyz9Dir$4@ZE$_J^5}LYy`DXO{_g@9~0kDit0i!<#GBaFHr?Pt4vGRrF2m19hPl$5zexGr!yTjkr z>@CgS*6toVVR0@QpeIYyuH^54HgqR|@unA0j=7jB=#T*Xw66*W8Bg0%GRk#|F9Azg z0OdD9FKz-W;{dys4@9t!iH+BY?M4E_1C7ok`uUnR#2h j95=4h=Q@4kb|s*RucIpe!%0RT={M4Zo*_P5agO{8u2Qs* literal 0 HcmV?d00001 diff --git a/src/utils/logger.py b/src/utils/logger.py new file mode 100644 index 0000000..aa5c868 --- /dev/null +++ b/src/utils/logger.py @@ -0,0 +1,44 @@ +# src/utils/logger.py + +from datetime import datetime +import mysql.connector +import os + +class Logger: + def __init__(self, source="system"): + self.source = source + self.verbose = True + self._connect_db() + + def _connect_db(self): + try: + self.conn = mysql.connector.connect( + host=os.getenv("DB_HOST", "localhost"), + port=int(os.getenv("DB_PORT", "3306")), + user=os.getenv("DB_USER", "emailuser"), + password=os.getenv("DB_PASSWORD", "miguel33020"), + database=os.getenv("DB_NAME", "emailassistant") + ) + self.cursor = self.conn.cursor() + except Exception as e: + self.conn = None + print(f"[Logger] ❌ Could not connect to logs DB: {e}") + + def log(self, message, level="INFO"): + timestamp = datetime.now().strftime("[%Y-%m-%d %H:%M:%S]") + formatted = f"{timestamp} [{level}] {self.source.upper()}: {message}" + + # Console log + if self.verbose: + print(formatted) + + # DB log + if self.conn: + try: + self.cursor.execute(""" + INSERT INTO logs (level, source, message) + VALUES (%s, %s, %s) + """, (level, self.source, message)) + self.conn.commit() + except Exception as e: + print(f"[Logger] ⚠️ Failed to log to DB: {e}") diff --git a/src/utils/scheduler.py b/src/utils/scheduler.py new file mode 100644 index 0000000..438cf95 --- /dev/null +++ b/src/utils/scheduler.py @@ -0,0 +1,14 @@ +# src/utils/scheduler.py + +class Scheduler: + """ + Placeholder for scheduled tasks or cron-like runs. + Will manage background sync in v2. + """ + + def __init__(self): + pass + + def run_scheduled_tasks(self): + # TODO: Run assistant on schedule (via cron, Celery, etc.) + pass diff --git a/ui/streamlit_app/Home.py b/ui/streamlit_app/Home.py new file mode 100644 index 0000000..fef5034 --- /dev/null +++ b/ui/streamlit_app/Home.py @@ -0,0 +1,19 @@ +# ui/streamlit_app/Home.py +import streamlit as st + +from utils.logger import Logger + +logger = Logger(source="test_ui") +logger.log("This is a test log from the UI!", level="INFO") + + +def run(): + st.set_page_config(page_title="Email Assistant", layout="wide") + st.title("📬 Email Assistant Dashboard") + st.markdown("Welcome to your AI-powered inbox control center!") + + # TODO: Show summary stats, recent labels, account statuses + st.info("Stats coming soon! Build in progress.") + +if __name__ == "__main__": + run() diff --git a/ui/streamlit_app/__init__.py b/ui/streamlit_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/streamlit_app/__pycache__/utils.cpython-310.pyc b/ui/streamlit_app/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd367b1252c12a0b38e0c1fa25ff78d0a8a7ad99 GIT binary patch literal 1042 zcmZuwO>5jR5S3)bYj1XwLTLj%g%S$BZ0N0oP+FQm4{dq~CDa&<*3vo-{z8%ywj`&` zy?>yG>@j~y*FvFxAr#V)HXG7XX=iryVb6Q>#?5fpM=)-m{>-llLO<-{;Rs>z227oR zimlHzUa*{Cv!5#bByh6uMDa%4qc4Pe%$A)e1-d$I9`RQ z#{-B4JmmdL6c0H*MA6_E z>_z0jU>SLaHm#@)owBNoL&YW{EgQxoVra|svB8sTByGiR4zr!)vl3DzRgrfh`BaNN zS)7VwugSVf)LJF)n@VhcVB=&z-rTQPQEtA?;UMXXL}@7?MWIvHwn?XpQjOZF8QNs& zlCEW@d~gFC;H_J^3Snj`FNX)gJ^fu+0DbJ}uU`;HADx3p7#%$*fTas1*!XV=&LU5) zz>B0!YbfukHuB=9MhLEksY6qBcEV$eLBc1Zg@cN|C1VhLYKck4H