139 lines
4.9 KiB
JavaScript
139 lines
4.9 KiB
JavaScript
const {
|
|
normalizeLink,
|
|
absoluteInternalUrl,
|
|
routeLabel,
|
|
stripTags
|
|
} = require("./link_normalizer");
|
|
|
|
const METHODS = /\b(?:GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s+(\/[^\s<>"')\]]+)/gi;
|
|
const MARKDOWN_LINK = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
const HTML_LINK = /<a\b[^>]*\bhref\s*=\s*(["'])(.*?)\1[^>]*>(.*?)<\/a>/gi;
|
|
const REPO_PATH = /\b(?:src|plugins|scripts|data)\/[a-z0-9_./-]+\.(?:js|json|ejs|md|css|txt)\b/gi;
|
|
const SOURCE_DETAIL = /\s*\((?:src|plugins|scripts)\/[^)]+\)/gi;
|
|
|
|
function formatAssistantResponse({
|
|
text,
|
|
links = [],
|
|
baseUrl = "",
|
|
verifiedRoutes = [],
|
|
role = "user",
|
|
allowModeratorCodeHelp = false,
|
|
maxLength = null
|
|
}) {
|
|
const routeSet = new Set(verifiedRoutes);
|
|
const collected = [];
|
|
let output = normalizeCodeFences(String(text || ""));
|
|
|
|
output = output.replace(HTML_LINK, (_match, _quote, href, label) => {
|
|
const normalized = collectLink(collected, { href, label: stripTags(label), external: true }, baseUrl, routeSet);
|
|
return normalized ? markdownLink(normalized) : stripTags(label);
|
|
});
|
|
output = output.replace(MARKDOWN_LINK, (_match, label, href) => {
|
|
const normalized = collectLink(collected, { href, label, external: true }, baseUrl, routeSet);
|
|
return normalized ? markdownLink(normalized) : label;
|
|
});
|
|
output = output.replace(METHODS, (_match, route) => {
|
|
const punctuation = route.match(/[.,;:!?]+$/)?.[0] || "";
|
|
const cleanRoute = punctuation ? route.slice(0, -punctuation.length) : route;
|
|
const normalized = collectLink(collected, { href: cleanRoute, label: routeLabel(cleanRoute) }, baseUrl, routeSet);
|
|
return normalized ? `${markdownLink(normalized)}${punctuation}` : route;
|
|
});
|
|
for (const link of Array.isArray(links) ? links : []) {
|
|
collectLink(collected, link, baseUrl, routeSet);
|
|
}
|
|
|
|
output = output
|
|
.replace(/<br\s*\/?>/gi, "\n")
|
|
.replace(/<\/p\s*>/gi, "\n\n")
|
|
.replace(/<[^>]*>/g, "");
|
|
if (role !== "admin") {
|
|
output = output.replace(REPO_PATH, "internal Lumi component").replace(SOURCE_DETAIL, "");
|
|
if (role === "user" || !allowModeratorCodeHelp) {
|
|
output = output.replace(/```[\s\S]*?```/g, "Technical implementation details are available to administrators.");
|
|
}
|
|
}
|
|
output = output.replace(/[ \t]+\n/g, "\n").replace(/\n{4,}/g, "\n\n\n").trim();
|
|
|
|
const originalLength = output.length;
|
|
const constrained = constrainFinal(output, maxLength);
|
|
const delivered = constrained.text;
|
|
return {
|
|
text: delivered,
|
|
links: uniqueLinks(collected),
|
|
original_final_length: originalLength,
|
|
delivered_length: delivered.length,
|
|
truncated: delivered.length < originalLength,
|
|
delivery_action: constrained.action
|
|
};
|
|
}
|
|
|
|
function collectLink(output, link, baseUrl, routeSet) {
|
|
const normalized = normalizeLink(link, baseUrl, routeSet);
|
|
if (normalized) output.push(normalized);
|
|
return normalized;
|
|
}
|
|
|
|
function truncateFinal(value, maxLength) {
|
|
return constrainFinal(value, maxLength).text;
|
|
}
|
|
|
|
function constrainFinal(value, maxLength) {
|
|
const output = String(value || "");
|
|
const requestedLimit = Number(maxLength);
|
|
if (!Number.isFinite(requestedLimit) || requestedLimit <= 0 || output.length <= requestedLimit) {
|
|
return { text: output, action: "none" };
|
|
}
|
|
const limit = Math.max(100, Number.parseInt(requestedLimit, 10) || 4000);
|
|
const suffix = " [Reply shortened for platform limits.]";
|
|
const available = Math.max(1, limit - suffix.length);
|
|
const candidate = output.slice(0, available);
|
|
const minimumBoundary = Math.floor(available * 0.55);
|
|
const sentenceBoundary = lastBoundary(candidate, /(?:[.!?][\])}"']?|\n\n)(?=\s|$)/g, minimumBoundary);
|
|
const lineBoundary = lastBoundary(candidate, /\n/g, minimumBoundary);
|
|
const wordBoundary = candidate.lastIndexOf(" ");
|
|
const boundary = sentenceBoundary >= minimumBoundary
|
|
? sentenceBoundary
|
|
: lineBoundary >= minimumBoundary
|
|
? lineBoundary
|
|
: wordBoundary >= minimumBoundary
|
|
? wordBoundary
|
|
: available;
|
|
return {
|
|
text: `${candidate.slice(0, boundary).trimEnd()}${suffix}`,
|
|
action: "compacted"
|
|
};
|
|
}
|
|
|
|
function lastBoundary(value, pattern, minimum) {
|
|
let boundary = -1;
|
|
for (const match of value.matchAll(pattern)) {
|
|
const end = (match.index || 0) + match[0].length;
|
|
if (end >= minimum) boundary = end;
|
|
}
|
|
return boundary;
|
|
}
|
|
|
|
function markdownLink(link) {
|
|
const label = String(link.label || "").replace(/[[\]]/g, "");
|
|
const href = String(link.href || "").replace(/[<>\s]/g, (character) => encodeURIComponent(character));
|
|
return `[${label}](${href})`;
|
|
}
|
|
|
|
function uniqueLinks(links) {
|
|
return [...new Map(links.map((link) => [link.href, link])).values()];
|
|
}
|
|
|
|
function normalizeCodeFences(value) {
|
|
return value.replace(/```(javascript|python)\s+([^\n][\s\S]*?)```/gi, (_match, language, code) =>
|
|
`\`\`\`${language.toLowerCase()}\n${code.trim()}\n\`\`\``);
|
|
}
|
|
|
|
module.exports = {
|
|
formatAssistantResponse,
|
|
normalizeLink,
|
|
truncateFinal,
|
|
constrainFinal,
|
|
absoluteInternalUrl,
|
|
normalizeCodeFences
|
|
};
|