Skip to content

Commit 523ba4d

Browse files
authored
Add conversation sentiment tool (#11)
Adds a tool to retrieve conversation sentiment
1 parent 179323e commit 523ba4d

File tree

8 files changed

+265
-41
lines changed

8 files changed

+265
-41
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ in the [tools doc](/docs/tools.md).
1616
| [Query Queue Volumes](/docs/tools.md#query-queue-volumes) | Retrieves conversation volumes and member count by Queue IDs |
1717
| [Sample Conversations By Queue](/docs/tools.md#sample-conversations-by-queue) | Retrieves a representative sample of Conversation IDs for a Queue ID |
1818
| [Voice Call Quality](/docs/tools.md#voice-call-quality) | Retrieves voice call quality metrics for one or more conversations by ID |
19+
| [Conversation Sentiment](/docs/tools.md#conversation-sentiment) | Retrieves the sentiment for one or more conversations by ID |
1920

2021
## Authentication
2122

docs/tools.md

Lines changed: 65 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,22 @@ or listing available queues.
1212

1313
### Inputs
1414

15-
* `name`
16-
* The name (or partial name) of the routing queue(s) to search for. Wildcards ('*') are supported for pattern matching (e.g., 'Support*', '*Emergency', '*Sales*'). Use '*' alone to retrieve all queues.
17-
* `pageNumber`
18-
* The page number of the results to retrieve, starting from 1. Defaults to 1 if not specified. Used with 'pageSize' for navigating large result sets.
19-
* `pageSize`
20-
* The maximum number of queues to return per page. Defaults to 100 if not specified. Used with 'pageNumber' for pagination. The maximum value is 500.
15+
- `name`
16+
- The name (or partial name) of the routing queue(s) to search for. Wildcards ('\*') are supported for pattern matching (e.g., 'Support\*', '\*Emergency', '\*Sales\*'). Use '\*' alone to retrieve all queues.
17+
- `pageNumber`
18+
- The page number of the results to retrieve, starting from 1. Defaults to 1 if not specified. Used with 'pageSize' for navigating large result sets.
19+
- `pageSize`
20+
- The maximum number of queues to return per page. Defaults to 100 if not specified. Used with 'pageNumber' for pagination. The maximum value is 500.
2121

2222
### Security
2323

2424
Required permission:
25-
* `routing:queue:view`
25+
26+
- `routing:queue:view`
2627

2728
Platform API endpoint used:
28-
* [`GET /api/v2/routing/queues`](https://developer.genesys.cloud/routing/routing/#get-api-v2-routing-queues)
29+
30+
- [`GET /api/v2/routing/queues`](https://developer.genesys.cloud/routing/routing/#get-api-v2-routing-queues)
2931

3032
## Query Queue Volumes
3133

@@ -36,22 +38,24 @@ dates. Useful for comparing workload across queues.
3638

3739
### Inputs
3840

39-
* `queueIds`
40-
* The IDs of the queues to filter conversations by. Max 300.
41-
* `startDate`
42-
* The start date/time in ISO-8601 format (e.g., '2024-01-01T00:00:00Z').
43-
* `endDate`
44-
* The end date/time in ISO-8601 format (e.g., '2024-01-07T23:59:59Z').
41+
- `queueIds`
42+
- The IDs of the queues to filter conversations by. Max 300.
43+
- `startDate`
44+
- The start date/time in ISO-8601 format (e.g., '2024-01-01T00:00:00Z').
45+
- `endDate`
46+
- The end date/time in ISO-8601 format (e.g., '2024-01-07T23:59:59Z').
4547

4648
### Security
4749

4850
Required permission:
49-
* `analytics:conversationDetail:view`
51+
52+
- `analytics:conversationDetail:view`
5053

5154
Platform API endpoints used:
52-
* [`POST /api/v2/analytics/conversations/details/jobs`](https://developer.genesys.cloud/analyticsdatamanagement/analytics/analytics-apis#post-api-v2-analytics-conversations-details-jobs)
53-
* [`GET /api/v2/analytics/conversations/details/jobs/{jobId}`](https://developer.genesys.cloud/analyticsdatamanagement/analytics/analytics-apis#get-api-v2-analytics-conversations-details-jobs--jobId-)
54-
* [`GET /api/v2/analytics/conversations/details/jobs/{jobId}/results`](https://developer.genesys.cloud/analyticsdatamanagement/analytics/analytics-apis#get-api-v2-analytics-conversations-details-jobs--jobId--results)
55+
56+
- [`POST /api/v2/analytics/conversations/details/jobs`](https://developer.genesys.cloud/analyticsdatamanagement/analytics/analytics-apis#post-api-v2-analytics-conversations-details-jobs)
57+
- [`GET /api/v2/analytics/conversations/details/jobs/{jobId}`](https://developer.genesys.cloud/analyticsdatamanagement/analytics/analytics-apis#get-api-v2-analytics-conversations-details-jobs--jobId-)
58+
- [`GET /api/v2/analytics/conversations/details/jobs/{jobId}/results`](https://developer.genesys.cloud/analyticsdatamanagement/analytics/analytics-apis#get-api-v2-analytics-conversations-details-jobs--jobId--results)
5559

5660
## Sample Conversations By Queue
5761

@@ -60,22 +64,24 @@ representative sample of conversation IDs. Useful for reporting, investigation,
6064

6165
### Inputs
6266

63-
* `queueId`
64-
* The ID of the queue to filter conversations by.
65-
* `startDate`
66-
* The start date/time in ISO-8601 format (e.g., '2024-01-01T00:00:00Z').
67-
* `endDate`
68-
* The end date/time in ISO-8601 format (e.g., '2024-01-07T23:59:59Z').
67+
- `queueId`
68+
- The ID of the queue to filter conversations by.
69+
- `startDate`
70+
- The start date/time in ISO-8601 format (e.g., '2024-01-01T00:00:00Z').
71+
- `endDate`
72+
- The end date/time in ISO-8601 format (e.g., '2024-01-07T23:59:59Z').
6973

7074
### Security
7175

72-
Required Permissions:
73-
* `analytics:conversationDetail:view`
76+
Required Permission:
77+
78+
- `analytics:conversationDetail:view`
7479

7580
Platform API endpoints used:
76-
* [`POST /api/v2/analytics/conversations/details/jobs`](https://developer.genesys.cloud/analyticsdatamanagement/analytics/analytics-apis#post-api-v2-analytics-conversations-details-jobs)
77-
* [`GET /api/v2/analytics/conversations/details/jobs/{jobId}`](https://developer.genesys.cloud/analyticsdatamanagement/analytics/analytics-apis#get-api-v2-analytics-conversations-details-jobs--jobId-)
78-
* [`GET /api/v2/analytics/conversations/details/jobs/{jobId}/results`](https://developer.genesys.cloud/analyticsdatamanagement/analytics/analytics-apis#get-api-v2-analytics-conversations-details-jobs--jobId--results)
81+
82+
- [`POST /api/v2/analytics/conversations/details/jobs`](https://developer.genesys.cloud/analyticsdatamanagement/analytics/analytics-apis#post-api-v2-analytics-conversations-details-jobs)
83+
- [`GET /api/v2/analytics/conversations/details/jobs/{jobId}`](https://developer.genesys.cloud/analyticsdatamanagement/analytics/analytics-apis#get-api-v2-analytics-conversations-details-jobs--jobId-)
84+
- [`GET /api/v2/analytics/conversations/details/jobs/{jobId}/results`](https://developer.genesys.cloud/analyticsdatamanagement/analytics/analytics-apis#get-api-v2-analytics-conversations-details-jobs--jobId--results)
7985

8086
## Voice Call Quality
8187

@@ -89,13 +95,39 @@ Read more [about MOS scores and how they're determined](https://developer.genesy
8995

9096
### Inputs
9197

92-
* `conversationIds`
93-
* A list of up to 100 conversation IDs to evaluate voice call quality for.
98+
- `conversationIds`
99+
- A list of up to 100 conversation IDs to evaluate voice call quality for.
100+
101+
### Security
102+
103+
Required Permission:
104+
105+
- `analytics:conversationDetail:view`
106+
107+
Platform API endpoint used:
108+
109+
- [`GET /api/v2/analytics/conversations/details`](https://developer.genesys.cloud/analyticsdatamanagement/analytics/analytics-apis#get-api-v2-analytics-conversations-details)
110+
111+
## Conversation Sentiment
112+
113+
Retrieves sentiment analysis scores for one or more conversations. Sentiment is evaluated based on customer phrases,
114+
categorized as positive, neutral, or negative. The result includes both a numeric sentiment score (-100 to 100)
115+
and an interpreted sentiment label.
116+
117+
[Source file](/src/tools/conversationSentiment.ts).
118+
119+
### Inputs
120+
121+
- `conversationIds`
122+
- A list of up to 100 conversation IDs to retrieve sentiment for.
94123

95124
### Security
96125

97126
Required Permissions:
98-
* `analytics:conversationDetail:view`
127+
128+
- `speechAndTextAnalytics:data:view`
129+
- `recording:recording:view`
99130

100131
Platform API endpoint used:
101-
* [`GET /api/v2/analytics/conversations/details`](https://developer.genesys.cloud/analyticsdatamanagement/analytics/analytics-apis#get-api-v2-analytics-conversations-details)
132+
133+
- [GET /api/v2/speechandtextanalytics/conversations/{conversationId}](https://developer.genesys.cloud/analyticsdatamanagement/speechtextanalytics/#get-api-v2-speechandtextanalytics-conversations--conversationId-)

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@makingchatbots/genesys-cloud-mcp-server",
3-
"version": "0.0.3",
3+
"version": "0.0.4",
44
"description": "A MCP server for connecting LLMs to Genesys Cloud's Platform API",
55
"exports": "./dist/index.js",
66
"type": "module",

src/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { loadConfig } from "./loadConfig.js";
77
import { sampleConversationsByQueue } from "./tools/sampleConversationsByQueue.js";
88
import { queryQueueVolumes } from "./tools/queryQueueVolumes.js";
99
import { voiceCallQuality } from "./tools/voiceCallQuality.js";
10+
import { conversationSentiment } from "./tools/conversationSentiment.js";
1011

1112
const configResult = loadConfig(process.env);
1213
if (!configResult.success) {
@@ -68,6 +69,7 @@ server.tool(
6869
platformClient.ApiClient.instance,
6970
),
7071
);
72+
7173
const voiceCallQualityTool = voiceCallQuality({
7274
analyticsApi: new platformClient.AnalyticsApi(),
7375
});
@@ -84,5 +86,21 @@ server.tool(
8486
),
8587
);
8688

89+
const conversationSentimentTool = conversationSentiment({
90+
speechTextAnalyticsApi: new platformClient.SpeechTextAnalyticsApi(),
91+
});
92+
server.tool(
93+
conversationSentimentTool.schema.name,
94+
conversationSentimentTool.schema.description,
95+
conversationSentimentTool.schema.paramsSchema.shape,
96+
config.mockingEnabled
97+
? conversationSentimentTool.mockCall
98+
: withAuth(
99+
conversationSentimentTool.call,
100+
config.genesysCloud,
101+
platformClient.ApiClient.instance,
102+
),
103+
);
104+
87105
const transport = new StdioServerTransport();
88106
await server.connect(transport);

src/tools/conversationSentiment.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { z } from "zod";
2+
import { createTool, type ToolFactory } from "./utils/createTool.js";
3+
import { isUnauthorisedError } from "./utils/genesys/isUnauthorisedError.js";
4+
import { type SpeechTextAnalyticsApi } from "purecloud-platform-client-v2";
5+
import { type CallToolResult } from "@modelcontextprotocol/sdk/types.js";
6+
7+
interface Dependencies {
8+
readonly speechTextAnalyticsApi: SpeechTextAnalyticsApi;
9+
}
10+
11+
const paramsSchema = z.object({
12+
conversationIds: z
13+
.array(
14+
z
15+
.string()
16+
.uuid()
17+
.describe(
18+
"A unique ID for a Genesys Cloud conversation. Must be a valid UUID.",
19+
),
20+
)
21+
.min(1)
22+
.max(100)
23+
.describe(
24+
"A list of up to 100 conversation IDs to retrieve sentiment for.",
25+
),
26+
});
27+
28+
function errorResult(errorMessage: string): CallToolResult {
29+
return {
30+
isError: true,
31+
content: [
32+
{
33+
type: "text",
34+
text: errorMessage,
35+
},
36+
],
37+
};
38+
}
39+
40+
function interpretSentiment(score?: number): string {
41+
if (score === undefined) return "Unknown";
42+
if (score > 55) return "Positive";
43+
if (score >= 20 && score <= 55) return "Slightly Positive";
44+
if (score > -20 && score < 20) return "Neutral";
45+
if (score >= -55 && score <= -20) return "Slightly Negative";
46+
return "Negative";
47+
}
48+
49+
export const conversationSentiment: ToolFactory<
50+
Dependencies,
51+
typeof paramsSchema
52+
> = ({ speechTextAnalyticsApi }) =>
53+
createTool({
54+
schema: {
55+
name: "conversation_sentiment",
56+
description:
57+
"Retrieves sentiment analysis scores for one or more conversations. Sentiment is evaluated based on customer phrases, categorized as positive, neutral, or negative. The result includes both a numeric sentiment score (-100 to 100) and an interpreted sentiment label.",
58+
paramsSchema,
59+
},
60+
call: async ({ conversationIds }) => {
61+
try {
62+
const conversations = await Promise.all(
63+
conversationIds.map((id) =>
64+
speechTextAnalyticsApi.getSpeechandtextanalyticsConversation(id),
65+
),
66+
);
67+
68+
const output: string[] = [];
69+
70+
for (const convo of conversations) {
71+
const id = convo.conversation?.id;
72+
const score = convo.sentimentScore;
73+
74+
if (id === undefined || score === undefined) continue;
75+
const scaledScore = Math.round(score * 100);
76+
77+
output.push(
78+
`• Conversation ID: ${id}\n • Sentiment Score: ${String(scaledScore)} (${interpretSentiment(scaledScore)})`,
79+
);
80+
}
81+
82+
return {
83+
content: [
84+
{
85+
type: "text",
86+
text:
87+
output.length > 0
88+
? [
89+
`Sentiment results for ${String(output.length)} conversation(s):`,
90+
...output,
91+
].join("\n\n")
92+
: "No sentiment data found for the given conversation IDs.",
93+
},
94+
],
95+
};
96+
} catch (error: unknown) {
97+
const message = isUnauthorisedError(error)
98+
? "Failed to retrieve sentiment analysis: Unauthorised access. Please check API credentials or permissions."
99+
: `Failed to retrieve sentiment analysis: ${error instanceof Error ? error.message : JSON.stringify(error)}`;
100+
101+
return errorResult(message);
102+
}
103+
},
104+
mockCall: async ({ conversationIds }) => {
105+
return Promise.resolve({
106+
content: [
107+
{
108+
type: "text",
109+
text: [
110+
`Sentiment results for ${String(conversationIds.length)} conversation(s):`,
111+
...conversationIds.map((id, index) => {
112+
const score = [-80, 0, 25, 55, 85][index % 5];
113+
return `• Conversation ID: ${id}\n • Sentiment Score: ${String(score)} (${interpretSentiment(score)})`;
114+
}),
115+
].join("\n\n"),
116+
},
117+
],
118+
});
119+
},
120+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3+
import { join } from "path";
4+
import { afterEach, describe, expect, test } from "vitest";
5+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
6+
7+
describe("conversation sentiment tool", () => {
8+
let client: Client | null = null;
9+
10+
afterEach(async () => {
11+
if (client) await client.close();
12+
});
13+
14+
test("mock", async () => {
15+
// Debugging server possible by setting breakpoint inside js file
16+
17+
const transport = new StdioClientTransport({
18+
command: "node",
19+
args: ["--inspect", join(__dirname, "../dist/index.js")],
20+
env: {
21+
// Provides path for node binary to be used in test
22+
PATH: process.env.PATH!,
23+
MOCKING_ENABLED: "true",
24+
},
25+
});
26+
27+
client = new Client({
28+
name: "test-client",
29+
version: "1.0.0",
30+
});
31+
32+
await client.connect(transport);
33+
34+
const queueName = "Test";
35+
const result = await client.callTool({
36+
name: "conversation_sentiment",
37+
arguments: {
38+
conversationIds: [
39+
"00000000-0000-0000-0000-000000000001",
40+
"00000000-0000-0000-0000-000000000002",
41+
],
42+
},
43+
});
44+
45+
const text = (result as CallToolResult).content[0].text;
46+
expect(text).toStrictEqual(
47+
`Sentiment results for 2 conversation(s):
48+
49+
• Conversation ID: 00000000-0000-0000-0000-000000000001
50+
• Sentiment Score: -80 (Negative)
51+
52+
• Conversation ID: 00000000-0000-0000-0000-000000000002
53+
• Sentiment Score: 0 (Neutral)`,
54+
);
55+
});
56+
});

0 commit comments

Comments
 (0)