sfdx-hardis
Version:
Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards
373 lines • 13.4 kB
JavaScript
import { SfError } from "@salesforce/core";
import { dataCloudSqlQuery } from "./dataCloudUtils.js";
const DEFAULT_EXCLUDED_CONVERSATION_IDS = [
"8981bfeb-d3e8-4085-b515-d26e54fdff88",
"df15617e-5a56-46b4-aed6-22c81efb4ccd",
];
const DEFAULT_EXCLUDED_SESSION_IDS = [
"019a6d20-ba17-7b97-a658-b01176754b78",
"019a6e05-fa89-7a0d-9b1e-46290e13c3e2",
"019b1365-3f29-7347-866d-16a9355aa32b",
];
const PLACEHOLDER_VALUES = new Set(["NOT_SET", "UNKNOWN", "UNDEFINED", "NULL", "NONE", "N/A"]);
export function normalizeKeys(record) {
return Object.entries(record).reduce((acc, [key, value]) => {
acc[key.toLowerCase()] = value;
return acc;
}, {});
}
export function sanitizeUnicodeString(input) {
if (!input) {
return "";
}
let needsCleanup = false;
for (let i = 0; i < input.length; i += 1) {
const code = input.charCodeAt(i);
if (code >= 0xd800 && code <= 0xdfff) {
needsCleanup = true;
break;
}
}
if (!needsCleanup) {
return input;
}
const buffer = [];
for (let i = 0; i < input.length; i += 1) {
const code = input.charCodeAt(i);
if (code >= 0xd800 && code <= 0xdbff) {
const next = input.charCodeAt(i + 1);
if (next >= 0xdc00 && next <= 0xdfff) {
buffer.push(code, next);
i += 1;
continue;
}
continue;
}
if (code >= 0xdc00 && code <= 0xdfff) {
continue;
}
buffer.push(code);
}
if (!buffer.length) {
return "";
}
let result = "";
const CHUNK_SIZE = 60000;
for (let i = 0; i < buffer.length; i += CHUNK_SIZE) {
result += String.fromCharCode(...buffer.slice(i, i + CHUNK_SIZE));
}
return result;
}
export function stringValue(value) {
if (value === null || value === undefined) {
return "";
}
if (typeof value === "string") {
return sanitizeUnicodeString(value);
}
if (value instanceof Date) {
return value.toISOString();
}
return sanitizeUnicodeString(String(value));
}
export function buildConversation(userUtterance, agentResponse) {
const parts = [];
if (userUtterance) {
parts.push(`USER: ${userUtterance}`);
}
if (agentResponse) {
parts.push(`AGENT: ${agentResponse}`);
}
return parts.join("\n---\n");
}
export function buildConversationUrl(params) {
const { domain, conversationId, sessionId, agentApiName, timeFilterDays } = params;
if (!domain || !conversationId || !sessionId) {
return "";
}
const normalizedDomain = normalizeLightningDomain(domain);
if (!normalizedDomain) {
return "";
}
const baseUrl = `${normalizedDomain}/lightning/cmp/runtime_analytics_evf_aie__record`;
let url;
try {
url = new URL(baseUrl);
}
catch {
return "";
}
const safeTimeFilter = Number.isFinite(timeFilterDays) && timeFilterDays > 0 ? timeFilterDays : 30;
url.searchParams.set("c__id", conversationId);
url.searchParams.set("c__type", "2");
url.searchParams.set("c__sessionId", sessionId);
url.searchParams.set("c__timeFilter", String(safeTimeFilter));
if (agentApiName) {
url.searchParams.set("c__agentApiName", agentApiName);
}
return url.toString();
}
export function resolveConversationLinkDomain(instanceUrl) {
if (!instanceUrl) {
return null;
}
try {
const parsed = new URL(instanceUrl);
const { protocol } = parsed;
let host = parsed.hostname;
if (host.endsWith(".my.salesforce.com")) {
host = host.replace(".my.salesforce.com", ".lightning.force.com");
}
else if (host.endsWith(".salesforce.com") && !host.includes(".lightning.")) {
host = host.replace(".salesforce.com", ".lightning.force.com");
}
const potentialDomain = `${protocol}//${host}`;
const normalized = normalizeLightningDomain(potentialDomain);
if (normalized) {
return normalized;
}
return normalizeLightningDomain(parsed.origin);
}
catch {
return null;
}
}
export function normalizeLightningDomain(domain) {
const trimmed = domain.trim();
if (!trimmed) {
return null;
}
const withoutTrailingSlash = trimmed.replace(/\/+$/, "");
const withProtocol = /^https?:\/\//i.test(withoutTrailingSlash) ? withoutTrailingSlash : `https://${withoutTrailingSlash}`;
try {
const parsed = new URL(withProtocol);
return parsed.origin;
}
catch {
return null;
}
}
export function extractSessionIds(records, options) {
const safeRecords = Array.isArray(records) ? records : [];
const sessionIdSet = new Set();
const sanitizer = options?.sanitizer;
safeRecords.forEach((record) => {
if (!record || typeof record !== "object") {
return;
}
const normalized = normalizeKeys(record);
const rawSessionId = stringValue(normalized["sessionid"] ?? normalized["ssot__aiagentsessionid__c"]);
const finalSessionId = sanitizer ? sanitizer(rawSessionId) : rawSessionId;
if (finalSessionId) {
sessionIdSet.add(finalSessionId);
}
});
return Array.from(sessionIdSet);
}
export async function fetchConversationTranscripts(sessionIds, conn, options) {
const transcripts = new Map();
const uniqueIds = Array.from(new Set(sessionIds.filter((id) => Boolean(id))));
if (!uniqueIds.length) {
return transcripts;
}
const chunkSize = options?.chunkSize && options.chunkSize > 0 ? options.chunkSize : 50;
const transcriptBuckets = new Map();
const chunks = chunkArray(uniqueIds, chunkSize);
for (const chunk of chunks) {
if (!chunk.length) {
continue;
}
const quotedIds = chunk.map((id) => `'${id.replace(/'/g, "''")}'`).join(", ");
const transcriptQuery = `
SELECT
ai.ssot__AiAgentSessionId__c AS sessionId,
msg.ssot__MessageSentTimestamp__c AS messageTimestamp,
msg.ssot__AiAgentInteractionMessageType__c AS messageType,
msg.ssot__ContentText__c AS messageText,
part.ssot__AiAgentSessionParticipantRole__c AS participantRole
FROM ssot__AiAgentInteractionMessage__dlm msg
JOIN ssot__AiAgentInteraction__dlm ai ON ai.ssot__Id__c = msg.ssot__AiAgentInteractionId__c
LEFT JOIN ssot__AiAgentSessionParticipant__dlm part ON part.ssot__Id__c = msg.ssot__AiAgentSessionParticipantId__c
WHERE ai.ssot__AiAgentSessionId__c IN (${quotedIds})
ORDER BY ai.ssot__AiAgentSessionId__c, msg.ssot__MessageSentTimestamp__c
`;
const transcriptResult = await dataCloudSqlQuery(transcriptQuery.trim(), conn, {});
const rows = Array.isArray(transcriptResult.records) ? transcriptResult.records : [];
rows.forEach((row) => {
const normalized = normalizeKeys(row);
const rawSessionId = stringValue(normalized["sessionid"] ?? normalized["ssot__aiagentsessionid__c"]);
const sessionId = options?.sanitizeSessionId ? options.sanitizeSessionId(rawSessionId) : rawSessionId;
if (!sessionId) {
return;
}
const messageType = stringValue(normalized["messagetype"] ?? normalized["ssot__aiagentinteractionmessagetype__c"]);
const messageText = stringValue(normalized["messagetext"] ?? normalized["ssot__contenttext__c"]);
const timestamp = stringValue(normalized["messagetimestamp"] ?? normalized["ssot__messagesenttimestamp__c"]);
const participantRole = stringValue(normalized["participantrole"] ?? normalized["ssot__aiagentsessionparticipantrole__c"]);
const bucket = transcriptBuckets.get(sessionId) || [];
bucket.push({
speaker: inferSpeaker(participantRole, messageType),
text: messageText,
timestamp,
});
transcriptBuckets.set(sessionId, bucket);
});
}
transcriptBuckets.forEach((messages, sessionId) => {
const formatted = messages
.sort((a, b) => (a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0))
.map((message) => `${message.speaker}: ${message.text}`)
.join("\n---\n");
transcripts.set(sessionId, formatted);
});
return transcripts;
}
export function chunkArray(items, size) {
if (size <= 0) {
return [items];
}
const chunks = [];
for (let i = 0; i < items.length; i += size) {
chunks.push(items.slice(i, i + size));
}
return chunks;
}
export function inferSpeaker(participantRole, messageType) {
const normalizedRole = (participantRole || "").trim().toUpperCase();
if (normalizedRole.includes("AGENT") || normalizedRole === "BOT" || normalizedRole === "SYSTEM") {
return "AGENT";
}
if (normalizedRole) {
return "USER";
}
const normalizedType = (messageType || "").trim().toUpperCase();
if (normalizedType.includes("USER")) {
return "USER";
}
return "AGENT";
}
export function resolveDateFilterOptions(inputs) {
const { dateFromInput, dateToInput, lastNDaysInput } = inputs;
const parsedFrom = parseDateInput(dateFromInput, "--date-from");
const parsedTo = parseDateInput(dateToInput, "--date-to");
let effectiveFrom = parsedFrom;
let effectiveTo = parsedTo;
if (lastNDaysInput !== undefined && lastNDaysInput !== null) {
if (!Number.isFinite(lastNDaysInput) || lastNDaysInput <= 0) {
throw new SfError("--last-n-days must be a positive integer");
}
const now = new Date();
const relativeFrom = new Date(now);
relativeFrom.setUTCDate(relativeFrom.getUTCDate() - lastNDaysInput);
effectiveFrom = chooseLaterDate(effectiveFrom, relativeFrom);
effectiveTo = chooseEarlierDate(effectiveTo, now);
}
if (effectiveFrom && effectiveTo && effectiveFrom.getTime() > effectiveTo.getTime()) {
throw new SfError("Date filters are inconsistent: start date is after end date");
}
const filters = {};
if (effectiveFrom) {
filters.dateFrom = effectiveFrom.toISOString();
}
if (effectiveTo) {
filters.dateTo = effectiveTo.toISOString();
}
return filters;
}
export function parseDateInput(value, flagName) {
if (!value) {
return null;
}
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
throw new SfError(`Invalid ${flagName} value. Use ISO-8601 format, e.g. 2024-08-01T00:00:00Z`);
}
return parsed;
}
export function chooseLaterDate(first, second) {
if (!first) {
return second;
}
if (!second) {
return first;
}
return first.getTime() >= second.getTime() ? first : second;
}
export function chooseEarlierDate(first, second) {
if (!first) {
return second;
}
if (!second) {
return first;
}
return first.getTime() <= second.getTime() ? first : second;
}
export function buildDateFilterClause(filters) {
const clauses = [];
if (filters.dateFrom) {
clauses.push(` AND ggn.timestamp__c >= TIMESTAMP '${escapeSqlLiteral(filters.dateFrom)}'`);
}
if (filters.dateTo) {
clauses.push(` AND ggn.timestamp__c <= TIMESTAMP '${escapeSqlLiteral(filters.dateTo)}'`);
}
return clauses.join("");
}
export function escapeSqlLiteral(value) {
return value.replace(/'/g, "''");
}
export function buildExcludedSessionFilter() {
const excludedSessionIds = resolveExcludedSessionIds();
if (!excludedSessionIds.length) {
return "";
}
return `
WHERE (sess.sessionId IS NULL OR sess.sessionId NOT IN (${excludedSessionIds.map((id) => `'${id}'`).join(", ")}))`;
}
export function resolveExcludedConversationIds() {
const envValue = process.env.AGENTFORCE_FEEDBACK_EXCLUDED_CONV_IDS;
if (envValue) {
return envValue.split(",").map((id) => id.trim()).filter((id) => id);
}
return [...DEFAULT_EXCLUDED_CONVERSATION_IDS];
}
export function resolveExcludedSessionIds() {
const envValue = process.env.AGENTFORCE_EXCLUDED_SESSION_IDS;
if (envValue) {
return envValue.split(",").map((id) => id.trim()).filter((id) => id);
}
return [...DEFAULT_EXCLUDED_SESSION_IDS];
}
export function sanitizePlaceholderValue(value) {
const trimmed = value ? value.trim() : "";
if (!trimmed) {
return "";
}
if (PLACEHOLDER_VALUES.has(trimmed.toUpperCase())) {
return "";
}
return trimmed;
}
export function pickFirstMeaningfulValue(values) {
for (const value of values) {
const cleaned = sanitizePlaceholderValue(value);
if (cleaned) {
return cleaned;
}
}
return "";
}
export function extractSpeakerSegment(transcript, speaker) {
if (!transcript) {
return "";
}
const prefix = `${speaker}:`;
const segments = transcript.split(/\n---\n/);
for (const segment of segments) {
const trimmed = segment.trim();
if (trimmed.toUpperCase().startsWith(prefix)) {
return sanitizePlaceholderValue(trimmed.slice(prefix.length).trim());
}
}
return "";
}
//# sourceMappingURL=agentforceQueryUtils.js.map