Merge pull request #46 from wagesj45/codex/implement-ai-metadata-storage-and-ui
Add reasoning cache and viewer
This commit is contained in:
commit
0e6478269e
5 changed files with 196 additions and 7 deletions
|
@ -231,7 +231,8 @@ async function clearCacheForMessages(idsInput) {
|
||||||
if (browser.messageDisplayScripts?.registerScripts) {
|
if (browser.messageDisplayScripts?.registerScripts) {
|
||||||
try {
|
try {
|
||||||
await browser.messageDisplayScripts.registerScripts([
|
await browser.messageDisplayScripts.registerScripts([
|
||||||
{ js: [browser.runtime.getURL("resources/clearCacheButton.js")] }
|
{ js: [browser.runtime.getURL("resources/clearCacheButton.js")] },
|
||||||
|
{ js: [browser.runtime.getURL("resources/reasonButton.js")] }
|
||||||
]);
|
]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.aiLog("failed to register message display script", { level: 'warn' }, e);
|
logger.aiLog("failed to register message display script", { level: 'warn' }, e);
|
||||||
|
@ -313,6 +314,36 @@ async function clearCacheForMessages(idsInput) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.aiLog("failed to clear cache from message script", { level: 'error' }, e);
|
logger.aiLog("failed to clear cache from message script", { level: 'error' }, e);
|
||||||
}
|
}
|
||||||
|
} else if (msg?.type === "sortana:getReasons") {
|
||||||
|
try {
|
||||||
|
const id = msg.id;
|
||||||
|
const hdr = await messenger.messages.get(id);
|
||||||
|
const subject = hdr?.subject || "";
|
||||||
|
if (!aiRules.length) {
|
||||||
|
const { aiRules: stored } = await storage.local.get("aiRules");
|
||||||
|
aiRules = Array.isArray(stored) ? stored.map(r => {
|
||||||
|
if (r.actions) return r;
|
||||||
|
const actions = [];
|
||||||
|
if (r.tag) actions.push({ type: 'tag', tagKey: r.tag });
|
||||||
|
if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo });
|
||||||
|
const rule = { criterion: r.criterion, actions };
|
||||||
|
if (r.stopProcessing) rule.stopProcessing = true;
|
||||||
|
return rule;
|
||||||
|
}) : [];
|
||||||
|
}
|
||||||
|
const reasons = [];
|
||||||
|
for (const rule of aiRules) {
|
||||||
|
const key = await sha256Hex(`${id}|${rule.criterion}`);
|
||||||
|
const reason = AiClassifier.getReason(key);
|
||||||
|
if (reason) {
|
||||||
|
reasons.push({ criterion: rule.criterion, reason });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { subject, reasons };
|
||||||
|
} catch (e) {
|
||||||
|
logger.aiLog("failed to collect reasons", { level: 'error' }, e);
|
||||||
|
return { subject: '', reasons: [] };
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.aiLog("Unknown message type, ignoring", {level: 'warn'}, msg?.type);
|
logger.aiLog("Unknown message type, ignoring", {level: 'warn'}, msg?.type);
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,8 @@ let gAiParams = {
|
||||||
|
|
||||||
let gCache = new Map();
|
let gCache = new Map();
|
||||||
let gCacheLoaded = false;
|
let gCacheLoaded = false;
|
||||||
|
let gReasonCache = new Map();
|
||||||
|
let gReasonCacheLoaded = false;
|
||||||
|
|
||||||
async function loadCache() {
|
async function loadCache() {
|
||||||
if (gCacheLoaded) {
|
if (gCacheLoaded) {
|
||||||
|
@ -94,6 +96,50 @@ async function saveCache(updatedKey, updatedValue) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadReasonCache() {
|
||||||
|
if (gReasonCacheLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
aiLog(`[AiClassifier] Loading reason cache`, {debug: true});
|
||||||
|
try {
|
||||||
|
const { aiReasonCache } = await storage.local.get("aiReasonCache");
|
||||||
|
if (aiReasonCache) {
|
||||||
|
for (let [k, v] of Object.entries(aiReasonCache)) {
|
||||||
|
aiLog(`[AiClassifier] ⮡ Loaded reason '${k}'`, {debug: true});
|
||||||
|
gReasonCache.set(k, v);
|
||||||
|
}
|
||||||
|
aiLog(`[AiClassifier] Loaded ${gReasonCache.size} reason entries`, {debug: true});
|
||||||
|
} else {
|
||||||
|
aiLog(`[AiClassifier] Reason cache is empty`, {debug: true});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
aiLog(`Failed to load reason cache`, {level: 'error'}, e);
|
||||||
|
}
|
||||||
|
gReasonCacheLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadReasonCacheSync() {
|
||||||
|
if (!gReasonCacheLoaded) {
|
||||||
|
if (!Services?.tm?.spinEventLoopUntil) {
|
||||||
|
throw new Error("loadReasonCacheSync requires Services");
|
||||||
|
}
|
||||||
|
let done = false;
|
||||||
|
loadReasonCache().finally(() => { done = true; });
|
||||||
|
Services.tm.spinEventLoopUntil(() => done);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveReasonCache(updatedKey, updatedValue) {
|
||||||
|
if (typeof updatedKey !== "undefined") {
|
||||||
|
aiLog(`[AiClassifier] ⮡ Persisting reason '${updatedKey}'`, {debug: true});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await storage.local.set({ aiReasonCache: Object.fromEntries(gReasonCache) });
|
||||||
|
} catch (e) {
|
||||||
|
aiLog(`Failed to save reason cache`, {level: 'error'}, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadTemplate(name) {
|
async function loadTemplate(name) {
|
||||||
try {
|
try {
|
||||||
const url = typeof browser !== "undefined" && browser.runtime?.getURL
|
const url = typeof browser !== "undefined" && browser.runtime?.getURL
|
||||||
|
@ -185,6 +231,17 @@ function getCachedResult(cacheKey) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getReason(cacheKey) {
|
||||||
|
if (!gReasonCacheLoaded) {
|
||||||
|
if (Services?.tm?.spinEventLoopUntil) {
|
||||||
|
loadReasonCacheSync();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cacheKey ? gReasonCache.get(cacheKey) || null : null;
|
||||||
|
}
|
||||||
|
|
||||||
function buildPayload(text, criterion) {
|
function buildPayload(text, criterion) {
|
||||||
let payloadObj = Object.assign({
|
let payloadObj = Object.assign({
|
||||||
prompt: buildPrompt(text, criterion)
|
prompt: buildPrompt(text, criterion)
|
||||||
|
@ -199,7 +256,8 @@ function parseMatch(result) {
|
||||||
const cleanedText = rawText.replace(/<think>[\s\S]*?<\/think>/gi, "").trim();
|
const cleanedText = rawText.replace(/<think>[\s\S]*?<\/think>/gi, "").trim();
|
||||||
aiLog('[AiClassifier] ⮡ Cleaned Response Text:', {debug: true}, cleanedText);
|
aiLog('[AiClassifier] ⮡ Cleaned Response Text:', {debug: true}, cleanedText);
|
||||||
const obj = JSON.parse(cleanedText);
|
const obj = JSON.parse(cleanedText);
|
||||||
return obj.matched === true || obj.match === true;
|
const matched = obj.matched === true || obj.match === true;
|
||||||
|
return { matched, reason: thinkText };
|
||||||
}
|
}
|
||||||
|
|
||||||
function cacheResult(cacheKey, matched) {
|
function cacheResult(cacheKey, matched) {
|
||||||
|
@ -210,6 +268,14 @@ function cacheResult(cacheKey, matched) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cacheReason(cacheKey, reason) {
|
||||||
|
if (cacheKey) {
|
||||||
|
aiLog(`[AiClassifier] Caching reason '${cacheKey}'`, {debug: true});
|
||||||
|
gReasonCache.set(cacheKey, reason);
|
||||||
|
saveReasonCache(cacheKey, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function removeCacheEntries(keys = []) {
|
async function removeCacheEntries(keys = []) {
|
||||||
if (!Array.isArray(keys)) {
|
if (!Array.isArray(keys)) {
|
||||||
keys = [keys];
|
keys = [keys];
|
||||||
|
@ -223,9 +289,14 @@ async function removeCacheEntries(keys = []) {
|
||||||
removed = true;
|
removed = true;
|
||||||
aiLog(`[AiClassifier] Removed cache entry '${key}'`, {debug: true});
|
aiLog(`[AiClassifier] Removed cache entry '${key}'`, {debug: true});
|
||||||
}
|
}
|
||||||
|
if (gReasonCache.delete(key)) {
|
||||||
|
removed = true;
|
||||||
|
aiLog(`[AiClassifier] Removed reason entry '${key}'`, {debug: true});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (removed) {
|
if (removed) {
|
||||||
await saveCache();
|
await saveCache();
|
||||||
|
await saveReasonCache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,6 +304,9 @@ function classifyTextSync(text, criterion, cacheKey = null) {
|
||||||
if (!Services?.tm?.spinEventLoopUntil) {
|
if (!Services?.tm?.spinEventLoopUntil) {
|
||||||
throw new Error("classifyTextSync requires Services");
|
throw new Error("classifyTextSync requires Services");
|
||||||
}
|
}
|
||||||
|
if (!gReasonCacheLoaded) {
|
||||||
|
loadReasonCacheSync();
|
||||||
|
}
|
||||||
const cached = getCachedResult(cacheKey);
|
const cached = getCachedResult(cacheKey);
|
||||||
if (cached !== null) {
|
if (cached !== null) {
|
||||||
return cached;
|
return cached;
|
||||||
|
@ -255,7 +329,9 @@ function classifyTextSync(text, criterion, cacheKey = null) {
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
aiLog(`[AiClassifier] Received response:`, {debug: true}, json);
|
aiLog(`[AiClassifier] Received response:`, {debug: true}, json);
|
||||||
result = parseMatch(json);
|
result = parseMatch(json);
|
||||||
cacheResult(cacheKey, result);
|
cacheResult(cacheKey, result.matched);
|
||||||
|
cacheReason(cacheKey, result.reason);
|
||||||
|
result = result.matched;
|
||||||
} else {
|
} else {
|
||||||
aiLog(`HTTP status ${response.status}`, {level: 'warn'});
|
aiLog(`HTTP status ${response.status}`, {level: 'warn'});
|
||||||
result = false;
|
result = false;
|
||||||
|
@ -275,6 +351,9 @@ async function classifyText(text, criterion, cacheKey = null) {
|
||||||
if (!gCacheLoaded) {
|
if (!gCacheLoaded) {
|
||||||
await loadCache();
|
await loadCache();
|
||||||
}
|
}
|
||||||
|
if (!gReasonCacheLoaded) {
|
||||||
|
await loadReasonCache();
|
||||||
|
}
|
||||||
const cached = getCachedResult(cacheKey);
|
const cached = getCachedResult(cacheKey);
|
||||||
if (cached !== null) {
|
if (cached !== null) {
|
||||||
return cached;
|
return cached;
|
||||||
|
@ -298,13 +377,14 @@ async function classifyText(text, criterion, cacheKey = null) {
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
aiLog(`[AiClassifier] Received response:`, {debug: true}, result);
|
aiLog(`[AiClassifier] Received response:`, {debug: true}, result);
|
||||||
const matched = parseMatch(result);
|
const parsed = parseMatch(result);
|
||||||
cacheResult(cacheKey, matched);
|
cacheResult(cacheKey, parsed.matched);
|
||||||
return matched;
|
cacheReason(cacheKey, parsed.reason);
|
||||||
|
return parsed.matched;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
aiLog(`HTTP request failed`, {level: 'error'}, e);
|
aiLog(`HTTP request failed`, {level: 'error'}, e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { classifyText, classifyTextSync, setConfig, removeCacheEntries };
|
export { classifyText, classifyTextSync, setConfig, removeCacheEntries, getReason };
|
||||||
|
|
17
reasoning.html
Normal file
17
reasoning.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>AI Reasoning</title>
|
||||||
|
<link rel="stylesheet" href="options/bulma.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title" id="subject"></h1>
|
||||||
|
<div id="rules"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<script src="reasoning.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
27
reasoning.js
Normal file
27
reasoning.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const id = parseInt(params.get('mid'), 10);
|
||||||
|
if (!id) return;
|
||||||
|
try {
|
||||||
|
const { subject, reasons } = await browser.runtime.sendMessage({ type: 'sortana:getReasons', id });
|
||||||
|
document.getElementById('subject').textContent = subject;
|
||||||
|
const container = document.getElementById('rules');
|
||||||
|
for (const r of reasons) {
|
||||||
|
const article = document.createElement('article');
|
||||||
|
article.className = 'message mb-4';
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'message-header';
|
||||||
|
header.innerHTML = `<p>${r.criterion}</p>`;
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.className = 'message-body';
|
||||||
|
const pre = document.createElement('pre');
|
||||||
|
pre.textContent = r.reason;
|
||||||
|
body.appendChild(pre);
|
||||||
|
article.appendChild(header);
|
||||||
|
article.appendChild(body);
|
||||||
|
container.appendChild(article);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('failed to load reasons', e);
|
||||||
|
}
|
||||||
|
});
|
34
resources/reasonButton.js
Normal file
34
resources/reasonButton.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
(function() {
|
||||||
|
function addButton() {
|
||||||
|
const toolbar = document.querySelector("#header-view-toolbar") ||
|
||||||
|
document.querySelector("#mail-toolbox toolbar");
|
||||||
|
if (!toolbar || document.getElementById('sortana-reason-button')) return;
|
||||||
|
const button = document.createXULElement ?
|
||||||
|
document.createXULElement('toolbarbutton') :
|
||||||
|
document.createElement('button');
|
||||||
|
button.id = 'sortana-reason-button';
|
||||||
|
button.setAttribute('label', 'Show Reasoning');
|
||||||
|
button.className = 'toolbarbutton-1';
|
||||||
|
const icon = browser.runtime.getURL('resources/img/brain.png');
|
||||||
|
if (button.setAttribute) {
|
||||||
|
button.setAttribute('image', icon);
|
||||||
|
} else {
|
||||||
|
button.style.backgroundImage = `url(${icon})`;
|
||||||
|
button.style.backgroundSize = 'contain';
|
||||||
|
}
|
||||||
|
button.addEventListener('command', async () => {
|
||||||
|
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||||
|
const tabId = tabs[0]?.id;
|
||||||
|
const msgs = tabId ? await browser.messageDisplay.getDisplayedMessages(tabId) : [];
|
||||||
|
if (!msgs.length) return;
|
||||||
|
const url = browser.runtime.getURL(`reasoning.html?mid=${msgs[0].id}`);
|
||||||
|
browser.tabs.create({ url });
|
||||||
|
});
|
||||||
|
toolbar.appendChild(button);
|
||||||
|
}
|
||||||
|
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||||
|
addButton();
|
||||||
|
} else {
|
||||||
|
document.addEventListener('DOMContentLoaded', addButton, { once: true });
|
||||||
|
}
|
||||||
|
})();
|
Loading…
Add table
Add a link
Reference in a new issue