UNPKG

@makingchatbots/genesys-cloud-mcp-server

Version:

A Model Context Protocol (MCP) server exposing Genesys Cloud tools for LLMs, including sentiment analysis, conversation search, topic detection and more.

206 lines (205 loc) 8.22 kB
import { z } from "zod"; import { isWithinInterval } from "date-fns/isWithinInterval"; import { getBorderCharacters, table } from "table"; import { createTool } from "../utils/createTool.js"; import { isUnauthorisedError } from "../utils/genesys/isUnauthorisedError.js"; import { errorResult } from "../utils/errorResult.js"; import { formatTimeUtteranceStarted } from "./formatTimeUtteranceStarted.js"; function waitSeconds(seconds) { return new Promise((resolve) => setTimeout(resolve, seconds * 1000)); } export function friendlyPurposeName(participantPurpose) { switch (participantPurpose?.toLowerCase()) { case "internal": return "Agent"; case "external": return "Customer"; case "acd": return "ACD"; case "ivr": return "IVR"; default: return participantPurpose ?? "Unknown"; } } function friendlySentiment(sentiment) { if (sentiment === 1) { return "Positive"; } if (sentiment === 0) { return "Neutral"; } if (sentiment === -1) { return "Negative"; } return ""; } function isNonHuman(participant) { if (!participant?.participantPurpose) { return false; } return ["acd", "ivr", "voicemail", "fax"].includes(participant.participantPurpose.toLowerCase()); } function isInternalParticipant(participant) { const purpose = participant.participantPurpose?.toLowerCase(); if (!purpose) { return false; } return (purpose === "user" || purpose === "agent" || purpose === "internal" || isNonHuman(participant)); } function isExternalParticipant(participant) { const purpose = participant.participantPurpose?.toLowerCase(); if (!purpose) { return false; } return purpose === "external" || purpose === "customer"; } const paramsSchema = z.object({ conversationId: z .string() .uuid() .describe("The UUID of the conversation to retrieve the transcript for (e.g., 00000000-0000-0000-0000-000000000000)"), }); export const conversationTranscription = ({ recordingApi, speechTextAnalyticsApi, fetchUrl }) => createTool({ schema: { name: "conversation_transcript", description: "Retrieves a structured transcript of the conversation, including speaker labels, utterance timestamps, and sentiment annotations where available. The transcript is formatted as a time-aligned list of utterances attributed to each participant (e.g., customer or agent)", paramsSchema, }, call: async ({ conversationId }) => { let recordingSessionIds = null; // 1. Unarchive recordings let retryCounter = 0; while (!recordingSessionIds) { let recordings; try { recordings = (await recordingApi.getConversationRecordings(conversationId)); } catch (error) { const message = isUnauthorisedError(error) ? "Failed to retrieve transcript: Unauthorised access. Please check API credentials or permissions." : `Failed to retrieve transcript: ${error instanceof Error ? error.message : JSON.stringify(error)}`; return errorResult(message); } if (recordings) { recordingSessionIds = recordings .filter((s) => s.sessionId) .map((s) => s.sessionId); } else { retryCounter++; if (retryCounter > 5) { return errorResult("Failed to retrieve transcript."); } await waitSeconds(10); } } // 2. Download recordings const transcriptionsForRecordings = []; for (const recordingSessionId of recordingSessionIds) { if (!recordingSessionId) { continue; } let transcriptUrl = null; try { transcriptUrl = await speechTextAnalyticsApi.getSpeechandtextanalyticsConversationCommunicationTranscripturl(conversationId, recordingSessionId); } catch (error) { const message = isUnauthorisedError(error) ? "Failed to retrieve transcript: Unauthorised access. Please check API credentials or permissions." : `Failed to retrieve transcript: ${error instanceof Error ? error.message : JSON.stringify(error)}`; return errorResult(message); } if (!transcriptUrl.url) { return errorResult("URL for transcript was not provided for conversation"); } else { const response = await fetchUrl(transcriptUrl.url); const transcript = (await response.json()); transcriptionsForRecordings.push(transcript); } } const utterances = []; for (const recording of transcriptionsForRecordings) { for (const transcript of recording.transcripts ?? []) { const transcriptUtterances = (transcript.phrases ?? []).flatMap((p) => { const participantDetails = recording.participants?.find((pd) => { if (p.participantPurpose !== "external" && isExternalParticipant(pd)) { return false; // Ignore } if (p.participantPurpose !== "internal" && isInternalParticipant(pd)) { return false; // Ignore } if (!p.startTimeMs || !pd.startTimeMs || !pd.endTimeMs) { return false; // Ignore } return isWithinInterval(p.startTimeMs, { start: pd.startTimeMs, end: pd.endTimeMs, }); }); const recordingTimes = recording.conversationStartTime && p.startTimeMs ? { conversationStartInMs: recording.conversationStartTime, utteranceStartInMs: p.startTimeMs, } : null; const sentiment = transcript.analytics?.sentiment?.find((s) => s.phraseIndex === p.phraseIndex); return { times: recordingTimes, sentiment: sentiment?.sentiment, utterance: p.decoratedText ?? p.text ?? "", speaker: friendlyPurposeName(participantDetails?.participantPurpose ?? p.participantPurpose), }; }); if (transcriptUtterances.length > 0) { utterances.push(...transcriptUtterances); } } } const sentimentPresent = utterances.some((u) => u.sentiment !== undefined); const data = [ [ "Time", "Who", ...(sentimentPresent ? ["Sentiment"] : []), "Utterance", ], ...utterances.map((u) => { return [ formatTimeUtteranceStarted(u), u.speaker, ...(sentimentPresent ? [friendlySentiment(u.sentiment)] : []), u.utterance, ]; }), ]; const utteranceTable = table(data, { border: getBorderCharacters("void"), columnDefault: { paddingLeft: 0, paddingRight: 2, }, drawHorizontalLine: () => false, }) .split("\n") .map((line) => line.trimEnd()) .join("\n") .trim(); return { content: [ { type: "text", text: utteranceTable, }, ], }; }, });