conversation_topics
Retrieves business-level intents like cancellation or billing detected from speech and text analytics for a specific conversation.
Instructions
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.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| conversationId | Yes | A UUID for a conversation. (e.g., 00000000-0000-0000-0000-000000000000) |
Implementation Reference
- The main handler function for the conversation_topics tool. It receives a conversationId, fetches conversation details via analyticsApi, queries transcript aggregates to find topic IDs, then retrieves topic names from speechTextAnalyticsApi, returning them as JSON.
export const conversationTopics: ToolFactory< ToolDependencies, typeof paramsSchema > = ({ speechTextAnalyticsApi, analyticsApi }) => createTool({ schema: { name: "conversation_topics", annotations: { title: "Conversation Topics" }, description: "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.", paramsSchema, }, call: async ({ conversationId }) => { let conversationDetails: Models.AnalyticsConversationWithoutAttributes; try { conversationDetails = await analyticsApi.getAnalyticsConversationDetails(conversationId); } catch (error: unknown) { const errorMessage = isUnauthorisedError(error) ? "Failed to retrieve conversation topics: Unauthorised access. Please check API credentials or permissions" : `Failed to retrieve conversation topics: ${error instanceof Error ? error.message : JSON.stringify(error)}`; return errorResult(errorMessage); } if ( !conversationDetails.conversationStart || !conversationDetails.conversationEnd ) { return errorResult( "Unable to find conversation Start and End date needed for retrieving topics", ); } // Widen the time range either side to ensure the conversation timeframe is enclosed. // Conversation not returned if either only partially covered by interval, or matched exactly. const startDate = new Date(conversationDetails.conversationStart); startDate.setMinutes(startDate.getMinutes() - 10); const endDate = new Date(conversationDetails.conversationEnd); endDate.setMinutes(endDate.getMinutes() + 10); let jobDetails: Models.TranscriptAggregateQueryResponse; try { jobDetails = await analyticsApi.postAnalyticsTranscriptsAggregatesQuery( { interval: `${startDate.toISOString()}/${endDate.toISOString()}`, filter: { type: "and", predicates: [ { dimension: "conversationId", value: conversationId, }, { dimension: "resultsBy", value: "communication", }, ], }, groupBy: ["topicId"], metrics: ["nTopicCommunications"], }, ); } catch (error: unknown) { const errorMessage = isUnauthorisedError(error) ? "Failed to retrieve conversation topics: Unauthorised access. Please check API credentials or permissions" : `Failed to retrieve conversation topics: ${error instanceof Error ? error.message : JSON.stringify(error)}`; return errorResult(errorMessage); } const topicIds = new Set<string>(); for (const result of jobDetails.results ?? []) { if (result.group?.topicId) { topicIds.add(result.group.topicId); } } if (topicIds.size === 0) { return { content: [ { type: "text", text: `Conversation ID: ${conversationId}\nNo detected topics for this conversation.`, }, ], }; } const topics: Models.ListedTopic[] = []; try { for (const topicIdChunk of chunks( Array.from(topicIds.values()), MAX_IDS_ALLOWED_BY_API, )) { const topicsListings = await speechTextAnalyticsApi.getSpeechandtextanalyticsTopics({ ids: topicIdChunk, pageSize: MAX_IDS_ALLOWED_BY_API, }); topics.push(...(topicsListings.entities ?? [])); } } catch (error: unknown) { const errorMessage = isUnauthorisedError(error) ? "Failed to retrieve conversation topics: Unauthorised access. Please check API credentials or permissions" : `Failed to retrieve conversation topics: ${error instanceof Error ? error.message : JSON.stringify(error)}`; return errorResult(errorMessage); } const topicNames = topics .filter((topic) => topic.name && topic.description) .map(({ name, description }) => ({ name: name ?? "", description: description ?? "", })); return { content: [ { type: "text", text: JSON.stringify({ conversationId: conversationId, detectedTopics: topicNames, }), }, ], }; }, }); - Zod schema for input validation: requires a single 'conversationId' parameter as a UUID string.
const paramsSchema = z.object({ conversationId: z .string() .uuid() .describe( "A UUID for a conversation. (e.g., 00000000-0000-0000-0000-000000000000)", ), }); - src/index.ts:107-119 (registration)Registration of the conversation_topics tool on the MCP server. Creates the tool with dependencies (speechTextAnalyticsApi, analyticsApi), registers it by name, and wraps the call with OAuth authentication.
const conversationTopicsTool = conversationTopics({ speechTextAnalyticsApi, analyticsApi, }); server.registerTool( conversationTopicsTool.schema.name, { description: conversationTopicsTool.schema.description, inputSchema: conversationTopicsTool.schema.paramsSchema.shape, annotations: conversationTopicsTool.schema.annotations, }, withAuth(conversationTopicsTool.call), ); - Generator function that splits an array into chunks of a specified size, used to batch topic ID requests respecting the API limit of 50 IDs per call.
export function* chunks<T>(arr: T[], n: number): Generator<T[], void> { if (!Number.isInteger(n) || n <= 0) { throw new Error("Chunk size must be a positive integer"); } for (let i = 0; i < arr.length; i += n) { yield arr.slice(i, i + n); } }