Skip to content

Commit 239343f

Browse files
authored
Add tool to get conversation's topics (#17)
1 parent d0f4fb9 commit 239343f

12 files changed

+394
-53
lines changed

docs/tools.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,32 @@ Required Permissions:
131131
Platform API endpoint used:
132132

133133
- [GET /api/v2/speechandtextanalytics/conversations/{conversationId}](https://developer.genesys.cloud/analyticsdatamanagement/speechtextanalytics/#get-api-v2-speechandtextanalytics-conversations--conversationId-)
134+
135+
## Conversation Topics
136+
137+
Retrieves Speech and Text Analytics topics detected for a specific conversation. Topics
138+
represent business-level intents (e.g. cancellation, billing enquiry) inferred from recognised
139+
phrases in the customer-agent interaction.
140+
141+
Read more [about programs, topics, and phrases](https://help.mypurecloud.com/articles/about-programs-topics-and-phrases/).
142+
143+
[Source file](/src/tools/conversationTopics.ts).
144+
145+
### Input
146+
147+
- `conversationId`
148+
- A UUID ID for a conversation. (e.g., 00000000-0000-0000-0000-000000000000)
149+
150+
### Security
151+
152+
Required Permissions:
153+
154+
- `speechAndTextAnalytics:topic:view`
155+
- `analytics:conversationDetail:view`
156+
- `analytics:speechAndTextAnalyticsAggregates:view`
157+
158+
Platform API endpoints used:
159+
160+
- [GET /api/v2/speechandtextanalytics/topics](https://developer.genesys.cloud/analyticsdatamanagement/speechtextanalytics/#get-api-v2-speechandtextanalytics-topics)
161+
- [GET /api/v2/analytics/conversations/{conversationId}/details](https://developer.genesys.cloud/analyticsdatamanagement/analytics/analytics-apis#get-api-v2-analytics-conversations--conversationId--details)
162+
- [POST /api/v2/analytics/transcripts/aggregates/query](https://developer.genesys.cloud/analyticsdatamanagement/analytics/analytics-apis#post-api-v2-analytics-transcripts-aggregates-query)

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.6",
3+
"version": "0.0.7",
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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { sampleConversationsByQueue } from "./tools/sampleConversationsByQueue.j
88
import { queryQueueVolumes } from "./tools/queryQueueVolumes.js";
99
import { voiceCallQuality } from "./tools/voiceCallQuality.js";
1010
import { conversationSentiment } from "./tools/conversationSentiment.js";
11+
import { conversationTopics } from "./tools/conversationTopics.js";
1112

1213
const configResult = loadConfig(process.env);
1314
if (!configResult.success) {
@@ -90,6 +91,21 @@ server.tool(
9091
),
9192
);
9293

94+
const conversationTopicsTool = conversationTopics({
95+
speechTextAnalyticsApi,
96+
analyticsApi,
97+
});
98+
server.tool(
99+
conversationTopicsTool.schema.name,
100+
conversationTopicsTool.schema.description,
101+
conversationTopicsTool.schema.paramsSchema.shape,
102+
withAuth(
103+
conversationTopicsTool.call,
104+
config.genesysCloud,
105+
platformClient.ApiClient.instance,
106+
),
107+
);
108+
93109
const transport = new StdioServerTransport();
94110
await server.connect(transport);
95111
console.log("Started...");

src/tools/conversationSentiment.test.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
88
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
99
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
1010
import { randomUUID } from "node:crypto";
11+
import { McpError } from "@modelcontextprotocol/sdk/types.js";
1112

1213
describe("Conversation Sentiment Tool", () => {
1314
let toolDeps: MockedObjectDeep<ToolDependencies>;
@@ -80,13 +81,12 @@ describe("Conversation Sentiment Tool", () => {
8081
conversationIds: [],
8182
},
8283
}),
83-
).rejects.toMatchObject({
84-
name: "McpError",
85-
code: -32602,
86-
message: expect.stringContaining(
87-
"Array must contain at least 1 element(s)",
88-
) as string,
89-
});
84+
).rejects.toSatisfy(
85+
(error: McpError) =>
86+
error.name === "McpError" &&
87+
error.message.includes("conversationId") &&
88+
error.message.includes("Array must contain at least 1 element(s)"),
89+
);
9090
});
9191

9292
test("errors when conversation ID not UUID", async () => {
@@ -97,11 +97,12 @@ describe("Conversation Sentiment Tool", () => {
9797
conversationIds: ["invalid-uuid"],
9898
},
9999
}),
100-
).rejects.toMatchObject({
101-
name: "McpError",
102-
code: -32602,
103-
message: expect.stringContaining("Invalid uuid") as string,
104-
});
100+
).rejects.toSatisfy(
101+
(error: McpError) =>
102+
error.name === "McpError" &&
103+
error.message.includes("conversationIds") &&
104+
error.message.includes("Invalid uuid"),
105+
);
105106
});
106107

