@graphext/cuery 0.6.3 → 0.7.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/esm/src/schemas/sentiment.schema.d.ts +4 -0
- package/esm/src/schemas/sentiment.schema.d.ts.map +1 -1
- package/esm/src/schemas/sentiment.schema.js +6 -2
- package/esm/src/tools/sentiment.d.ts +8 -2
- package/esm/src/tools/sentiment.d.ts.map +1 -1
- package/esm/src/tools/sentiment.js +65 -9
- package/package.json +1 -1
- package/script/src/schemas/sentiment.schema.d.ts +4 -0
- package/script/src/schemas/sentiment.schema.d.ts.map +1 -1
- package/script/src/schemas/sentiment.schema.js +6 -2
- package/script/src/tools/sentiment.d.ts +8 -2
- package/script/src/tools/sentiment.d.ts.map +1 -1
- package/script/src/tools/sentiment.js +65 -9
|
@@ -6,6 +6,8 @@ export declare const ABSentimentSchema: z.ZodObject<{
|
|
|
6
6
|
negative: "negative";
|
|
7
7
|
}>;
|
|
8
8
|
reason: z.ZodString;
|
|
9
|
+
quote: z.ZodString;
|
|
10
|
+
context: z.ZodNullable<z.ZodString>;
|
|
9
11
|
}, z.core.$strip>;
|
|
10
12
|
export declare const ABSentimentsSchema: z.ZodObject<{
|
|
11
13
|
aspects: z.ZodArray<z.ZodObject<{
|
|
@@ -15,6 +17,8 @@ export declare const ABSentimentsSchema: z.ZodObject<{
|
|
|
15
17
|
negative: "negative";
|
|
16
18
|
}>;
|
|
17
19
|
reason: z.ZodString;
|
|
20
|
+
quote: z.ZodString;
|
|
21
|
+
context: z.ZodNullable<z.ZodString>;
|
|
18
22
|
}, z.core.$strip>>;
|
|
19
23
|
}, z.core.$strip>;
|
|
20
24
|
export type ABSentiment = z.infer<typeof ABSentimentSchema>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sentiment.schema.d.ts","sourceRoot":"","sources":["../../../src/src/schemas/sentiment.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,+CAA+C,CAAC;AAElE,eAAO,MAAM,iBAAiB
|
|
1
|
+
{"version":3,"file":"sentiment.schema.d.ts","sourceRoot":"","sources":["../../../src/src/schemas/sentiment.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,+CAA+C,CAAC;AAElE,eAAO,MAAM,iBAAiB;;;;;;;;;iBAc5B,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;iBAE7B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
|
|
@@ -2,8 +2,12 @@ import { z } from '../../deps/jsr.io/@zod/zod/4.3.6/src/index.js';
|
|
|
2
2
|
export const ABSentimentSchema = z.object({
|
|
3
3
|
aspect: z.string().describe('The specific entity or aspect mentioned in the text.'),
|
|
4
4
|
sentiment: z.enum(['positive', 'negative']).describe('The sentiment expressed toward the aspect, either positive or negative.'),
|
|
5
|
-
reason: z.string().describe('A brief explanation of why this sentiment was assigned to the aspect.')
|
|
5
|
+
reason: z.string().describe('A brief explanation of why this sentiment was assigned to the aspect.'),
|
|
6
|
+
quote: z.string().min(1).refine((val) => val.trim().length > 0, {
|
|
7
|
+
message: 'Quote must not be empty or whitespace-only',
|
|
8
|
+
}).describe('The exact text fragment from the input containing both the aspect and the sentiment expressed about it.'),
|
|
9
|
+
context: z.string().nullable().describe('Optional contextual information about the aspect, such as the brand or entity it relates to.'),
|
|
6
10
|
});
|
|
7
11
|
export const ABSentimentsSchema = z.object({
|
|
8
|
-
aspects: z.array(ABSentimentSchema).describe('A list of aspects with their associated sentiments and
|
|
12
|
+
aspects: z.array(ABSentimentSchema).describe('A list of aspects with their associated sentiments, reasons, quotes, and optional context.')
|
|
9
13
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Message } from '../llm.js';
|
|
1
|
+
import type { LLMResponse, Message } from '../llm.js';
|
|
2
2
|
import type { BrandContext } from '../schemas/brand.schema.js';
|
|
3
3
|
import { type ABSentiment, type ABSentiments } from '../schemas/sentiment.schema.js';
|
|
4
|
-
import {
|
|
4
|
+
import { type ModelConfig, Tool } from '../tool.js';
|
|
5
5
|
import { Classifier } from './classifier.js';
|
|
6
6
|
export interface SentimentExtractorConfig {
|
|
7
7
|
/** Additional instructions for sentiment extraction */
|
|
@@ -23,11 +23,17 @@ export declare class SentimentExtractor extends Tool<string | null, ABSentiments
|
|
|
23
23
|
negative: "negative";
|
|
24
24
|
}>;
|
|
25
25
|
reason: import("../../deps/jsr.io/@zod/zod/4.3.6/src/index.js").ZodString;
|
|
26
|
+
quote: import("../../deps/jsr.io/@zod/zod/4.3.6/src/index.js").ZodString;
|
|
27
|
+
context: import("../../deps/jsr.io/@zod/zod/4.3.6/src/index.js").ZodNullable<import("../../deps/jsr.io/@zod/zod/4.3.6/src/index.js").ZodString>;
|
|
26
28
|
}, import("../../deps/jsr.io/@zod/zod/4.3.6/src/v4/core/schemas.js").$strip>>;
|
|
27
29
|
}, import("../../deps/jsr.io/@zod/zod/4.3.6/src/v4/core/schemas.js").$strip>;
|
|
28
30
|
protected prompt(text: string | null): Message[];
|
|
29
31
|
protected isEmpty(text: string | null): boolean;
|
|
30
32
|
protected extractResult(parsed: ABSentiments): Array<ABSentiment>;
|
|
33
|
+
/**
|
|
34
|
+
* Override invoke to add quote validation
|
|
35
|
+
*/
|
|
36
|
+
invoke(input: string | null, options?: Partial<ModelConfig>): Promise<LLMResponse<Array<ABSentiment> | null>>;
|
|
31
37
|
}
|
|
32
38
|
/**
|
|
33
39
|
* Sentiment polarity labels for classification.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sentiment.d.ts","sourceRoot":"","sources":["../../../src/src/tools/sentiment.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"sentiment.d.ts","sourceRoot":"","sources":["../../../src/src/tools/sentiment.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAW,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAsB,MAAM,gCAAgC,CAAC;AACzG,OAAO,EAAE,KAAK,WAAW,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AA6D7C,MAAM,WAAW,wBAAwB;IACxC,uDAAuD;IACvD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yCAAyC;IACzC,KAAK,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;CAC5B;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,IAAI,CAAC,MAAM,GAAG,IAAI,EAAE,YAAY,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;IAC5F,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAE1B,MAAM,EAAE,wBAAwB,YAAK,EAAE,WAAW,EAAE,WAAW;cAuBxD,MAAM;;;;;;;;;;;;IAIzB,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,EAAE;cAQ7B,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO;cAIrC,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,KAAK,CAAC,WAAW,CAAC;IAI1E;;OAEG;IACY,MAAM,CACpB,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,OAAO,GAAE,OAAO,CAAC,WAAW,CAAM,GAChC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC,CAAC;CA+BlD;AAMD;;GAEG;AACH,eAAO,MAAM,yBAAyB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAI5D,CAAC;AAWF;;GAEG;AACH,MAAM,WAAW,iCAAiC;IACjD,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,qBAAa,2BAA4B,SAAQ,UAAU;gBAC9C,MAAM,EAAE,iCAAiC,YAAK,EAAE,WAAW,EAAE,WAAW;CAOpF;AAED,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACvF,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC"}
|
|
@@ -2,6 +2,15 @@ import { dedent } from '../helpers/utils.js';
|
|
|
2
2
|
import { ABSentimentsSchema } from '../schemas/sentiment.schema.js';
|
|
3
3
|
import { Tool } from '../tool.js';
|
|
4
4
|
import { Classifier } from './classifier.js';
|
|
5
|
+
/**
|
|
6
|
+
* Formats a portfolio array as a comma-separated list of product/service names.
|
|
7
|
+
* Includes category in parentheses if available.
|
|
8
|
+
*/
|
|
9
|
+
function formatPortfolio(portfolio) {
|
|
10
|
+
return portfolio
|
|
11
|
+
.map((p) => (p.category ? `${p.name} (${p.category})` : p.name))
|
|
12
|
+
.join(', ');
|
|
13
|
+
}
|
|
5
14
|
const ABS_PROMPT_SYSTEM = dedent(`
|
|
6
15
|
You're an expert in Aspect-Based Sentiment Analysis. Your task involves identifying specific
|
|
7
16
|
entities mentioned in a text (e.g. a person, product, service, or experience) and determining the
|
|
@@ -15,10 +24,23 @@ Specifically:
|
|
|
15
24
|
a. the aspect as it occurs in the text (key "aspect")
|
|
16
25
|
b. the sentiment label as either "positive" or "negative" (key "sentiment")
|
|
17
26
|
c. the reason for the sentiment assignment as a short text (key "reason")
|
|
27
|
+
d. the exact text fragment containing both the aspect and what is said about it (key "quote") - must be a verbatim substring
|
|
28
|
+
e. optional contextual information about the aspect, such as the brand or entity it relates to (key "context") - use null if not applicable
|
|
18
29
|
4. If there are no sentiment-bearing aspects in the text, the output should be an empty list
|
|
19
30
|
|
|
20
|
-
|
|
21
|
-
|
|
31
|
+
IMPORTANT: The "quote" field must be an EXACT verbatim substring from the input text. It should
|
|
32
|
+
include the complete phrase mentioning both the aspect and the sentiment expressed about it.
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
|
|
36
|
+
Input text: "The room service at the Grand Hotel was absolutely terrible and the staff were rude, but the view from our room was breathtaking."
|
|
37
|
+
|
|
38
|
+
Output:
|
|
39
|
+
[
|
|
40
|
+
{"aspect": "The room service at the Grand Hotel", "sentiment": "negative", "reason": "Described as terrible.", "quote": "The room service at the Grand Hotel was absolutely terrible", "context": "Grand Hotel"},
|
|
41
|
+
{"aspect": "the staff", "sentiment": "negative", "reason": "Described as rude.", "quote": "the staff were rude", "context": "Grand Hotel"},
|
|
42
|
+
{"aspect": "the view from our room", "sentiment": "positive", "reason": "Described as breathtaking.", "quote": "the view from our room was breathtaking", "context": "Grand Hotel"}
|
|
43
|
+
]
|
|
22
44
|
|
|
23
45
|
Only extract aspects that have an explicitly expressed sentiment associated with them, i.e.
|
|
24
46
|
subjective opinions, feelings, or evaluations. Do not infer sentiment from factual statements,
|
|
@@ -45,11 +67,18 @@ export class SentimentExtractor extends Tool {
|
|
|
45
67
|
super(modelConfig);
|
|
46
68
|
const { instructions = '', brand = null } = config;
|
|
47
69
|
const brandInstructions = brand
|
|
48
|
-
?
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
? (() => {
|
|
71
|
+
const portfolio = formatPortfolio(brand.portfolio);
|
|
72
|
+
const portfolioText = portfolio
|
|
73
|
+
? ` or its products/services (${portfolio})`
|
|
74
|
+
: '';
|
|
75
|
+
return dedent(`
|
|
76
|
+
Pay special attention to mentions of "${brand.shortName}"${portfolioText}.
|
|
77
|
+
When an aspect relates to a brand/entity, set the "context" field to "${brand.shortName}".
|
|
78
|
+
Keep aspect names and quoted text exactly as they appear in the original input.
|
|
79
|
+
Respond in language code ${brand.language}.
|
|
80
|
+
`);
|
|
81
|
+
})()
|
|
53
82
|
: '';
|
|
54
83
|
const combinedInstructions = [instructions, brandInstructions].filter(Boolean).join('\n\n');
|
|
55
84
|
this.systemPrompt = ABS_PROMPT_SYSTEM.replace('{instructions}', combinedInstructions);
|
|
@@ -61,7 +90,7 @@ export class SentimentExtractor extends Tool {
|
|
|
61
90
|
const userPrompt = ABS_PROMPT_USER.replace('{text}', text ?? '');
|
|
62
91
|
return [
|
|
63
92
|
{ role: 'system', content: this.systemPrompt },
|
|
64
|
-
{ role: 'user', content: userPrompt }
|
|
93
|
+
{ role: 'user', content: userPrompt },
|
|
65
94
|
];
|
|
66
95
|
}
|
|
67
96
|
isEmpty(text) {
|
|
@@ -70,6 +99,33 @@ export class SentimentExtractor extends Tool {
|
|
|
70
99
|
extractResult(parsed) {
|
|
71
100
|
return parsed.aspects;
|
|
72
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Override invoke to add quote validation
|
|
104
|
+
*/
|
|
105
|
+
async invoke(input, options = {}) {
|
|
106
|
+
const response = await super.invoke(input, options);
|
|
107
|
+
// If we have a successful result and non-empty input, validate quotes
|
|
108
|
+
if (response.parsed && input && input.trim() !== '') {
|
|
109
|
+
const validatedResult = response.parsed.filter((sentiment) => {
|
|
110
|
+
// Check that quote is non-empty and not just whitespace
|
|
111
|
+
if (!sentiment.quote || sentiment.quote.trim().length === 0) {
|
|
112
|
+
console.warn(`Empty or whitespace-only quote for aspect "${sentiment.aspect}"`);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
// Check that quote is a substring of the input
|
|
116
|
+
if (!input.includes(sentiment.quote)) {
|
|
117
|
+
console.warn(`Quote not found in text: "${sentiment.quote}" for aspect "${sentiment.aspect}"`);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
...response,
|
|
124
|
+
parsed: validatedResult,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return response;
|
|
128
|
+
}
|
|
73
129
|
}
|
|
74
130
|
// =============================================================================
|
|
75
131
|
// SentimentPolarityClassifier (overall sentiment polarity)
|
|
@@ -80,7 +136,7 @@ export class SentimentExtractor extends Tool {
|
|
|
80
136
|
export const SENTIMENT_POLARITY_LABELS = {
|
|
81
137
|
positive: 'Expresses favorable opinions, approval, satisfaction, or optimism.',
|
|
82
138
|
neutral: 'No clear sentiment expressed; factual, balanced, or ambiguous.',
|
|
83
|
-
negative: 'Expresses unfavorable opinions, criticism, dissatisfaction, or pessimism.'
|
|
139
|
+
negative: 'Expresses unfavorable opinions, criticism, dissatisfaction, or pessimism.',
|
|
84
140
|
};
|
|
85
141
|
/**
|
|
86
142
|
* Predefined instructions for sentiment polarity classification.
|
package/package.json
CHANGED
|
@@ -6,6 +6,8 @@ export declare const ABSentimentSchema: z.ZodObject<{
|
|
|
6
6
|
negative: "negative";
|
|
7
7
|
}>;
|
|
8
8
|
reason: z.ZodString;
|
|
9
|
+
quote: z.ZodString;
|
|
10
|
+
context: z.ZodNullable<z.ZodString>;
|
|
9
11
|
}, z.core.$strip>;
|
|
10
12
|
export declare const ABSentimentsSchema: z.ZodObject<{
|
|
11
13
|
aspects: z.ZodArray<z.ZodObject<{
|
|
@@ -15,6 +17,8 @@ export declare const ABSentimentsSchema: z.ZodObject<{
|
|
|
15
17
|
negative: "negative";
|
|
16
18
|
}>;
|
|
17
19
|
reason: z.ZodString;
|
|
20
|
+
quote: z.ZodString;
|
|
21
|
+
context: z.ZodNullable<z.ZodString>;
|
|
18
22
|
}, z.core.$strip>>;
|
|
19
23
|
}, z.core.$strip>;
|
|
20
24
|
export type ABSentiment = z.infer<typeof ABSentimentSchema>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sentiment.schema.d.ts","sourceRoot":"","sources":["../../../src/src/schemas/sentiment.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,+CAA+C,CAAC;AAElE,eAAO,MAAM,iBAAiB
|
|
1
|
+
{"version":3,"file":"sentiment.schema.d.ts","sourceRoot":"","sources":["../../../src/src/schemas/sentiment.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,+CAA+C,CAAC;AAElE,eAAO,MAAM,iBAAiB;;;;;;;;;iBAc5B,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;iBAE7B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
|
|
@@ -5,8 +5,12 @@ const index_js_1 = require("../../deps/jsr.io/@zod/zod/4.3.6/src/index.js");
|
|
|
5
5
|
exports.ABSentimentSchema = index_js_1.z.object({
|
|
6
6
|
aspect: index_js_1.z.string().describe('The specific entity or aspect mentioned in the text.'),
|
|
7
7
|
sentiment: index_js_1.z.enum(['positive', 'negative']).describe('The sentiment expressed toward the aspect, either positive or negative.'),
|
|
8
|
-
reason: index_js_1.z.string().describe('A brief explanation of why this sentiment was assigned to the aspect.')
|
|
8
|
+
reason: index_js_1.z.string().describe('A brief explanation of why this sentiment was assigned to the aspect.'),
|
|
9
|
+
quote: index_js_1.z.string().min(1).refine((val) => val.trim().length > 0, {
|
|
10
|
+
message: 'Quote must not be empty or whitespace-only',
|
|
11
|
+
}).describe('The exact text fragment from the input containing both the aspect and the sentiment expressed about it.'),
|
|
12
|
+
context: index_js_1.z.string().nullable().describe('Optional contextual information about the aspect, such as the brand or entity it relates to.'),
|
|
9
13
|
});
|
|
10
14
|
exports.ABSentimentsSchema = index_js_1.z.object({
|
|
11
|
-
aspects: index_js_1.z.array(exports.ABSentimentSchema).describe('A list of aspects with their associated sentiments and
|
|
15
|
+
aspects: index_js_1.z.array(exports.ABSentimentSchema).describe('A list of aspects with their associated sentiments, reasons, quotes, and optional context.')
|
|
12
16
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Message } from '../llm.js';
|
|
1
|
+
import type { LLMResponse, Message } from '../llm.js';
|
|
2
2
|
import type { BrandContext } from '../schemas/brand.schema.js';
|
|
3
3
|
import { type ABSentiment, type ABSentiments } from '../schemas/sentiment.schema.js';
|
|
4
|
-
import {
|
|
4
|
+
import { type ModelConfig, Tool } from '../tool.js';
|
|
5
5
|
import { Classifier } from './classifier.js';
|
|
6
6
|
export interface SentimentExtractorConfig {
|
|
7
7
|
/** Additional instructions for sentiment extraction */
|
|
@@ -23,11 +23,17 @@ export declare class SentimentExtractor extends Tool<string | null, ABSentiments
|
|
|
23
23
|
negative: "negative";
|
|
24
24
|
}>;
|
|
25
25
|
reason: import("../../deps/jsr.io/@zod/zod/4.3.6/src/index.js").ZodString;
|
|
26
|
+
quote: import("../../deps/jsr.io/@zod/zod/4.3.6/src/index.js").ZodString;
|
|
27
|
+
context: import("../../deps/jsr.io/@zod/zod/4.3.6/src/index.js").ZodNullable<import("../../deps/jsr.io/@zod/zod/4.3.6/src/index.js").ZodString>;
|
|
26
28
|
}, import("../../deps/jsr.io/@zod/zod/4.3.6/src/v4/core/schemas.js").$strip>>;
|
|
27
29
|
}, import("../../deps/jsr.io/@zod/zod/4.3.6/src/v4/core/schemas.js").$strip>;
|
|
28
30
|
protected prompt(text: string | null): Message[];
|
|
29
31
|
protected isEmpty(text: string | null): boolean;
|
|
30
32
|
protected extractResult(parsed: ABSentiments): Array<ABSentiment>;
|
|
33
|
+
/**
|
|
34
|
+
* Override invoke to add quote validation
|
|
35
|
+
*/
|
|
36
|
+
invoke(input: string | null, options?: Partial<ModelConfig>): Promise<LLMResponse<Array<ABSentiment> | null>>;
|
|
31
37
|
}
|
|
32
38
|
/**
|
|
33
39
|
* Sentiment polarity labels for classification.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sentiment.d.ts","sourceRoot":"","sources":["../../../src/src/tools/sentiment.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"sentiment.d.ts","sourceRoot":"","sources":["../../../src/src/tools/sentiment.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAW,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAsB,MAAM,gCAAgC,CAAC;AACzG,OAAO,EAAE,KAAK,WAAW,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AA6D7C,MAAM,WAAW,wBAAwB;IACxC,uDAAuD;IACvD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yCAAyC;IACzC,KAAK,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;CAC5B;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,IAAI,CAAC,MAAM,GAAG,IAAI,EAAE,YAAY,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;IAC5F,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAE1B,MAAM,EAAE,wBAAwB,YAAK,EAAE,WAAW,EAAE,WAAW;cAuBxD,MAAM;;;;;;;;;;;;IAIzB,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,EAAE;cAQ7B,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO;cAIrC,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,KAAK,CAAC,WAAW,CAAC;IAI1E;;OAEG;IACY,MAAM,CACpB,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,OAAO,GAAE,OAAO,CAAC,WAAW,CAAM,GAChC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC,CAAC;CA+BlD;AAMD;;GAEG;AACH,eAAO,MAAM,yBAAyB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAI5D,CAAC;AAWF;;GAEG;AACH,MAAM,WAAW,iCAAiC;IACjD,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,qBAAa,2BAA4B,SAAQ,UAAU;gBAC9C,MAAM,EAAE,iCAAiC,YAAK,EAAE,WAAW,EAAE,WAAW;CAOpF;AAED,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACvF,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC"}
|
|
@@ -5,6 +5,15 @@ const utils_js_1 = require("../helpers/utils.js");
|
|
|
5
5
|
const sentiment_schema_js_1 = require("../schemas/sentiment.schema.js");
|
|
6
6
|
const tool_js_1 = require("../tool.js");
|
|
7
7
|
const classifier_js_1 = require("./classifier.js");
|
|
8
|
+
/**
|
|
9
|
+
* Formats a portfolio array as a comma-separated list of product/service names.
|
|
10
|
+
* Includes category in parentheses if available.
|
|
11
|
+
*/
|
|
12
|
+
function formatPortfolio(portfolio) {
|
|
13
|
+
return portfolio
|
|
14
|
+
.map((p) => (p.category ? `${p.name} (${p.category})` : p.name))
|
|
15
|
+
.join(', ');
|
|
16
|
+
}
|
|
8
17
|
const ABS_PROMPT_SYSTEM = (0, utils_js_1.dedent)(`
|
|
9
18
|
You're an expert in Aspect-Based Sentiment Analysis. Your task involves identifying specific
|
|
10
19
|
entities mentioned in a text (e.g. a person, product, service, or experience) and determining the
|
|
@@ -18,10 +27,23 @@ Specifically:
|
|
|
18
27
|
a. the aspect as it occurs in the text (key "aspect")
|
|
19
28
|
b. the sentiment label as either "positive" or "negative" (key "sentiment")
|
|
20
29
|
c. the reason for the sentiment assignment as a short text (key "reason")
|
|
30
|
+
d. the exact text fragment containing both the aspect and what is said about it (key "quote") - must be a verbatim substring
|
|
31
|
+
e. optional contextual information about the aspect, such as the brand or entity it relates to (key "context") - use null if not applicable
|
|
21
32
|
4. If there are no sentiment-bearing aspects in the text, the output should be an empty list
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
IMPORTANT: The "quote" field must be an EXACT verbatim substring from the input text. It should
|
|
35
|
+
include the complete phrase mentioning both the aspect and the sentiment expressed about it.
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
|
|
39
|
+
Input text: "The room service at the Grand Hotel was absolutely terrible and the staff were rude, but the view from our room was breathtaking."
|
|
40
|
+
|
|
41
|
+
Output:
|
|
42
|
+
[
|
|
43
|
+
{"aspect": "The room service at the Grand Hotel", "sentiment": "negative", "reason": "Described as terrible.", "quote": "The room service at the Grand Hotel was absolutely terrible", "context": "Grand Hotel"},
|
|
44
|
+
{"aspect": "the staff", "sentiment": "negative", "reason": "Described as rude.", "quote": "the staff were rude", "context": "Grand Hotel"},
|
|
45
|
+
{"aspect": "the view from our room", "sentiment": "positive", "reason": "Described as breathtaking.", "quote": "the view from our room was breathtaking", "context": "Grand Hotel"}
|
|
46
|
+
]
|
|
25
47
|
|
|
26
48
|
Only extract aspects that have an explicitly expressed sentiment associated with them, i.e.
|
|
27
49
|
subjective opinions, feelings, or evaluations. Do not infer sentiment from factual statements,
|
|
@@ -48,11 +70,18 @@ class SentimentExtractor extends tool_js_1.Tool {
|
|
|
48
70
|
super(modelConfig);
|
|
49
71
|
const { instructions = '', brand = null } = config;
|
|
50
72
|
const brandInstructions = brand
|
|
51
|
-
? (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
? (() => {
|
|
74
|
+
const portfolio = formatPortfolio(brand.portfolio);
|
|
75
|
+
const portfolioText = portfolio
|
|
76
|
+
? ` or its products/services (${portfolio})`
|
|
77
|
+
: '';
|
|
78
|
+
return (0, utils_js_1.dedent)(`
|
|
79
|
+
Pay special attention to mentions of "${brand.shortName}"${portfolioText}.
|
|
80
|
+
When an aspect relates to a brand/entity, set the "context" field to "${brand.shortName}".
|
|
81
|
+
Keep aspect names and quoted text exactly as they appear in the original input.
|
|
82
|
+
Respond in language code ${brand.language}.
|
|
83
|
+
`);
|
|
84
|
+
})()
|
|
56
85
|
: '';
|
|
57
86
|
const combinedInstructions = [instructions, brandInstructions].filter(Boolean).join('\n\n');
|
|
58
87
|
this.systemPrompt = ABS_PROMPT_SYSTEM.replace('{instructions}', combinedInstructions);
|
|
@@ -64,7 +93,7 @@ class SentimentExtractor extends tool_js_1.Tool {
|
|
|
64
93
|
const userPrompt = ABS_PROMPT_USER.replace('{text}', text ?? '');
|
|
65
94
|
return [
|
|
66
95
|
{ role: 'system', content: this.systemPrompt },
|
|
67
|
-
{ role: 'user', content: userPrompt }
|
|
96
|
+
{ role: 'user', content: userPrompt },
|
|
68
97
|
];
|
|
69
98
|
}
|
|
70
99
|
isEmpty(text) {
|
|
@@ -73,6 +102,33 @@ class SentimentExtractor extends tool_js_1.Tool {
|
|
|
73
102
|
extractResult(parsed) {
|
|
74
103
|
return parsed.aspects;
|
|
75
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Override invoke to add quote validation
|
|
107
|
+
*/
|
|
108
|
+
async invoke(input, options = {}) {
|
|
109
|
+
const response = await super.invoke(input, options);
|
|
110
|
+
// If we have a successful result and non-empty input, validate quotes
|
|
111
|
+
if (response.parsed && input && input.trim() !== '') {
|
|
112
|
+
const validatedResult = response.parsed.filter((sentiment) => {
|
|
113
|
+
// Check that quote is non-empty and not just whitespace
|
|
114
|
+
if (!sentiment.quote || sentiment.quote.trim().length === 0) {
|
|
115
|
+
console.warn(`Empty or whitespace-only quote for aspect "${sentiment.aspect}"`);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
// Check that quote is a substring of the input
|
|
119
|
+
if (!input.includes(sentiment.quote)) {
|
|
120
|
+
console.warn(`Quote not found in text: "${sentiment.quote}" for aspect "${sentiment.aspect}"`);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
...response,
|
|
127
|
+
parsed: validatedResult,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return response;
|
|
131
|
+
}
|
|
76
132
|
}
|
|
77
133
|
exports.SentimentExtractor = SentimentExtractor;
|
|
78
134
|
// =============================================================================
|
|
@@ -84,7 +140,7 @@ exports.SentimentExtractor = SentimentExtractor;
|
|
|
84
140
|
exports.SENTIMENT_POLARITY_LABELS = {
|
|
85
141
|
positive: 'Expresses favorable opinions, approval, satisfaction, or optimism.',
|
|
86
142
|
neutral: 'No clear sentiment expressed; factual, balanced, or ambiguous.',
|
|
87
|
-
negative: 'Expresses unfavorable opinions, criticism, dissatisfaction, or pessimism.'
|
|
143
|
+
negative: 'Expresses unfavorable opinions, criticism, dissatisfaction, or pessimism.',
|
|
88
144
|
};
|
|
89
145
|
/**
|
|
90
146
|
* Predefined instructions for sentiment polarity classification.
|