toolsProvider.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.toolsProvider = void 0;
const sdk_1 = require("@lmstudio/sdk");
const zod_1 = require("zod");
const config_1 = require("./config");
const detector_1 = require("./detector");
function json(obj) {
return JSON.stringify(obj, null, 2);
}
function safe_impl(name, fn) {
return async (params) => {
try {
return await fn(params);
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return JSON.stringify({
tool_error: true,
tool: name,
error: msg,
hint: "Read the error above, fix the parameter causing the issue, and retry the tool call.",
}, null, 2);
}
};
}
const VALID_TONES = ["casual", "professional", "academic", "custom"];
const TONE_GUIDELINES = {
casual: "Conversational and friendly. Use contractions (it's, you're, don't). " +
"Short, punchy sentences. First-person where natural. Informal vocabulary. Avoid jargon.",
professional: "Clear, direct, and natural. No stiff formality, but authoritative. " +
"Complete sentences. Avoid slang. No contrived complexity.",
academic: "Formal but varied. Nuanced and precise. Avoid LLM academic clichés " +
"('it is evident that', 'this paper examines'). Varied sentence structure.",
custom: "",
};
function resolveAndValidateTone(tone, configDefault) {
const candidate = tone ?? configDefault ?? "casual";
return VALID_TONES.includes(candidate)
? candidate
: "casual";
}
function buildHumanizePayload(inputText, tone, customToneDesc, detectedPhrases) {
const toneGuidelines = tone === "custom"
? customToneDesc
: TONE_GUIDELINES[tone];
return {
originalText: inputText,
tone,
toneGuidelines,
detectedPatterns: detectedPhrases,
instructions: [
"Rewrite the originalText above to sound authentically human-written.",
"TONE: " + toneGuidelines,
"RULES:",
"1. Vary sentence lengths significantly — mix short (3–7 word) sentences with longer ones.",
"2. Remove or rephrase every item in detectedPatterns.",
"3. Eliminate uniform paragraph lengths — some paragraphs can be a single sentence.",
"4. Replace transition words (moreover, furthermore, in conclusion) with natural connectors or omit them.",
"5. Preserve ALL original meaning, facts, and arguments — do not add, remove, or change substance.",
"6. Output ONLY the rewritten text — no preamble, no explanation of changes.",
"7. Match register to content: do not make technical content sound casual.",
].join("\n"),
};
}
const toolsProvider = async (ctl) => {
const cfg = ctl.getPluginConfig(config_1.pluginConfigSchematics);
const tools = [
(0, sdk_1.tool)({
name: "detect_ai_text",
description: (0, sdk_1.text) `
Analyze text to estimate how likely it was written by an AI (LLM).
Returns a score from 0 (human) to 100 (AI) based on five heuristic signals:
flagged phrase density, sentence burstiness, transition word density,
paragraph symmetry, and average sentence length.
NOTE: Detection is probabilistic — false positives are possible.
`,
parameters: {
textContent: zod_1.z.string().min(20).describe("The text to analyze (minimum 20 characters)"),
},
implementation: safe_impl("detect_ai_text", async ({ textContent }) => {
const result = (0, detector_1.detectAiText)(textContent);
return json({
score: result.score,
verdict: result.verdict,
signals: result.signals,
disclaimer: "Score is probabilistic. Below 30 = likely human; above 60 = likely AI.",
});
}),
}),
(0, sdk_1.tool)({
name: "humanize_text",
description: (0, sdk_1.text) `
Rewrite AI-generated text to sound authentically human.
Returns a prompt payload with instructions for the LLM to perform the rewrite.
The rewrite preserves all original meaning while varying sentence structure,
removing LLM phrases, and matching the requested tone.
Tone options: casual, professional, academic, custom.
`,
parameters: {
textContent: zod_1.z.string().min(20).describe("The AI-generated text to humanize"),
tone: zod_1.z.enum(["casual", "professional", "academic", "custom"]).optional()
.describe("Target tone. Defaults to the configured defaultTone."),
customToneDescription: zod_1.z.string().default("")
.describe("Required when tone is 'custom'. Describe the desired writing style."),
},
implementation: safe_impl("humanize_text", async ({ textContent, tone, customToneDescription }) => {
const resolvedTone = resolveAndValidateTone(tone, cfg.get("defaultTone"));
if (resolvedTone === "custom" && !customToneDescription.trim()) {
throw new Error("customToneDescription is required when tone is 'custom'.");
}
const detection = (0, detector_1.detectAiText)(textContent);
const payload = buildHumanizePayload(textContent, resolvedTone, customToneDescription, detection.flaggedPhrasesFound);
return json(payload);
}),
}),
(0, sdk_1.tool)({
name: "analyze_and_humanize",
description: (0, sdk_1.text) `
Convenience tool: detects AI score first, then returns a humanize payload
only if the score meets or exceeds the threshold.
If the text scores below the threshold, returns detection result with
a "appears human" verdict and no rewrite payload.
Use this for the full detect-then-rewrite workflow in one call.
`,
parameters: {
textContent: zod_1.z.string().min(20).describe("The text to analyze and potentially humanize"),
tone: zod_1.z.enum(["casual", "professional", "academic", "custom"]).optional()
.describe("Target tone for rewriting. Defaults to configured defaultTone."),
threshold: zod_1.z.coerce.number().int().min(0).max(100).optional()
.describe("Score threshold to trigger rewriting. Defaults to configured aiScoreThreshold."),
customToneDescription: zod_1.z.string().default("")
.describe("Required when tone is 'custom'."),
},
implementation: safe_impl("analyze_and_humanize", async ({ textContent, tone, threshold, customToneDescription }) => {
const resolvedTone = resolveAndValidateTone(tone, cfg.get("defaultTone"));
const resolvedThreshold = threshold ?? cfg.get("aiScoreThreshold") ?? 60;
const detection = (0, detector_1.detectAiText)(textContent);
if (detection.score < resolvedThreshold) {
return json({
score: detection.score,
verdict: detection.verdict,
signals: detection.signals,
rewrite: null,
reason: `Score ${detection.score} is below threshold ${resolvedThreshold}. Text reads as sufficiently human-written.`,
});
}
if (resolvedTone === "custom" && !customToneDescription.trim()) {
throw new Error("customToneDescription is required when tone is 'custom'.");
}
const payload = buildHumanizePayload(textContent, resolvedTone, customToneDescription, detection.flaggedPhrasesFound);
return json({
score: detection.score,
verdict: detection.verdict,
signals: detection.signals,
threshold: resolvedThreshold,
rewrite: payload,
});
}),
}),
];
return tools;
};
exports.toolsProvider = toolsProvider;