107108
test("error from Genesys Cloud's Platform SDK returned", async () => {

src/tools/conversationSentiment.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
SpeechTextAnalyticsApi,
66
Models,
77
} from "purecloud-platform-client-v2";
8-
import { type CallToolResult } from "@modelcontextprotocol/sdk/types.js";
8+
import { errorResult } from "./utils/errorResult.js";
99

1010
export interface ToolDependencies {
1111
readonly speechTextAnalyticsApi: Pick<
@@ -29,18 +29,6 @@ const paramsSchema = z.object({
2929
.describe("A list of up to 100 conversation IDs to retrieve sentiment for"),
3030
});
3131

32-
function errorResult(errorMessage: string): CallToolResult {
33-
return {
34-
isError: true,
35-
content: [
36-
{
37-
type: "text",
38-
text: errorMessage,
39-
},
40-
],
41-
};
42-
}
43-
4432
function interpretSentiment(score?: number): string {
4533
if (score === undefined) return "Unknown";
4634
if (score > 55) return "Positive";
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { beforeEach, describe, expect, test, vi } from "vitest";
2+
import { conversationTopics, ToolDependencies } from "./conversationTopics.js";
3+
import { MockedObjectDeep } from "@vitest/spy";
4+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
7+
import { randomUUID } from "node:crypto";
8+
import { McpError } from "@modelcontextprotocol/sdk/types.js";
9+
10+
describe("Conversation Topics Tool", () => {
11+
let toolDeps: MockedObjectDeep<ToolDependencies>;
12+
let client: Client;
13+
let toolName: string;
14+
15+
beforeEach(async () => {
16+
toolDeps = {
17+
speechTextAnalyticsApi: {
18+
getSpeechandtextanalyticsTopics: vi.fn(),
19+
},
20+
analyticsApi: {
21+
getAnalyticsConversationDetails: vi.fn(),
22+
postAnalyticsTranscriptsAggregatesQuery: vi.fn(),
23+
},
24+
};
25+
26+
const toolDefinition = conversationTopics(toolDeps);
27+
toolName = toolDefinition.schema.name;
28+
29+
const server = new McpServer({ name: "TestServer", version: "test" });
30+
server.tool(
31+
toolDefinition.schema.name,
32+
toolDefinition.schema.description,
33+
toolDefinition.schema.paramsSchema.shape,
34+
toolDefinition.call,
35+
);
36+
37+
const [serverTransport, clientTransport] =
38+
InMemoryTransport.createLinkedPair();
39+
40+
await server.connect(serverTransport);
41+
42+
client = new Client({ name: "test-client", version: "1.0.0" });
43+
await client.connect(clientTransport);
44+
});
45+
46+
test("schema describes tool", async () => {
47+
const tools = await client.listTools();
48+
expect(tools.tools[0]).toStrictEqual({
49+
name: "conversation_topics",
50+
description:
51+
"Retrieves Speech and Text Analytics topics detected for a specific conversation. Topics represent business-level intents (e.g. cancellation, billing enquiry) inferred from recognised phrases in the customer-agent interaction.",
52+
inputSchema: {
53+
type: "object",
54+
properties: {
55+
conversationId: {
56+
description:
57+
"A UUID ID for a conversation. (e.g., 00000000-0000-0000-0000-000000000000)",
58+
format: "uuid",
59+
type: "string",
60+
},
61+
},
62+
required: ["conversationId"],
63+
additionalProperties: false,
64+
$schema: "http://json-schema.org/draft-07/schema#",
65+
},
66+
annotations: undefined,
67+
});
68+
});
69+
70+
test("errors when no conversation ID provided", async () => {
71+
await expect(
72+
client.callTool({
73+
name: toolName,
74+
arguments: {
75+
conversationId: "",
76+
},
77+
}),
78+
).rejects.toSatisfy(
79+
(error: McpError) =>
80+
error.name === "McpError" &&
81+
error.message.includes("conversationId") &&
82+
error.message.includes("Invalid uuid"),
83+
);
84+
});
85+
86+
test("sentiment returned for single conversation", async () => {
87+
const conversationId = randomUUID();
88+
89+
toolDeps.analyticsApi.getAnalyticsConversationDetails.mockResolvedValue({
90+
conversationStart: "2025-05-19T20:00:07.395Z",
91+
conversationEnd: "2025-05-19T21:00:52.686Z",
92+
});
93+
toolDeps.analyticsApi.postAnalyticsTranscriptsAggregatesQuery.mockResolvedValue(
94+
{
95+
results: [
96+
{ group: { topicId: "test-topic-id-1" } },
97+
{ group: { topicId: "test-topic-id-2" } },
98+
],
99+
},
100+
);
101+
toolDeps.speechTextAnalyticsApi.getSpeechandtextanalyticsTopics.mockResolvedValue(
102+
{
103+
entities: [
104+
{
105+
name: "Test Topic 1",
106+
description: "Test Topic 1 Desc",
107+
},
108+
{
109+
name: "Test Topic 2",
110+
description: "Test Topic 2 Desc",
111+
},
112+
],
113+
},
114+
);
115+
116+
const result = await client.callTool({
117+
name: toolName,
118+
arguments: {
119+
conversationId: conversationId,
120+
},
121+
});
122+
123+
expect(result).toStrictEqual({
124+
content: [
125+
{
126+
type: "text",
127+
text: `
128+
Conversation ID: ${conversationId}
129+
Detected Topics:
130+
• Test Topic 1: Test Topic 1 Desc
131+
• Test Topic 2: Test Topic 2 Desc
132+
`.trim(),
133+
},
134+
],
135+
});
136+
});
137+
});

0 commit comments

Comments
 (0)