Compare commits

..

No commits in common. "main" and "codex/implement-token-reduction-strategies" have entirely different histories.

15 changed files with 27 additions and 2391 deletions

View file

@ -16,7 +16,6 @@ message meets a specified criterion.
- **Advanced parameters** tune generation settings like temperature, topp and more from the options page. - **Advanced parameters** tune generation settings like temperature, topp and more from the options page.
- **Markdown conversion** optionally convert HTML bodies to Markdown before sending them to the AI service. - **Markdown conversion** optionally convert HTML bodies to Markdown before sending them to the AI service.
- **Debug logging** optional colorized logs help troubleshoot interactions with the AI service. - **Debug logging** optional colorized logs help troubleshoot interactions with the AI service.
- **Debug tab** view the last request payload and a diff between the unaltered message text and the final prompt.
- **Light/Dark themes** automatically match Thunderbird's appearance with optional manual override. - **Light/Dark themes** automatically match Thunderbird's appearance with optional manual override.
- **Automatic rules** create rules that tag, move, copy, forward, reply, delete, archive, mark read/unread or flag/unflag messages based on AI classification. Rules can optionally apply only to unread messages and can ignore messages outside a chosen age range. - **Automatic rules** create rules that tag, move, copy, forward, reply, delete, archive, mark read/unread or flag/unflag messages based on AI classification. Rules can optionally apply only to unread messages and can ignore messages outside a chosen age range.
- **Rule ordering** drag rules to prioritize them and optionally stop processing after a match. - **Rule ordering** drag rules to prioritize them and optionally stop processing after a match.
@ -141,8 +140,6 @@ uses the following third party libraries:
- MIT License - MIT License
- [turndown v7.2.0](https://github.com/mixmark-io/turndown/tree/v7.2.0) - [turndown v7.2.0](https://github.com/mixmark-io/turndown/tree/v7.2.0)
- MIT License - MIT License
- [diff](https://github.com/google/diff-match-patch/blob/62f2e689f498f9c92dbc588c58750addec9b1654/javascript/diff_match_patch_uncompressed.js)
- Apache-2.0 license
## License ## License

View file

@ -19,7 +19,8 @@
"options.stripUrlParams": { "message": "Remove URL tracking parameters" }, "options.stripUrlParams": { "message": "Remove URL tracking parameters" },
"options.altTextImages": { "message": "Replace images with alt text" }, "options.altTextImages": { "message": "Replace images with alt text" },
"options.collapseWhitespace": { "message": "Collapse long whitespace" }, "options.collapseWhitespace": { "message": "Collapse long whitespace" },
"options.tokenReduction": { "message": "Aggressive token reduction" } "options.tokenReduction": { "message": "Aggressive token reduction" },
"options.contextLength": { "message": "Context length" }
,"action.read": { "message": "read" } ,"action.read": { "message": "read" }
,"action.flag": { "message": "flag" } ,"action.flag": { "message": "flag" }
,"action.copy": { "message": "copy" } ,"action.copy": { "message": "copy" }

View file

@ -108,7 +108,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "img", "img", "{F266602F-175
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "js", "js", "{21D2A42C-3F85-465C-9141-C106AFD92B68}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "js", "js", "{21D2A42C-3F85-465C-9141-C106AFD92B68}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
resources\js\diff_match_patch_uncompressed.js = resources\js\diff_match_patch_uncompressed.js
resources\js\turndown.js = resources\js\turndown.js resources\js\turndown.js = resources\js\turndown.js
EndProjectSection EndProjectSection
EndProject EndProject

View file

@ -27,13 +27,12 @@ let stripUrlParams = false;
let altTextImages = false; let altTextImages = false;
let collapseWhitespace = false; let collapseWhitespace = false;
let tokenReduction = false; let tokenReduction = false;
let maxTokens = 4096; let contextLength = 16384;
let TurndownService = null; let TurndownService = null;
let userTheme = 'auto'; let userTheme = 'auto';
let currentTheme = 'light'; let currentTheme = 'light';
let detectSystemTheme; let detectSystemTheme;
let errorPending = false; let errorPending = false;
let showDebugTab = false;
const ERROR_NOTIFICATION_ID = 'sortana-error'; const ERROR_NOTIFICATION_ID = 'sortana-error';
function normalizeRules(rules) { function normalizeRules(rules) {
@ -210,38 +209,17 @@ function collectText(part, bodyParts, attachments) {
} }
} }
function collectRawText(part, bodyParts, attachments) { function buildEmailText(full) {
if (part.parts && part.parts.length) {
for (const p of part.parts) collectRawText(p, bodyParts, attachments);
return;
}
const ct = (part.contentType || "text/plain").toLowerCase();
const cd = (part.headers?.["content-disposition"]?.[0] || "").toLowerCase();
const body = String(part.body || "");
if (cd.includes("attachment") || !ct.startsWith("text/")) {
const nameMatch = /filename\s*=\s*"?([^";]+)/i.exec(cd) || /name\s*=\s*"?([^";]+)/i.exec(part.headers?.["content-type"]?.[0] || "");
const name = nameMatch ? nameMatch[1] : "";
attachments.push(`${name} (${ct}, ${part.size || byteSize(body)} bytes)`);
} else if (ct.startsWith("text/html")) {
const doc = new DOMParser().parseFromString(body, 'text/html');
bodyParts.push(doc.body.textContent || "");
} else {
bodyParts.push(body);
}
}
function buildEmailText(full, applyTransforms = true) {
const bodyParts = []; const bodyParts = [];
const attachments = []; const attachments = [];
const collect = applyTransforms ? collectText : collectRawText; collectText(full, bodyParts, attachments);
collect(full, bodyParts, attachments);
const headers = Object.entries(full.headers || {}) const headers = Object.entries(full.headers || {})
.map(([k, v]) => `${k}: ${v.join(' ')}`) .map(([k, v]) => `${k}: ${v.join(' ')}`)
.join('\n'); .join('\n');
const attachInfo = `Attachments: ${attachments.length}` + const attachInfo = `Attachments: ${attachments.length}` +
(attachments.length ? "\n" + attachments.map(a => ` - ${a}`).join('\n') : ""); (attachments.length ? "\n" + attachments.map(a => ` - ${a}`).join('\n') : "");
let combined = `${headers}\n${attachInfo}\n\n${bodyParts.join('\n')}`.trim(); let combined = `${headers}\n${attachInfo}\n\n${bodyParts.join('\n')}`.trim();
if (applyTransforms && tokenReduction) { if (tokenReduction) {
const seen = new Set(); const seen = new Set();
combined = combined.split('\n').filter(l => { combined = combined.split('\n').filter(l => {
if (seen.has(l)) return false; if (seen.has(l)) return false;
@ -249,7 +227,7 @@ function buildEmailText(full, applyTransforms = true) {
return true; return true;
}).join('\n'); }).join('\n');
} }
return applyTransforms ? sanitizeString(combined) : combined; return sanitizeString(combined);
} }
function updateTimingStats(elapsed) { function updateTimingStats(elapsed) {
@ -283,17 +261,13 @@ async function processMessage(id) {
updateActionIcon(); updateActionIcon();
try { try {
const full = await messenger.messages.getFull(id); const full = await messenger.messages.getFull(id);
const originalText = buildEmailText(full, false);
let text = buildEmailText(full); let text = buildEmailText(full);
if (tokenReduction && maxTokens > 0) { if (tokenReduction && contextLength > 0) {
const limit = Math.floor(maxTokens * 0.9); const limit = Math.floor(contextLength * 0.9);
if (text.length > limit) { if (text.length > limit) {
text = text.slice(0, limit); text = text.slice(0, limit);
} }
} }
if (showDebugTab) {
await storage.local.set({ lastFullText: originalText, lastPromptText: text });
}
let hdr; let hdr;
let currentTags = []; let currentTags = [];
let alreadyRead = false; let alreadyRead = false;
@ -451,7 +425,7 @@ async function clearCacheForMessages(idsInput) {
} }
try { try {
const store = await storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging", "htmlToMarkdown", "stripUrlParams", "altTextImages", "collapseWhitespace", "tokenReduction", "aiRules", "theme", "errorPending", "showDebugTab"]); const store = await storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging", "htmlToMarkdown", "stripUrlParams", "altTextImages", "collapseWhitespace", "tokenReduction", "contextLength", "aiRules", "theme", "errorPending"]);
logger.setDebug(store.debugLogging); logger.setDebug(store.debugLogging);
await AiClassifier.setConfig(store); await AiClassifier.setConfig(store);
userTheme = store.theme || 'auto'; userTheme = store.theme || 'auto';
@ -462,11 +436,8 @@ async function clearCacheForMessages(idsInput) {
altTextImages = store.altTextImages === true; altTextImages = store.altTextImages === true;
collapseWhitespace = store.collapseWhitespace === true; collapseWhitespace = store.collapseWhitespace === true;
tokenReduction = store.tokenReduction === true; tokenReduction = store.tokenReduction === true;
if (store.aiParams && typeof store.aiParams.max_tokens !== 'undefined') { contextLength = parseInt(store.contextLength) || contextLength;
maxTokens = parseInt(store.aiParams.max_tokens) || maxTokens;
}
errorPending = store.errorPending === true; errorPending = store.errorPending === true;
showDebugTab = store.showDebugTab === true;
const savedStats = await storage.local.get('classifyStats'); const savedStats = await storage.local.get('classifyStats');
if (savedStats.classifyStats && typeof savedStats.classifyStats === 'object') { if (savedStats.classifyStats && typeof savedStats.classifyStats === 'object') {
Object.assign(timingStats, savedStats.classifyStats); Object.assign(timingStats, savedStats.classifyStats);
@ -488,12 +459,7 @@ async function clearCacheForMessages(idsInput) {
if (changes.templateName) config.templateName = changes.templateName.newValue; if (changes.templateName) config.templateName = changes.templateName.newValue;
if (changes.customTemplate) config.customTemplate = changes.customTemplate.newValue; if (changes.customTemplate) config.customTemplate = changes.customTemplate.newValue;
if (changes.customSystemPrompt) config.customSystemPrompt = changes.customSystemPrompt.newValue; if (changes.customSystemPrompt) config.customSystemPrompt = changes.customSystemPrompt.newValue;
if (changes.aiParams) { if (changes.aiParams) config.aiParams = changes.aiParams.newValue;
config.aiParams = changes.aiParams.newValue;
if (changes.aiParams.newValue && typeof changes.aiParams.newValue.max_tokens !== 'undefined') {
maxTokens = parseInt(changes.aiParams.newValue.max_tokens) || maxTokens;
}
}
if (changes.debugLogging) { if (changes.debugLogging) {
config.debugLogging = changes.debugLogging.newValue === true; config.debugLogging = changes.debugLogging.newValue === true;
logger.setDebug(config.debugLogging); logger.setDebug(config.debugLogging);
@ -521,8 +487,9 @@ async function clearCacheForMessages(idsInput) {
tokenReduction = changes.tokenReduction.newValue === true; tokenReduction = changes.tokenReduction.newValue === true;
logger.aiLog("tokenReduction updated from storage change", { debug: true }, tokenReduction); logger.aiLog("tokenReduction updated from storage change", { debug: true }, tokenReduction);
} }
if (changes.showDebugTab) { if (changes.contextLength) {
showDebugTab = changes.showDebugTab.newValue === true; contextLength = parseInt(changes.contextLength.newValue) || contextLength;
logger.aiLog("contextLength updated from storage change", { debug: true }, contextLength);
} }
if (changes.errorPending) { if (changes.errorPending) {
errorPending = changes.errorPending.newValue === true; errorPending = changes.errorPending.newValue === true;

View file

@ -1,13 +1,13 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "Sortana", "name": "Sortana",
"version": "2.2.0", "version": "2.1.2",
"default_locale": "en-US", "default_locale": "en-US",
"applications": { "applications": {
"gecko": { "gecko": {
"id": "ai-filter@jordanwages", "id": "ai-filter@jordanwages",
"strict_min_version": "128.0", "strict_min_version": "128.0",
"strict_max_version": "140.*" "strict_max_version": "139.*"
} }
}, },
"icons": { "icons": {

View file

@ -308,11 +308,6 @@ async function classifyText(text, criterion, cacheKey = null) {
} }
const payload = buildPayload(text, criterion); const payload = buildPayload(text, criterion);
try {
await storage.local.set({ lastPayload: JSON.parse(payload) });
} catch (e) {
aiLog('failed to save last payload', { level: 'warn' }, e);
}
aiLog(`[AiClassifier] Sending classification request to ${gEndpoint}`, {debug: true}); aiLog(`[AiClassifier] Sending classification request to ${gEndpoint}`, {debug: true});
aiLog(`[AiClassifier] Classification request payload:`, { debug: true }, payload); aiLog(`[AiClassifier] Classification request payload:`, { debug: true }, payload);

View file

@ -31,10 +31,6 @@
.tag { .tag {
--bulma-tag-h: 318; --bulma-tag-h: 318;
} }
#diff-display {
white-space: pre-wrap;
font-family: monospace;
}
</style> </style>
</head> </head>
<body> <body>
@ -51,7 +47,6 @@
<li class="is-active" data-tab="settings"><a><span class="icon is-small"><img data-icon="settings" data-size="16" src="../resources/img/settings-light-16.png" alt=""></span><span>Settings</span></a></li> <li class="is-active" data-tab="settings"><a><span class="icon is-small"><img data-icon="settings" data-size="16" src="../resources/img/settings-light-16.png" alt=""></span><span>Settings</span></a></li>
<li data-tab="rules"><a><span class="icon is-small"><img data-icon="clipboarddata" data-size="16" src="../resources/img/clipboarddata-light-16.png" alt=""></span><span>Rules</span></a></li> <li data-tab="rules"><a><span class="icon is-small"><img data-icon="clipboarddata" data-size="16" src="../resources/img/clipboarddata-light-16.png" alt=""></span><span>Rules</span></a></li>
<li data-tab="maintenance"><a><span class="icon is-small"><img data-icon="gear" data-size="16" src="../resources/img/gear-light-16.png" alt=""></span><span>Maintenance</span></a></li> <li data-tab="maintenance"><a><span class="icon is-small"><img data-icon="gear" data-size="16" src="../resources/img/gear-light-16.png" alt=""></span><span>Maintenance</span></a></li>
<li id="debug-tab-button" class="is-hidden" data-tab="debug"><a><span class="icon is-small"><img data-icon="average" data-size="16" src="../resources/img/average-light-16.png" alt=""></span><span>Debug</span></a></li>
</ul> </ul>
</div> </div>
</div> </div>
@ -155,9 +150,10 @@
</label> </label>
</div> </div>
<div class="field"> <div class="field">
<label class="checkbox"> <label class="label" for="context-length">Context length</label>
<input type="checkbox" id="show-debug-tab"> Show debug information <div class="control">
</label> <input class="input" type="number" id="context-length">
</div>
</div> </div>
<div class="field"> <div class="field">
<label class="label" for="max_tokens">Max tokens</label> <label class="label" for="max_tokens">Max tokens</label>
@ -283,21 +279,8 @@
</p> </p>
</div> </div>
</div> </div>
<div id="debug-tab" class="tab-content is-hidden">
<h2 class="title is-4">
<span class="icon is-small"><img data-icon="average" data-size="16" src="../resources/img/average-light-16.png" alt=""></span>
<span>Debug</span>
</h2>
<pre id="payload-display"></pre>
<div id="diff-container" class="mt-4 is-hidden">
<label class="label">Prompt diff</label>
<div id="diff-display" class="box content is-family-monospace"></div>
</div>
</div>
</div> </div>
</section> </section>
<script src="../resources/js/diff_match_patch_uncompressed.js"></script>
<script src="options.js"></script> <script src="options.js"></script>
</body> </body>
</html> </html>

View file

@ -17,13 +17,10 @@ document.addEventListener('DOMContentLoaded', async () => {
'altTextImages', 'altTextImages',
'collapseWhitespace', 'collapseWhitespace',
'tokenReduction', 'tokenReduction',
'contextLength',
'aiRules', 'aiRules',
'aiCache', 'aiCache',
'theme', 'theme'
'showDebugTab',
'lastPayload',
'lastFullText',
'lastPromptText'
]); ]);
const tabButtons = document.querySelectorAll('#main-tabs li'); const tabButtons = document.querySelectorAll('#main-tabs li');
const tabs = document.querySelectorAll('.tab-content'); const tabs = document.querySelectorAll('.tab-content');
@ -68,33 +65,6 @@ document.addEventListener('DOMContentLoaded', async () => {
} }
await applyTheme(themeSelect.value); await applyTheme(themeSelect.value);
const payloadDisplay = document.getElementById('payload-display');
const diffDisplay = document.getElementById('diff-display');
const diffContainer = document.getElementById('diff-container');
let lastFullText = defaults.lastFullText || '';
let lastPromptText = defaults.lastPromptText || '';
let lastPayload = defaults.lastPayload ? JSON.stringify(defaults.lastPayload, null, 2) : '';
if (lastPayload) {
payloadDisplay.textContent = lastPayload;
}
if (lastFullText && lastPromptText && diff_match_patch) {
const dmp = new diff_match_patch();
dmp.Diff_EditCost = 4;
const diffs = dmp.diff_main(lastFullText, lastPromptText);
dmp.diff_cleanupEfficiency(diffs);
const hasDiff = diffs.some(d => d[0] !== 0);
if (hasDiff) {
diffDisplay.innerHTML = dmp.diff_prettyHtml(diffs);
diffContainer.classList.remove('is-hidden');
} else {
diffDisplay.innerHTML = '';
diffContainer.classList.add('is-hidden');
}
} else {
diffContainer.classList.add('is-hidden');
}
themeSelect.addEventListener('change', async () => { themeSelect.addEventListener('change', async () => {
markDirty(); markDirty();
await applyTheme(themeSelect.value); await applyTheme(themeSelect.value);
@ -150,16 +120,8 @@ document.addEventListener('DOMContentLoaded', async () => {
const tokenReductionToggle = document.getElementById('token-reduction'); const tokenReductionToggle = document.getElementById('token-reduction');
tokenReductionToggle.checked = defaults.tokenReduction === true; tokenReductionToggle.checked = defaults.tokenReduction === true;
const debugTabToggle = document.getElementById('show-debug-tab'); const contextLengthInput = document.getElementById('context-length');
const debugTabBtn = document.getElementById('debug-tab-button'); contextLengthInput.value = defaults.contextLength || 16384;
function updateDebugTab() {
const visible = debugTabToggle.checked;
debugTabBtn.classList.toggle('is-hidden', !visible);
}
debugTabToggle.checked = defaults.showDebugTab === true;
debugTabToggle.addEventListener('change', () => { updateDebugTab(); markDirty(); });
updateDebugTab();
const aiParams = Object.assign({}, DEFAULT_AI_PARAMS, defaults.aiParams || {}); const aiParams = Object.assign({}, DEFAULT_AI_PARAMS, defaults.aiParams || {});
for (const [key, val] of Object.entries(aiParams)) { for (const [key, val] of Object.entries(aiParams)) {
@ -744,38 +706,6 @@ document.addEventListener('DOMContentLoaded', async () => {
} catch { } catch {
cacheCountEl.textContent = '?'; cacheCountEl.textContent = '?';
} }
try {
if (debugTabToggle.checked) {
const latest = await storage.local.get(['lastPayload', 'lastFullText', 'lastPromptText']);
const payloadStr = latest.lastPayload ? JSON.stringify(latest.lastPayload, null, 2) : '';
if (payloadStr !== lastPayload) {
lastPayload = payloadStr;
payloadDisplay.textContent = payloadStr;
}
if (latest.lastFullText !== lastFullText || latest.lastPromptText !== lastPromptText) {
lastFullText = latest.lastFullText || '';
lastPromptText = latest.lastPromptText || '';
if (lastFullText && lastPromptText && diff_match_patch) {
const dmp = new diff_match_patch();
dmp.Diff_EditCost = 4;
const diffs = dmp.diff_main(lastFullText, lastPromptText);
dmp.diff_cleanupEfficiency(diffs);
const hasDiff = diffs.some(d => d[0] !== 0);
if (hasDiff) {
diffDisplay.innerHTML = dmp.diff_prettyHtml(diffs);
diffContainer.classList.remove('is-hidden');
} else {
diffDisplay.innerHTML = '';
diffContainer.classList.add('is-hidden');
}
} else {
diffDisplay.innerHTML = '';
diffContainer.classList.add('is-hidden');
}
}
}
} catch {}
} }
refreshMaintenance(); refreshMaintenance();
@ -870,9 +800,9 @@ document.addEventListener('DOMContentLoaded', async () => {
const altTextImages = altTextToggle.checked; const altTextImages = altTextToggle.checked;
const collapseWhitespace = collapseWhitespaceToggle.checked; const collapseWhitespace = collapseWhitespaceToggle.checked;
const tokenReduction = tokenReductionToggle.checked; const tokenReduction = tokenReductionToggle.checked;
const showDebugTab = debugTabToggle.checked; const contextLength = parseInt(contextLengthInput.value) || 0;
const theme = themeSelect.value; const theme = themeSelect.value;
await storage.local.set({ endpoint, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging, htmlToMarkdown, stripUrlParams, altTextImages, collapseWhitespace, tokenReduction, aiRules: rules, theme, showDebugTab }); await storage.local.set({ endpoint, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging, htmlToMarkdown, stripUrlParams, altTextImages, collapseWhitespace, tokenReduction, contextLength, aiRules: rules, theme });
await applyTheme(theme); await applyTheme(theme);
try { try {
await AiClassifier.setConfig({ endpoint, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging }); await AiClassifier.setConfig({ endpoint, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 828 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 813 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because it is too large Load diff