@tpmjs/feedback-themes 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2025 TPMJS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,50 @@
1
+ import * as ai from 'ai';
2
+
3
+ /**
4
+ * Feedback Themes Extraction Tool for TPMJS
5
+ * Extracts themes and sentiment from customer feedback
6
+ *
7
+ * This is a proper AI SDK v6 tool that can be used with streamText()
8
+ * Uses jsonSchema() to avoid Zod 4 JSON Schema conversion issues with OpenAI
9
+ */
10
+ interface Theme {
11
+ name: string;
12
+ frequency: number;
13
+ sentiment: 'positive' | 'negative' | 'neutral' | 'mixed';
14
+ sentimentScore: number;
15
+ examples: string[];
16
+ }
17
+ interface FeedbackThemes {
18
+ themes: Theme[];
19
+ overallSentiment: 'positive' | 'negative' | 'neutral' | 'mixed';
20
+ totalFeedback: number;
21
+ summary: {
22
+ positiveCount: number;
23
+ negativeCount: number;
24
+ neutralCount: number;
25
+ };
26
+ }
27
+ /**
28
+ * Input type for Feedback Themes Tool
29
+ */
30
+ type FeedbackThemesInput = {
31
+ feedback: string[];
32
+ };
33
+ /**
34
+ * Feedback Themes Tool
35
+ * Extracts themes and sentiment from customer feedback
36
+ *
37
+ * This is a proper AI SDK v6 tool that can be used with streamText()
38
+ */
39
+ declare const feedbackThemesTool: ai.Tool<FeedbackThemesInput, {
40
+ themes: Theme[];
41
+ overallSentiment: "positive" | "negative" | "neutral" | "mixed";
42
+ totalFeedback: number;
43
+ summary: {
44
+ positiveCount: number;
45
+ negativeCount: number;
46
+ neutralCount: number;
47
+ };
48
+ }>;
49
+
50
+ export { type FeedbackThemes, type Theme, feedbackThemesTool as default, feedbackThemesTool };
package/dist/index.js ADDED
@@ -0,0 +1,183 @@
1
+ import { tool, jsonSchema } from 'ai';
2
+
3
+ // src/index.ts
4
+ function analyzeSentiment(text) {
5
+ const lowerText = text.toLowerCase();
6
+ const positiveKeywords = [
7
+ "great",
8
+ "excellent",
9
+ "amazing",
10
+ "fantastic",
11
+ "love",
12
+ "perfect",
13
+ "wonderful",
14
+ "awesome",
15
+ "best",
16
+ "good",
17
+ "helpful",
18
+ "easy",
19
+ "fast",
20
+ "impressed",
21
+ "thank",
22
+ "appreciate",
23
+ "satisfied"
24
+ ];
25
+ const negativeKeywords = [
26
+ "bad",
27
+ "terrible",
28
+ "awful",
29
+ "horrible",
30
+ "worst",
31
+ "hate",
32
+ "disappointing",
33
+ "poor",
34
+ "slow",
35
+ "difficult",
36
+ "confusing",
37
+ "frustrated",
38
+ "bug",
39
+ "broken",
40
+ "issue",
41
+ "problem",
42
+ "error",
43
+ "crash",
44
+ "fail"
45
+ ];
46
+ let positiveScore = 0;
47
+ let negativeScore = 0;
48
+ for (const keyword of positiveKeywords) {
49
+ if (lowerText.includes(keyword)) {
50
+ positiveScore++;
51
+ }
52
+ }
53
+ for (const keyword of negativeKeywords) {
54
+ if (lowerText.includes(keyword)) {
55
+ negativeScore++;
56
+ }
57
+ }
58
+ const totalScore = positiveScore - negativeScore;
59
+ const normalizedScore = Math.max(-1, Math.min(1, totalScore / 3));
60
+ if (normalizedScore > 0.2) {
61
+ return { sentiment: "positive", score: normalizedScore };
62
+ }
63
+ if (normalizedScore < -0.2) {
64
+ return { sentiment: "negative", score: normalizedScore };
65
+ }
66
+ return { sentiment: "neutral", score: normalizedScore };
67
+ }
68
+ function extractThemes(feedbackList) {
69
+ const themeMap = /* @__PURE__ */ new Map();
70
+ const themeKeywords = [
71
+ { name: "Performance", keywords: ["slow", "fast", "speed", "performance", "lag", "quick"] },
72
+ { name: "User Interface", keywords: ["ui", "interface", "design", "layout", "look", "visual"] },
73
+ {
74
+ name: "Ease of Use",
75
+ keywords: ["easy", "difficult", "simple", "complex", "intuitive", "confusing"]
76
+ },
77
+ { name: "Features", keywords: ["feature", "functionality", "capability", "option", "tool"] },
78
+ {
79
+ name: "Support",
80
+ keywords: ["support", "help", "customer service", "response", "assistance"]
81
+ },
82
+ { name: "Bugs", keywords: ["bug", "error", "crash", "broken", "issue", "problem"] },
83
+ { name: "Documentation", keywords: ["documentation", "docs", "guide", "tutorial", "help"] },
84
+ { name: "Pricing", keywords: ["price", "cost", "expensive", "cheap", "value", "pricing"] },
85
+ { name: "Integration", keywords: ["integration", "integrate", "api", "connect", "compatible"] },
86
+ { name: "Mobile", keywords: ["mobile", "app", "ios", "android", "phone", "tablet"] }
87
+ ];
88
+ for (const feedback of feedbackList) {
89
+ const lowerFeedback = feedback.toLowerCase();
90
+ for (const { name, keywords } of themeKeywords) {
91
+ if (keywords.some((keyword) => lowerFeedback.includes(keyword))) {
92
+ if (!themeMap.has(name)) {
93
+ themeMap.set(name, []);
94
+ }
95
+ themeMap.get(name)?.push(feedback);
96
+ }
97
+ }
98
+ }
99
+ return themeMap;
100
+ }
101
+ function determineOverallSentiment(sentiments) {
102
+ const counts = {
103
+ positive: sentiments.filter((s) => s === "positive").length,
104
+ negative: sentiments.filter((s) => s === "negative").length,
105
+ neutral: sentiments.filter((s) => s === "neutral").length
106
+ };
107
+ const total = sentiments.length;
108
+ const positiveRatio = counts.positive / total;
109
+ const negativeRatio = counts.negative / total;
110
+ if (positiveRatio > 0.6) return "positive";
111
+ if (negativeRatio > 0.6) return "negative";
112
+ if (positiveRatio > 0.3 && negativeRatio > 0.3) return "mixed";
113
+ return "neutral";
114
+ }
115
+ var feedbackThemesTool = tool({
116
+ description: "Extracts themes and sentiment from customer feedback text. Identifies recurring themes, scores sentiment per theme, and provides frequency counts.",
117
+ inputSchema: jsonSchema({
118
+ type: "object",
119
+ properties: {
120
+ feedback: {
121
+ type: "array",
122
+ description: "Array of customer feedback entries (comments, reviews, survey responses)",
123
+ items: {
124
+ type: "string",
125
+ description: "Individual feedback text"
126
+ }
127
+ }
128
+ },
129
+ required: ["feedback"],
130
+ additionalProperties: false
131
+ }),
132
+ async execute({ feedback }) {
133
+ if (!Array.isArray(feedback) || feedback.length === 0) {
134
+ throw new Error("feedback must be a non-empty array");
135
+ }
136
+ const validFeedback = feedback.filter((f) => f && f.trim().length > 0);
137
+ if (validFeedback.length === 0) {
138
+ throw new Error("No valid feedback entries provided");
139
+ }
140
+ const themeMap = extractThemes(validFeedback);
141
+ const themes = [];
142
+ for (const [themeName, examples] of themeMap.entries()) {
143
+ const sentiments = examples.map((ex) => analyzeSentiment(ex));
144
+ const avgScore = sentiments.reduce((sum, { score }) => sum + score, 0) / sentiments.length;
145
+ let themeSentiment = "neutral";
146
+ const posCount = sentiments.filter((s) => s.sentiment === "positive").length;
147
+ const negCount = sentiments.filter((s) => s.sentiment === "negative").length;
148
+ const ratio = posCount / sentiments.length;
149
+ if (ratio > 0.6) {
150
+ themeSentiment = "positive";
151
+ } else if (negCount / sentiments.length > 0.6) {
152
+ themeSentiment = "negative";
153
+ } else if (posCount > 0 && negCount > 0) {
154
+ themeSentiment = "mixed";
155
+ }
156
+ themes.push({
157
+ name: themeName,
158
+ frequency: examples.length,
159
+ sentiment: themeSentiment,
160
+ sentimentScore: Math.round(avgScore * 100) / 100,
161
+ examples: examples.slice(0, 3)
162
+ // Top 3 examples
163
+ });
164
+ }
165
+ themes.sort((a, b) => b.frequency - a.frequency);
166
+ const allSentiments = validFeedback.map((f) => analyzeSentiment(f).sentiment);
167
+ const overallSentiment = determineOverallSentiment(allSentiments);
168
+ const summary = {
169
+ positiveCount: allSentiments.filter((s) => s === "positive").length,
170
+ negativeCount: allSentiments.filter((s) => s === "negative").length,
171
+ neutralCount: allSentiments.filter((s) => s === "neutral").length
172
+ };
173
+ return {
174
+ themes,
175
+ overallSentiment,
176
+ totalFeedback: validFeedback.length,
177
+ summary
178
+ };
179
+ }
180
+ });
181
+ var index_default = feedbackThemesTool;
182
+
183
+ export { index_default as default, feedbackThemesTool };
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@tpmjs/feedback-themes",
3
+ "version": "0.1.0",
4
+ "description": "Extracts themes and sentiment from customer feedback text",
5
+ "type": "module",
6
+ "keywords": [
7
+ "tpmjs",
8
+ "cx",
9
+ "feedback",
10
+ "sentiment",
11
+ "analysis",
12
+ "customer-success"
13
+ ],
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "devDependencies": {
24
+ "tsup": "^8.3.5",
25
+ "typescript": "^5.9.3",
26
+ "@tpmjs/tsconfig": "0.0.0"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/ajaxdavis/tpmjs.git",
34
+ "directory": "packages/tools/official/feedback-themes"
35
+ },
36
+ "homepage": "https://tpmjs.com",
37
+ "license": "MIT",
38
+ "tpmjs": {
39
+ "category": "cx",
40
+ "frameworks": [
41
+ "vercel-ai"
42
+ ],
43
+ "tools": [
44
+ {
45
+ "exportName": "feedbackThemesTool",
46
+ "description": "Extracts themes and sentiment from customer feedback text. Identifies recurring themes, scores sentiment per theme, and provides frequency counts.",
47
+ "parameters": [
48
+ {
49
+ "name": "feedback",
50
+ "type": "string[]",
51
+ "description": "Array of customer feedback entries",
52
+ "required": true
53
+ }
54
+ ],
55
+ "returns": {
56
+ "type": "FeedbackThemes",
57
+ "description": "Themes with sentiment scores, frequency counts, and example feedback"
58
+ },
59
+ "aiAgent": {
60
+ "useCase": "Use this tool to analyze customer feedback, identify common themes, track sentiment trends, and prioritize product improvements based on customer voice.",
61
+ "limitations": "Sentiment analysis is keyword-based. For complex sentiment, consider using an AI model. Requires sufficient feedback volume for meaningful themes.",
62
+ "examples": [
63
+ "Analyze product reviews to identify improvement areas",
64
+ "Extract themes from NPS survey comments",
65
+ "Track sentiment trends across feedback channels"
66
+ ]
67
+ }
68
+ }
69
+ ]
70
+ },
71
+ "dependencies": {
72
+ "ai": "6.0.0-beta.124"
73
+ },
74
+ "scripts": {
75
+ "build": "tsup",
76
+ "dev": "tsup --watch",
77
+ "type-check": "tsc --noEmit",
78
+ "clean": "rm -rf dist .turbo"
79
+ }
80
+ }