From 6532608f7d43557642e29b74958477620aa4f62c Mon Sep 17 00:00:00 2001 From: AlAnoud2003 Date: Tue, 14 Apr 2026 03:17:17 +0300 Subject: [PATCH] Created a new branch for precision metric and personalization task edits --- background.js | 57 +++++++++++++++++++++++- display_logs.py | 27 +++++++++--- encoders.joblib | Bin 1648 -> 0 bytes encryption.py | 4 +- logger.py | 94 +++++++++++++++++++++++++++++++--------- model_metadata.json | 63 ++++++++++++++++----------- model_trainer.py | 80 +++++++++++++++++++++++++++++----- preference_model.joblib | Bin 31781 -> 75331 bytes preprocessed_data.csv | 16 ++++--- preprocessor.py | 59 ++++++++++++++----------- user_interactions.json | 2 +- 11 files changed, 304 insertions(+), 98 deletions(-) delete mode 100644 encoders.joblib diff --git a/background.js b/background.js index 5267f27..e0a4b47 100644 --- a/background.js +++ b/background.js @@ -138,13 +138,52 @@ function getRecommendationFromConfig(concern, config, userQA) { return { focusLabel: selected.focus_label || concern, - recommendation: selected.recommendation || "", + recommendation: selected.recommendations || [], latestQuestion: userQA[userQA.length - 1] || "" }; } + + +async function logInteraction(question, detected_concern) { + const entry = { + question, + detected_concern, + timestamp: new Date().toISOString() + }; + + const data = await chrome.storage.local.get(["interactions"]); + const interactions = data.interactions || []; + + interactions.push(entry); + + await chrome.storage.local.set({ interactions }); +} + + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === "logQA") { + (async () => { + const entry = { + question: request.userQA?.[0], + detected_concern: "general_privacy", + timestamp: new Date().toISOString() + }; + + const data = await chrome.storage.local.get(["interactions"]); + const interactions = data.interactions || []; + + interactions.push(entry); + + await chrome.storage.local.set({ interactions }); + + console.log("Logged interaction:", entry); + })(); + + return true; + } + if (request.action === "detectGDPR") { detectGDPRFromIP(); sendResponse({ status: "started" }); @@ -183,6 +222,22 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { personalization = getRecommendationFromConfig(inferredConcern, config, userQA); } + +//ADDED + await chrome.storage.local.get(["eval_logs"]).then(async (data) => { + const logs = data.eval_logs || []; + + logs.push({ + question: request.userQA, + concern: inferredConcern, + recommendation: personalization?.recommendation, + focusLabel: personalization?.focusLabel, + timestamp: new Date().toISOString() + }); + + await chrome.storage.local.set({ eval_logs: logs }); + }); + let prompt = ` You are a legal AI assistant. diff --git a/display_logs.py b/display_logs.py index 00bc2b9..0a3be5f 100644 --- a/display_logs.py +++ b/display_logs.py @@ -1,16 +1,31 @@ import json import os +from encryption import decrypt_file, encrypt_file LOG_FILE = "user_interactions.json" def display_interactions(): - """ - Display all logged user interactions. - """ if os.path.exists(LOG_FILE): - with open(LOG_FILE, "r") as file: - data = json.load(file) + try: + # Step 1: Decrypt file + decrypt_file(LOG_FILE) + + # Step 2: Read JSON + with open(LOG_FILE, "r") as file: + data = json.load(file) + + # Step 3: Print interactions for interaction in data["interactions"]: print(interaction) + + # Step 4: Re-encrypt file + encrypt_file(LOG_FILE) + + except Exception as e: + print(f"Error reading interactions: {e}") else: - print("No interactions logged yet. Run initialize_schema() first.") \ No newline at end of file + print("No interactions logged yet. Run initialize_schema() first.") + + +if __name__ == "__main__": + display_interactions() \ No newline at end of file diff --git a/encoders.joblib b/encoders.joblib deleted file mode 100644 index 35261d606e1921e278d6f0b02af6d101db0a661b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1648 zcmds%zi-qq6vr=Zk87(^u4-Z6H(Ob(cI$#FRJss!NH8!V%XX}@!Lcpdxe^JaGF0Ux zPw?*V1t+-<5fd9a56-jop5OQLUhK<_#j|YCj&E=nx2#c;)mRn~ANtRV6Q_}N+&aU! z_fk(0R)lf|apz6*J=e?>+`%t+{vDbVHkl&cbB`hJe=<}_igZ=l8e7TIc@Xy|&GMyl zr2gQDwY^|p(Ka69u#JK^(gkUJQ-(P7f>;ju+EucRYx2hIEZIe<)4tffv6AoF+Y^}o zxQ2Idn6Jm|D@-Pn*T2(fr~EVkjoU;ibAii}SeVBLoz6BJCQ43q$#uZA4%|aNuIS}( z@v|${Al_@85oX45R8}g;PG#J|jx(cua1{%!=)Xbo!4;fk6gqWT!z0%JP5b;~s&#gh z_>X}n7PNSOBW;YAjl(nUyfiu$=op=#BdBP+MObLI#75UIF%sKL>$Vt+y;jSN~=Sy($L_AG|cRUXv{iv7;qNluACDd1a)ZKJ5XVO$2&xoqh1z4S?p7u$% aG{0Cq$QOOcQF=E5{}NWIkDEQj3jG0)J93!- diff --git a/encryption.py b/encryption.py index 78a261c..06c795b 100644 --- a/encryption.py +++ b/encryption.py @@ -26,7 +26,7 @@ def encrypt_file(file_path): file.write(encrypted_data) print(f"File '{file_path}' encrypted successfully.") -# Decrypt a file +# Decrypt a file def decrypt_file(file_path): cipher = load_key() if os.path.exists(file_path): @@ -42,4 +42,4 @@ def decrypt_file(file_path): # If decryption fails, assume the file is already in plaintext print(f"File '{file_path}' is not encrypted or decryption failed: {e}") else: - print(f"File '{file_path}' does not exist. Cannot decrypt.") \ No newline at end of file + print(f"File '{file_path}' does not exist. Cannot decrypt.") diff --git a/logger.py b/logger.py index a354e0b..41c4337 100644 --- a/logger.py +++ b/logger.py @@ -1,30 +1,84 @@ import json -import os # Import the os module +import os from datetime import datetime +from encryption import decrypt_file, encrypt_file LOG_FILE = "user_interactions.json" -def log_interaction(policy_id, action, setting_changed=None, previous_value=None, new_value=None, context=None): +def log_interaction(question, detected_concern): """ - Log a user interaction with the privacy policy. + Log a user Q&A interaction (Option A). """ - log_entry = { - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "policy_id": policy_id, - "action": action, - "setting_changed": setting_changed, - "previous_value": previous_value, - "new_value": new_value, - "context": context - } - - # Append the log entry to the JSON file - if os.path.exists(LOG_FILE): - with open(LOG_FILE, "r+") as file: + + if not os.path.exists(LOG_FILE): + print("Log file not found. Initialize it first.") + return + + try: + # Step 1: Decrypt file (this modifies file in place) + decrypt_file(LOG_FILE) + + # Step 2: Load JSON data + with open(LOG_FILE, "r") as file: data = json.load(file) - data["interactions"].append(log_entry) - file.seek(0) + + # Step 3: Create new entry + new_entry = { + "question": question, + "detected_concern": detected_concern, + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + + # Step 4: Append + data["interactions"].append(new_entry) + + # Step 5: Save updated JSON + with open(LOG_FILE, "w") as file: json.dump(data, file, indent=4) + + # Step 6: Re-encrypt + encrypt_file(LOG_FILE) + print("Interaction logged successfully.") - else: - print("Schema not initialized. Run initialize_schema() first.") \ No newline at end of file + + except Exception as e: + print(f"Logging error: {e}") + + +# Optional: test it manually +if __name__ == "__main__": + log_interaction( + "How can I protect my account?", + "account_security" + ) + + # Decrypt, load, append, and re-encrypt the file + # try: + # if os.path.exists(LOG_FILE): + # decrypted_data = decrypt_file(LOG_FILE) + # data = json.loads(decrypted_data) + # else: + # print("Schema not initialized. Run initialize_schema() first.") + # return + + # data["interactions"].append(log_entry) + + # # Re-encrypt and save + # encrypted_data = encrypt_file(json.dumps(data, indent=4)) + # with open(LOG_FILE, "wb") as file: # Write bytes for encrypted data + # file.write(encrypted_data) + + # print("Interaction logged and encrypted successfully.") + # except Exception as e: + # print(f"Logging error: {e}") + + # Append the log entry to the JSON file + #if os.path.exists(LOG_FILE): + #with open(LOG_FILE, "r+") as file: + #data = json.load(file) + #data["interactions"].append(log_entry) + #file.seek(0) + #json.dump(data, file, indent=4) + #print("Interaction logged successfully.") + #else: + #print("Schema not initialized. Run initialize_schema() first.") \ No newline at end of file diff --git a/model_metadata.json b/model_metadata.json index d651c46..daa00a3 100644 --- a/model_metadata.json +++ b/model_metadata.json @@ -4,43 +4,54 @@ "n_estimators": 50 }, "scores": { - "accuracy": 1.0, - "precision_macro": 1.0, - "recall_macro": 1.0, - "f1_macro": 1.0 + "accuracy": 0.1, + "precision_macro": 0.08333333333333333, + "recall_macro": 0.08333333333333333, + "f1_macro": 0.08333333333333333, + "precision_at_1": 1.0, + "precision_at_3": 0.3333333333333333, + "precision_at_5": 0.25 }, "classification_report": { "0": { - "precision": 1.0, - "recall": 1.0, - "f1-score": 1.0, - "support": 2.0 + "precision": 0.3333333333333333, + "recall": 0.3333333333333333, + "f1-score": 0.3333333333333333, + "support": 3.0 }, "1": { - "precision": 1.0, - "recall": 1.0, - "f1-score": 1.0, - "support": 2.0 + "precision": 0.0, + "recall": 0.0, + "f1-score": 0.0, + "support": 3.0 }, - "accuracy": 1.0, + "2": { + "precision": 0.0, + "recall": 0.0, + "f1-score": 0.0, + "support": 3.0 + }, + "3": { + "precision": 0.0, + "recall": 0.0, + "f1-score": 0.0, + "support": 1.0 + }, + "accuracy": 0.1, "macro avg": { - "precision": 1.0, - "recall": 1.0, - "f1-score": 1.0, - "support": 4.0 + "precision": 0.08333333333333333, + "recall": 0.08333333333333333, + "f1-score": 0.08333333333333333, + "support": 10.0 }, "weighted avg": { - "precision": 1.0, - "recall": 1.0, - "f1-score": 1.0, - "support": 4.0 + "precision": 0.1, + "recall": 0.1, + "f1-score": 0.1, + "support": 10.0 } }, "features": [ - "policy_id", - "action", - "previous_value", - "new_value", - "context" + "question" ] } \ No newline at end of file diff --git a/model_trainer.py b/model_trainer.py index b464ec1..04cdd6e 100644 --- a/model_trainer.py +++ b/model_trainer.py @@ -2,6 +2,7 @@ import json import argparse import joblib +import numpy as np import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import StratifiedKFold, GridSearchCV, cross_val_predict @@ -15,28 +16,82 @@ def load_data(path=PREPROCESSED): df = pd.read_csv(path) + if "timestamp" in df.columns: df = df.drop(columns=["timestamp"]) + df = df.dropna() + + # Encode detected_concern (this is now your label) if os.path.exists(ENCODERS_FILE): encoders = joblib.load(ENCODERS_FILE) - for col, enc in encoders.items(): - if col in df.columns: - df[col] = enc.transform(df[col].astype(str)) - X = df.drop(columns=["setting_changed"]) - y = df["setting_changed"].astype(int) + if "detected_concern" in encoders: + df["detected_concern"] = encoders["detected_concern"].transform( + df["detected_concern"].astype(str) + ) + else: + le = LabelEncoder() + df["detected_concern"] = le.fit_transform(df["detected_concern"].astype(str)) + + X = df.drop(columns=["detected_concern"]) + y = df["detected_concern"] + return X, y + +def precision_at_k(y_true, y_proba, classes, k): + """ + Standard Precision@K for multi-class classification. + + For each sample: + - take top-K predicted classes + - check if true label is in those top-K + """ + + if k <= 0: + raise ValueError("k must be a positive integer") + + k = min(k, len(classes)) + + hits = 0 + + for actual, probs in zip(y_true, y_proba): + topk_indices = np.argsort(probs)[::-1][:k] + topk_labels = classes[topk_indices] + + if actual in topk_labels: + hits += 1 + + return hits / len(y_true) + + + +def evaluate_precision_at_k(model, X, y, ks=(1, 3, 5)): + if not hasattr(model, "predict_proba"): + return {} + proba = model.predict_proba(X) + classes = np.array(model.classes_) + return { + f"precision_at_{k}": precision_at_k(y.to_numpy(), proba, classes, k) + for k in ks + } + + + + + def train_and_save(random_state=42): X, y = load_data() feature_names = list(X.columns) rf = RandomForestClassifier(random_state=random_state) param_grid = {"n_estimators": [50, 100], "max_depth": [None, 5, 10]} cv = StratifiedKFold( - n_splits=min(5, max(2, len(y)//2)), + n_splits = max(2, min(3, len(y))), shuffle=True, random_state=random_state ) + + gs = GridSearchCV(rf, param_grid, cv=cv, scoring="f1_macro", n_jobs=-1) gs.fit(X, y) best = gs.best_estimator_ @@ -44,14 +99,15 @@ def train_and_save(random_state=42): report = classification_report(y, preds, output_dict=True, zero_division=0) joblib.dump({"model": best, "features": feature_names}, MODEL_FILE) - + precision_scores = evaluate_precision_at_k(best, X, y, ks=(1, 3, 5)) metadata = { "best_params": gs.best_params_, "scores": { "accuracy": accuracy_score(y, preds), "precision_macro": precision_score(y, preds, average="macro", zero_division=0), "recall_macro": recall_score(y, preds, average="macro", zero_division=0), - "f1_macro": f1_score(y, preds, average="macro", zero_division=0) + "f1_macro": f1_score(y, preds, average="macro", zero_division=0), + **precision_scores }, "classification_report": report, "features": feature_names @@ -76,9 +132,9 @@ def recommend(sample_row): if os.path.exists(ENCODERS_FILE): encs = joblib.load(ENCODERS_FILE) - if "setting_changed" in encs: + if "detected_concern" in encs: try: - human_label = encs["setting_changed"].inverse_transform([int(pred)])[0] + human_label = encs["detected_concern"].inverse_transform([int(pred)])[0] except Exception: human_label = str(pred) @@ -173,8 +229,8 @@ def export_runtime_personalization(output_path=RUNTIME_FILE): labels = [] if os.path.exists(ENCODERS_FILE): encs = joblib.load(ENCODERS_FILE) - if "setting_changed" in encs: - labels = [str(x) for x in getattr(encs["setting_changed"], "classes_", [])] + if "detected_concern" in encs: + labels = [str(x) for x in getattr(encs["detected_concern"], "classes_", [])] runtime_payload = { "model_source": MODEL_FILE if os.path.exists(MODEL_FILE) else "", diff --git a/preference_model.joblib b/preference_model.joblib index 712cb7d84ac718958dc9646068e84678d8bbd2d7..827cd4aa1dbe920dad361e64c4aa4b4441ac5ef5 100644 GIT binary patch literal 75331 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