Migrate classifier to chat completions
This commit is contained in:
parent
245bb2e3e1
commit
76195f9a92
15 changed files with 73 additions and 192 deletions
0
.codex
Normal file
0
.codex
Normal file
|
|
@ -10,7 +10,6 @@ This file provides guidelines for codex agents contributing to the Sortana proje
|
|||
- `options/`: The options page HTML, JavaScript and bundled Bulma CSS (v1.0.3).
|
||||
- `details.html` and `details.js`: View AI reasoning and clear cache for a message.
|
||||
- `resources/`: Images and other static files.
|
||||
- `prompt_templates/`: Prompt template files for the AI service (openai, qwen, mistral, harmony).
|
||||
- `build-xpi.ps1`: PowerShell script to package the extension.
|
||||
- `build-xpi.sh`: Bash script to package the extension.
|
||||
- `resources/svg2img.ps1`: PowerShell script to regenerate themed PNG icons from SVGs.
|
||||
|
|
@ -35,10 +34,10 @@ There are currently no automated tests for this project. If you add tests in the
|
|||
|
||||
## Endpoint Notes
|
||||
|
||||
Sortana targets the `/v1/completions` API. The endpoint value stored in settings is a base URL; the full request URL is constructed by appending `/v1/completions` (adding a slash when needed) and defaulting to `https://` if no scheme is provided.
|
||||
Sortana targets the OpenAI-compatible `/v1/chat/completions` API. The endpoint value stored in settings is a base URL; the full request URL is constructed by appending `/v1/chat/completions` (adding a slash when needed) and defaulting to `https://` if no scheme is provided.
|
||||
The options page can query `/v1/models` from the same base URL to populate the Model dropdown; selecting **None** omits the `model` field from the request payload.
|
||||
Advanced options allow an optional API key plus `OpenAI-Organization` and `OpenAI-Project` headers; these headers are only sent when values are provided.
|
||||
Responses are expected to include a JSON object with `match` (or `matched`) plus a short `reason` string; the parser extracts the last JSON object in the response text and ignores any surrounding commentary.
|
||||
Requests use `messages` with system and user roles. Responses are expected to include text in `choices[0].message.content` containing a JSON object with `match` (or `matched`) plus a short `reason` string; the parser extracts the last JSON object in that content and ignores any surrounding commentary.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
|
|
|||
17
README.md
17
README.md
|
|
@ -4,11 +4,12 @@
|
|||
|
||||
Sortana is an experimental Thunderbird add-on that integrates an AI-powered filter rule.
|
||||
It allows you to classify email messages by sending their contents to a configurable
|
||||
HTTP endpoint. Sortana uses the `/v1/completions` API; the options page stores a base
|
||||
URL and appends `/v1/completions` when sending requests. The endpoint should respond
|
||||
with JSON indicating whether the message meets a specified criterion, including a
|
||||
short reasoning summary.
|
||||
Responses are parsed by extracting the last JSON object in the response text and
|
||||
HTTP endpoint. Sortana uses the OpenAI-compatible `/v1/chat/completions` API; the options page stores a base
|
||||
URL and appends `/v1/chat/completions` when sending requests. Requests contain a system
|
||||
message and a user message. The endpoint should respond
|
||||
with text in `choices[0].message.content` containing JSON indicating whether the
|
||||
message meets a specified criterion, including a short reasoning summary.
|
||||
Responses are parsed by extracting the last JSON object in that content and
|
||||
expecting a `match` (or `matched`) boolean plus a `reason` string.
|
||||
|
||||
## Features
|
||||
|
|
@ -16,10 +17,10 @@ expecting a `match` (or `matched`) boolean plus a `reason` string.
|
|||
- **Configurable endpoint** – set the classification service base URL on the options page.
|
||||
- **Model selection** – load available models from the endpoint and choose one (or omit the model field).
|
||||
- **Optional OpenAI auth headers** – provide an API key plus optional organization/project headers when needed.
|
||||
- **Prompt templates** – choose between OpenAI/ChatML, Qwen, Mistral, Harmony (gpt-oss), or provide your own custom template.
|
||||
- **Custom system prompts** – tailor the instructions sent to the model for more precise results.
|
||||
- **Custom system prompts** – tailor the instructions sent as the system message for more precise results.
|
||||
- **Persistent result caching** – classification results and reasoning are saved to disk so messages aren't re-evaluated across restarts.
|
||||
- **Advanced parameters** – tune generation settings like temperature, top‑p and more from the options page.
|
||||
Provider-specific advanced fields are forwarded as configured and may be rejected by strict OpenAI endpoints.
|
||||
- **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 tab** – view the last request payload and a diff between the unaltered message text and the final prompt.
|
||||
|
|
@ -83,7 +84,7 @@ Sortana is implemented entirely with documented MailExtension/WebExtension APIs.
|
|||
## Usage
|
||||
|
||||
1. Open the add-on's options and set the base URL of your classification service
|
||||
(Sortana will append `/v1/completions`). Use the Model dropdown to load
|
||||
(Sortana will append `/v1/chat/completions`). Use the Model dropdown to load
|
||||
`/v1/models` and select a model or choose **None** to omit the `model` field.
|
||||
Advanced settings include optional API key, organization, and project headers
|
||||
for OpenAI-hosted endpoints.
|
||||
|
|
|
|||
|
|
@ -4,16 +4,8 @@
|
|||
"doesntMatch": { "message": "doesn't match" },
|
||||
"options.title": { "message": "AI Filter Options" },
|
||||
"options.endpoint": { "message": "Endpoint" },
|
||||
"options.template": { "message": "Prompt template" },
|
||||
"options.customTemplate": { "message": "Custom template" },
|
||||
"options.systemInstructions": { "message": "System instructions" },
|
||||
"options.reset": { "message": "Reset to default" },
|
||||
"options.placeholders": { "message": "Placeholders: {{system}}, {{email}}, {{query}}" },
|
||||
"template.openai": { "message": "OpenAI / ChatML" },
|
||||
"template.qwen": { "message": "Qwen" },
|
||||
"template.mistral": { "message": "Mistral" },
|
||||
"template.harmony": { "message": "Harmony (gpt-oss)" },
|
||||
"template.custom": { "message": "Custom" },
|
||||
"options.save": { "message": "Save" },
|
||||
"options.debugLogging": { "message": "Enable debug logging" },
|
||||
"options.htmlToMarkdown": { "message": "Convert HTML body to Markdown" },
|
||||
|
|
|
|||
|
|
@ -39,14 +39,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "en-US", "en-US", "{8BEA7793
|
|||
_locales\en-US\messages.json = _locales\en-US\messages.json
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "prompt_templates", "prompt_templates", "{86516D53-50D4-4FE2-9D8A-977A8F5EBDBD}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
prompt_templates\mistral.txt = prompt_templates\mistral.txt
|
||||
prompt_templates\openai.txt = prompt_templates\openai.txt
|
||||
prompt_templates\qwen.txt = prompt_templates\qwen.txt
|
||||
prompt_templates\harmony.txt = prompt_templates\harmony.txt
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "resources", "resources", "{68A87938-5C2B-49F5-8AAA-8A34FBBFD854}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
resources\svg2img.ps1 = resources\svg2img.ps1
|
||||
|
|
|
|||
|
|
@ -484,7 +484,7 @@ async function clearCacheForMessages(idsInput) {
|
|||
}
|
||||
|
||||
try {
|
||||
const store = await storage.local.get(["endpoint", "model", "apiKey", "openaiOrganization", "openaiProject", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging", "htmlToMarkdown", "stripUrlParams", "altTextImages", "collapseWhitespace", "tokenReduction", "aiRules", "theme", "showDebugTab"]);
|
||||
const store = await storage.local.get(["endpoint", "model", "apiKey", "openaiOrganization", "openaiProject", "customSystemPrompt", "aiParams", "debugLogging", "htmlToMarkdown", "stripUrlParams", "altTextImages", "collapseWhitespace", "tokenReduction", "aiRules", "theme", "showDebugTab"]);
|
||||
logger.setDebug(store.debugLogging);
|
||||
await AiClassifier.setConfig(store);
|
||||
userTheme = store.theme || 'auto';
|
||||
|
|
@ -514,15 +514,13 @@ async function clearCacheForMessages(idsInput) {
|
|||
aiRules = normalizeRules(newRules);
|
||||
logger.aiLog("aiRules updated from storage change", { debug: true }, aiRules);
|
||||
}
|
||||
if (changes.endpoint || changes.model || changes.apiKey || changes.openaiOrganization || changes.openaiProject || changes.templateName || changes.customTemplate || changes.customSystemPrompt || changes.aiParams || changes.debugLogging) {
|
||||
if (changes.endpoint || changes.model || changes.apiKey || changes.openaiOrganization || changes.openaiProject || changes.customSystemPrompt || changes.aiParams || changes.debugLogging) {
|
||||
const config = {};
|
||||
if (changes.endpoint) config.endpoint = changes.endpoint.newValue;
|
||||
if (changes.model) config.model = changes.model.newValue;
|
||||
if (changes.apiKey) config.apiKey = changes.apiKey.newValue;
|
||||
if (changes.openaiOrganization) config.openaiOrganization = changes.openaiOrganization.newValue;
|
||||
if (changes.openaiProject) config.openaiProject = changes.openaiProject.newValue;
|
||||
if (changes.templateName) config.templateName = changes.templateName.newValue;
|
||||
if (changes.customTemplate) config.customTemplate = changes.customTemplate.newValue;
|
||||
if (changes.customSystemPrompt) config.customSystemPrompt = changes.customSystemPrompt.newValue;
|
||||
if (changes.aiParams) {
|
||||
config.aiParams = changes.aiParams.newValue;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Sortana",
|
||||
"version": "2.4.3",
|
||||
"version": "3.0.0",
|
||||
"default_locale": "en-US",
|
||||
"applications": {
|
||||
"gecko": {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { DEFAULT_AI_PARAMS } from "./defaultParams.js";
|
|||
|
||||
const storage = (globalThis.messenger ?? globalThis.browser).storage;
|
||||
|
||||
const COMPLETIONS_PATH = "/v1/completions";
|
||||
const CHAT_COMPLETIONS_PATH = "/v1/chat/completions";
|
||||
const MODELS_PATH = "/v1/models";
|
||||
|
||||
const SYSTEM_PREFIX = `You are an email-classification assistant.
|
||||
|
|
@ -22,10 +22,7 @@ Do not add any other keys, text, or formatting.`;
|
|||
|
||||
let gEndpointBase = "http://127.0.0.1:5000";
|
||||
let gEndpoint = buildEndpointUrl(gEndpointBase);
|
||||
let gTemplateName = "openai";
|
||||
let gCustomTemplate = "";
|
||||
let gCustomSystemPrompt = DEFAULT_CUSTOM_SYSTEM_PROMPT;
|
||||
let gTemplateText = "";
|
||||
|
||||
let gAiParams = Object.assign({}, DEFAULT_AI_PARAMS);
|
||||
let gModel = "";
|
||||
|
|
@ -44,7 +41,7 @@ function normalizeEndpointBase(endpoint) {
|
|||
if (!base) {
|
||||
return "";
|
||||
}
|
||||
base = base.replace(/\/v1\/(completions|models)\/?$/i, "");
|
||||
base = base.replace(/\/v1\/(chat\/completions|completions|models)\/?$/i, "");
|
||||
return base;
|
||||
}
|
||||
|
||||
|
|
@ -55,7 +52,7 @@ function buildEndpointUrl(endpointBase) {
|
|||
}
|
||||
const withScheme = /^https?:\/\//i.test(base) ? base : `https://${base}`;
|
||||
const needsSlash = withScheme.endsWith("/");
|
||||
const path = COMPLETIONS_PATH.replace(/^\//, "");
|
||||
const path = CHAT_COMPLETIONS_PATH.replace(/^\//, "");
|
||||
return `${withScheme}${needsSlash ? "" : "/"}${path}`;
|
||||
}
|
||||
|
||||
|
|
@ -147,21 +144,6 @@ async function saveCache(updatedKey, updatedValue) {
|
|||
}
|
||||
|
||||
|
||||
async function loadTemplate(name) {
|
||||
try {
|
||||
const url = typeof browser !== "undefined" && browser.runtime?.getURL
|
||||
? browser.runtime.getURL(`prompt_templates/${name}.txt`)
|
||||
: `resource://aifilter/prompt_templates/${name}.txt`;
|
||||
const res = await fetch(url);
|
||||
if (res.ok) {
|
||||
return await res.text();
|
||||
}
|
||||
} catch (e) {
|
||||
aiLog(`Failed to load template '${name}':`, {level: 'error'}, e);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async function setConfig(config = {}) {
|
||||
if (typeof config.endpoint === "string") {
|
||||
const base = normalizeEndpointBase(config.endpoint);
|
||||
|
|
@ -170,12 +152,6 @@ async function setConfig(config = {}) {
|
|||
}
|
||||
gEndpoint = buildEndpointUrl(gEndpointBase);
|
||||
}
|
||||
if (config.templateName) {
|
||||
gTemplateName = config.templateName;
|
||||
}
|
||||
if (typeof config.customTemplate === "string") {
|
||||
gCustomTemplate = config.customTemplate;
|
||||
}
|
||||
if (typeof config.customSystemPrompt === "string") {
|
||||
gCustomSystemPrompt = config.customSystemPrompt;
|
||||
}
|
||||
|
|
@ -201,17 +177,11 @@ async function setConfig(config = {}) {
|
|||
if (typeof config.debugLogging === "boolean") {
|
||||
setDebug(config.debugLogging);
|
||||
}
|
||||
if (gTemplateName === "custom") {
|
||||
gTemplateText = gCustomTemplate;
|
||||
} else {
|
||||
gTemplateText = await loadTemplate(gTemplateName);
|
||||
}
|
||||
if (!gEndpoint) {
|
||||
gEndpoint = buildEndpointUrl(gEndpointBase);
|
||||
}
|
||||
aiLog(`[AiClassifier] Endpoint base set to ${gEndpointBase}`, {debug: true});
|
||||
aiLog(`[AiClassifier] Endpoint set to ${gEndpoint}`, {debug: true});
|
||||
aiLog(`[AiClassifier] Template set to ${gTemplateName}`, {debug: true});
|
||||
}
|
||||
|
||||
function buildAuthHeaders() {
|
||||
|
|
@ -234,13 +204,7 @@ function buildSystemPrompt() {
|
|||
|
||||
function buildPrompt(body, criterion) {
|
||||
aiLog(`[AiClassifier] Building prompt with criterion: "${criterion}"`, {debug: true});
|
||||
const data = {
|
||||
system: buildSystemPrompt(),
|
||||
email: body,
|
||||
query: criterion,
|
||||
};
|
||||
let template = gTemplateText || "";
|
||||
return template.replace(/{{\s*(\w+)\s*}}/g, (m, key) => data[key] || "");
|
||||
return `**Email Contents**\n\`\`\`\n${body}\n\`\`\`\nClassification Criterion: ${criterion}`;
|
||||
}
|
||||
|
||||
function getCachedResult(cacheKey) {
|
||||
|
|
@ -265,7 +229,10 @@ function getReason(cacheKey) {
|
|||
|
||||
function buildPayload(text, criterion) {
|
||||
let payloadObj = Object.assign({
|
||||
prompt: buildPrompt(text, criterion)
|
||||
messages: [
|
||||
{ role: "system", content: buildSystemPrompt() },
|
||||
{ role: "user", content: buildPrompt(text, criterion) }
|
||||
]
|
||||
}, gAiParams);
|
||||
if (gModel) {
|
||||
payloadObj.model = gModel;
|
||||
|
|
@ -337,7 +304,11 @@ function extractLastJsonObject(text) {
|
|||
}
|
||||
|
||||
function parseMatch(result) {
|
||||
const rawText = result.choices?.[0]?.text || "";
|
||||
const rawText = result.choices?.[0]?.message?.content;
|
||||
if (typeof rawText !== "string") {
|
||||
reportParseError("Chat response missing text content.", JSON.stringify(result).slice(0, 800));
|
||||
return { matched: false, reason: "" };
|
||||
}
|
||||
const candidate = extractLastJsonObject(rawText);
|
||||
if (!candidate) {
|
||||
reportParseError("No JSON object found in AI response.", rawText.slice(0, 800));
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ const KEY_GROUPS = {
|
|||
'apiKey',
|
||||
'openaiOrganization',
|
||||
'openaiProject',
|
||||
'templateName',
|
||||
'customTemplate',
|
||||
'customSystemPrompt',
|
||||
'aiParams',
|
||||
'debugLogging',
|
||||
|
|
|
|||
|
|
@ -92,23 +92,6 @@
|
|||
<p class="help" id="model-help"></p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="template">Prompt template</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="template"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="custom-template-container" class="field is-hidden">
|
||||
<label class="label">Custom template</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" id="custom-template" rows="6" placeholder="Enter your custom template here..."></textarea>
|
||||
</div>
|
||||
<p class="help">Placeholders: {{system}}, {{email}}, {{query}}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="system-instructions">System instructions</label>
|
||||
<div class="control">
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
const { DEFAULT_AI_PARAMS } = await import(browser.runtime.getURL('modules/defaultParams.js'));
|
||||
const defaults = await storage.local.get([
|
||||
'endpoint',
|
||||
'templateName',
|
||||
'customTemplate',
|
||||
'customSystemPrompt',
|
||||
'model',
|
||||
'apiKey',
|
||||
|
|
@ -163,21 +161,47 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
async function fetchModels(preferredModel = '') {
|
||||
if (!modelSelect || !refreshModelsBtn) return;
|
||||
const modelsUrl = AiClassifier.buildModelsUrl(endpointInput.value);
|
||||
const selectedModel = preferredModel || modelSelect.value;
|
||||
if (!modelsUrl) {
|
||||
logger.aiLog('[options] model refresh skipped: invalid endpoint', { level: 'warn' }, {
|
||||
endpoint: endpointInput.value
|
||||
});
|
||||
setModelHelp('Set a valid endpoint to load models.', true);
|
||||
populateModelOptions([], preferredModel || modelSelect.value);
|
||||
populateModelOptions([], selectedModel);
|
||||
return;
|
||||
}
|
||||
|
||||
refreshModelsBtn.disabled = true;
|
||||
setModelHelp('Loading models...');
|
||||
const headers = buildAuthHeaders();
|
||||
logger.aiLog('[options] loading models', {}, {
|
||||
endpoint: endpointInput.value,
|
||||
modelsUrl,
|
||||
selectedModel,
|
||||
hasAuthorization: typeof headers.Authorization === 'string',
|
||||
hasOrganization: typeof headers['OpenAI-Organization'] === 'string',
|
||||
hasProject: typeof headers['OpenAI-Project'] === 'string'
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(modelsUrl, { method: 'GET', headers: buildAuthHeaders() });
|
||||
const response = await fetch(modelsUrl, { method: 'GET', headers });
|
||||
logger.aiLog('[options] model refresh response received', {}, {
|
||||
status: response.status,
|
||||
ok: response.ok,
|
||||
contentType: response.headers.get('content-type') || ''
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
logger.aiLog('[options] model refresh payload parsed', {}, {
|
||||
payloadType: Array.isArray(data) ? 'array' : typeof data,
|
||||
hasDataArray: Array.isArray(data?.data),
|
||||
hasModelsArray: Array.isArray(data?.models),
|
||||
topLevelKeys: data && typeof data === 'object' && !Array.isArray(data)
|
||||
? Object.keys(data)
|
||||
: []
|
||||
});
|
||||
let models = [];
|
||||
if (Array.isArray(data?.data)) {
|
||||
models = data.data.map(model => model?.id ?? model?.name ?? model?.model ?? '').filter(Boolean);
|
||||
|
|
@ -187,12 +211,23 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
models = data.map(model => model?.id ?? model?.name ?? model?.model ?? model).filter(Boolean);
|
||||
}
|
||||
models = [...new Set(models)];
|
||||
populateModelOptions(models, preferredModel || modelSelect.value);
|
||||
logger.aiLog('[options] model refresh parsed models', {}, {
|
||||
count: models.length,
|
||||
models
|
||||
});
|
||||
populateModelOptions(models, selectedModel);
|
||||
setModelHelp(models.length ? `Loaded ${models.length} model${models.length === 1 ? '' : 's'}.` : 'No models returned.');
|
||||
} catch (e) {
|
||||
logger.aiLog('[options] failed to load models', { level: 'warn' }, e);
|
||||
setModelHelp('Failed to load models. Check the endpoint and network.', true);
|
||||
populateModelOptions([], preferredModel || modelSelect.value);
|
||||
const message = String(e?.message || e || '');
|
||||
const isLikelyCors = e instanceof TypeError || /Failed to fetch|NetworkError|Load failed/i.test(message);
|
||||
setModelHelp(
|
||||
isLikelyCors
|
||||
? 'Failed to load models. The server responded, but Thunderbird may have blocked access to the response. Check CORS on the AI server and verify the endpoint is reachable from the add-on.'
|
||||
: 'Failed to load models. Check the endpoint, HTTP status, and network.',
|
||||
true
|
||||
);
|
||||
populateModelOptions([], selectedModel);
|
||||
} finally {
|
||||
refreshModelsBtn.disabled = false;
|
||||
}
|
||||
|
|
@ -200,35 +235,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
|
||||
populateModelOptions([], storedModel);
|
||||
refreshModelsBtn?.addEventListener('click', () => {
|
||||
logger.aiLog('[options] model refresh button clicked');
|
||||
fetchModels(modelSelect.value);
|
||||
});
|
||||
|
||||
const templates = {
|
||||
openai: browser.i18n.getMessage('template.openai'),
|
||||
qwen: browser.i18n.getMessage('template.qwen'),
|
||||
mistral: browser.i18n.getMessage('template.mistral'),
|
||||
harmony: browser.i18n.getMessage('template.harmony'),
|
||||
custom: browser.i18n.getMessage('template.custom')
|
||||
};
|
||||
const templateSelect = document.getElementById('template');
|
||||
for (const [value, label] of Object.entries(templates)) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = value;
|
||||
opt.textContent = label;
|
||||
templateSelect.appendChild(opt);
|
||||
}
|
||||
templateSelect.value = defaults.templateName || 'openai';
|
||||
|
||||
const customBox = document.getElementById('custom-template-container');
|
||||
const customTemplate = document.getElementById('custom-template');
|
||||
customTemplate.value = defaults.customTemplate || '';
|
||||
|
||||
function updateVisibility() {
|
||||
customBox.classList.toggle('is-hidden', templateSelect.value !== 'custom');
|
||||
}
|
||||
templateSelect.addEventListener('change', updateVisibility);
|
||||
updateVisibility();
|
||||
|
||||
const advancedBox = document.getElementById('advanced-options');
|
||||
const advancedBtn = document.getElementById('toggle-advanced');
|
||||
advancedBtn.addEventListener('click', () => {
|
||||
|
|
@ -1047,8 +1057,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
const apiKey = apiKeyInput?.value.trim() || '';
|
||||
const openaiOrganization = openaiOrgInput?.value.trim() || '';
|
||||
const openaiProject = openaiProjectInput?.value.trim() || '';
|
||||
const templateName = templateSelect.value;
|
||||
const customTemplateText = customTemplate.value;
|
||||
const customSystemPrompt = systemBox.value;
|
||||
const aiParamsSave = {};
|
||||
for (const key of Object.keys(DEFAULT_AI_PARAMS)) {
|
||||
|
|
@ -1112,10 +1120,11 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
const tokenReduction = tokenReductionToggle.checked;
|
||||
const showDebugTab = debugTabToggle.checked;
|
||||
const theme = themeSelect.value;
|
||||
await storage.local.set({ endpoint, model, apiKey, openaiOrganization, openaiProject, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging, htmlToMarkdown, stripUrlParams, altTextImages, collapseWhitespace, tokenReduction, aiRules: rules, theme, showDebugTab });
|
||||
await storage.local.set({ endpoint, model, apiKey, openaiOrganization, openaiProject, customSystemPrompt, aiParams: aiParamsSave, debugLogging, htmlToMarkdown, stripUrlParams, altTextImages, collapseWhitespace, tokenReduction, aiRules: rules, theme, showDebugTab });
|
||||
await storage.local.remove(['templateName', 'customTemplate']);
|
||||
await applyTheme(theme);
|
||||
try {
|
||||
await AiClassifier.setConfig({ endpoint, model, apiKey, openaiOrganization, openaiProject, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging });
|
||||
await AiClassifier.setConfig({ endpoint, model, apiKey, openaiOrganization, openaiProject, customSystemPrompt, aiParams: aiParamsSave, debugLogging });
|
||||
logger.setDebug(debugLogging);
|
||||
} catch (e) {
|
||||
logger.aiLog('[options] failed to apply config', {level: 'error'}, e);
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
<|start|>system<|message|>You are ChatGPT, a large language model trained by OpenAI.
|
||||
Knowledge cutoff: 2024-06
|
||||
Current date: 2025-06-28
|
||||
|
||||
Reasoning: medium
|
||||
|
||||
# Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|>
|
||||
<|start|>developer<|message|># Instructions
|
||||
|
||||
{{system}}<|end|>
|
||||
<|start|>user<|message|>**Email Contents**
|
||||
```
|
||||
{{email}}
|
||||
```
|
||||
Classification Criterion: {{query}}
|
||||
Remember, return ONLY a JSON object on a single line of the form:
|
||||
{"match": true, "reason": "<short explanation>"} - if the email satisfies the criterion
|
||||
{"match": false, "reason": "<short explanation>"} - otherwise
|
||||
|
||||
Do not add any other keys, text, or formatting.<|end|>
|
||||
<|start|>assistant
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
[INST] {{system}}
|
||||
|
||||
Email:
|
||||
{{email}}
|
||||
|
||||
Criterion: {{query}}
|
||||
Remember, return ONLY a JSON object on a single line of the form:
|
||||
{"match": true, "reason": "<short explanation>"} - if the email satisfies the criterion
|
||||
{"match": false, "reason": "<short explanation>"} - otherwise
|
||||
|
||||
Do not add any other keys, text, or formatting.
|
||||
[/INST]
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<|im_start|>system
|
||||
{{system}}<|im_end|>
|
||||
<|im_start|>user
|
||||
**Email Contents**
|
||||
```
|
||||
{{email}}
|
||||
```
|
||||
Classification Criterion: {{query}}
|
||||
Remember, return ONLY a JSON object on a single line of the form:
|
||||
{"match": true, "reason": "<short explanation>"} - if the email satisfies the criterion
|
||||
{"match": false, "reason": "<short explanation>"} - otherwise
|
||||
|
||||
Do not add any other keys, text, or formatting.<|im_end|>
|
||||
<|im_start|>assistant
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<|im_start|>system
|
||||
{{system}}
|
||||
<|im_end|>
|
||||
<|im_start|>user
|
||||
Email:
|
||||
{{email}}
|
||||
|
||||
Criterion: {{query}}
|
||||
Remember, return ONLY a JSON object on a single line of the form:
|
||||
{"match": true, "reason": "<short explanation>"} - if the email satisfies the criterion
|
||||
{"match": false, "reason": "<short explanation>"} - otherwise
|
||||
|
||||
Do not add any other keys, text, or formatting.
|
||||
<|im_end|>
|
||||
<|im_start|>assistant
|
||||
Loading…
Reference in a new issue