sentiment_insights 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c3ede714068979aaa53076ed6fef752235b50d45ab57f388d9baf8a0e952baf
4
- data.tar.gz: 6fc6b77e018b41a3503d45938b65575f0bd90dfbd8a16ac26257c738591709f3
3
+ metadata.gz: '008d6ea6d7343f377abedc02ac6c28bd072c21f8f69d150fc5c5a2dd744fbf96'
4
+ data.tar.gz: 1cd6db74422570ea2ec02a6e9b8976cd9b8a91120d271ee900057431a4b4665e
5
5
  SHA512:
6
- metadata.gz: d5025dc6ab42d5b4358d66084e3306773984e796da881acf917fd879d579375d152cb7458477d3071f080d43bbb90b0ea8b122833343b963f5b84aac15d11adf
7
- data.tar.gz: 63f1abac85a05be6ee68006eeb5729bcc53f1a0f4932ad13067e9ca06f4b89be9ce62f88ca5c8008d647862b29fd7704658a6bb3153dcd8063923650a6c2eb63
6
+ metadata.gz: e393148073465c4f022bff7440d11df6e2249b77aee022a2f7c6a6d941a7271c1541b331688f00c5d9cdf3928fcfb45dec8a087ea151316595830b769c9974e1
7
+ data.tar.gz: b8229c858ff529e53144214d567d679eb608c3f03dc5779a3541f07a2167e3eba6c56090bc0260b1c92ca7a32e4b2572ecf8ce5db315802c2a44b7f878c04e2c
data/Gemfile.lock CHANGED
@@ -2,7 +2,7 @@ PATH
2
2
  remote: .
3
3
  specs:
4
4
  sentiment_insights (0.1.0)
5
- aws-sdk-comprehend (~> 1.98.0)
5
+ aws-sdk-comprehend (>= 1.98.0)
6
6
  sentimental (~> 1.4.0)
7
7
 
8
8
  GEM
data/README.md CHANGED
@@ -1,12 +1,94 @@
1
- # SentimentInsights 💬📊
1
+ # SentimentInsights
2
2
 
3
- **SentimentInsights** is a Ruby gem that helps you uncover meaningful insights from open-ended survey responses using Natural Language Processing (NLP). It supports multi-provider analysis via OpenAI, AWS Comprehend, or a local fallback engine.
3
+ **SentimentInsights** is a Ruby gem for extracting sentiment, key phrases, and named entities from survey responses or free-form textual data. It offers a plug-and-play interface to different NLP providers, including OpenAI and AWS.
4
4
 
5
5
  ---
6
6
 
7
- ## Features
7
+ ## Table of Contents
8
8
 
9
- ### ✅ 1. Sentiment Analysis
9
+ - [Installation](#installation)
10
+ - [Configuration](#configuration)
11
+ - [Usage](#usage)
12
+ - [Sentiment Analysis](#sentiment-analysis)
13
+ - [Key Phrase Extraction](#key-phrase-extraction)
14
+ - [Entity Extraction](#entity-extraction)
15
+ - [Provider Options & Custom Prompts](#provider-options--custom-prompts)
16
+ - [Full Example](#full-example)
17
+ - [Contributing](#contributing)
18
+ - [License](#license)
19
+
20
+ ---
21
+
22
+ ## Installation
23
+
24
+ Add to your Gemfile:
25
+
26
+ ```ruby
27
+ gem 'sentiment_insights'
28
+ ```
29
+
30
+ Then install:
31
+
32
+ ```bash
33
+ bundle install
34
+ ```
35
+
36
+ Or install it directly:
37
+
38
+ ```bash
39
+ gem install sentiment_insights
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Configuration
45
+
46
+ Configure the provider and (if using OpenAI or AWS) your API key:
47
+
48
+ ```ruby
49
+ require 'sentiment_insights'
50
+
51
+ # For OpenAI
52
+ SentimentInsights.configure do |config|
53
+ config.provider = :openai
54
+ config.openai_api_key = ENV["OPENAI_API_KEY"]
55
+ end
56
+
57
+ # For AWS
58
+ SentimentInsights.configure do |config|
59
+ config.provider = :aws
60
+ config.aws_region = 'us-east-1'
61
+ end
62
+
63
+ # For sentimental
64
+ SentimentInsights.configure do |config|
65
+ config.provider = :sentimental
66
+ end
67
+ ```
68
+
69
+ Supported providers:
70
+ - `:openai`
71
+ - `:aws`
72
+ - `:sentimental` (local fallback, limited feature set)
73
+
74
+ ---
75
+
76
+ ## Usage
77
+
78
+ Data entries should be hashes with at least an `:answer` key. Optionally include segmentation info under `:segment`.
79
+
80
+ ```ruby
81
+ entries = [
82
+ { answer: "Amazon Checkout was smooth!", segment: { age_group: "18-25", gender: "Female" } },
83
+ { answer: "Walmart Shipping was delayed.", segment: { age_group: "18-25", gender: "Female" } },
84
+ { answer: "Target Support was decent.", segment: { age_group: "26-35", gender: "Male" } },
85
+ { answer: "Loved the product!", segment: { age_group: "18-25", gender: "Male" } }
86
+ ]
87
+ ```
88
+
89
+ ---
90
+
91
+ ### Sentiment Analysis
10
92
 
11
93
  Quickly classify and summarize user responses as positive, neutral, or negative — globally or by segment (e.g., age, region).
12
94
 
@@ -17,6 +99,34 @@ insight = SentimentInsights::Insights::Sentiment.new
17
99
  result = insight.analyze(entries)
18
100
  ```
19
101
 
102
+ With options:
103
+
104
+ ```ruby
105
+ custom_prompt = <<~PROMPT
106
+ For each of the following customer responses, classify the sentiment as Positive, Neutral, or Negative, and assign a score between -1.0 (very negative) and 1.0 (very positive).
107
+
108
+ Reply with a numbered list like:
109
+ 1. Positive (0.9)
110
+ 2. Negative (-0.8)
111
+ 3. Neutral (0.0)
112
+ PROMPT
113
+
114
+ insight = SentimentInsights::Insights::Sentiment.new
115
+ result = insight.analyze(
116
+ entries,
117
+ question: "How was your experience today?",
118
+ prompt: custom_prompt,
119
+ batch_size: 10
120
+ )
121
+ ```
122
+
123
+ #### Available Options (`analyze`)
124
+ | Option | Type | Description | Provider |
125
+ |---------------|---------|------------------------------------------------------------------------|-------------|
126
+ | `question` | String | Contextual question for the batch | OpenAI only |
127
+ | `prompt` | String | Custom prompt text for LLM | OpenAI only |
128
+ | `batch_size` | Integer | Number of entries per OpenAI completion call (default: 50) | OpenAI only |
129
+
20
130
  #### 📾 Sample Output
21
131
 
22
132
  ```ruby
@@ -59,15 +169,56 @@ result = insight.analyze(entries)
59
169
  :sentiment_score=>0.9}]}}
60
170
  ```
61
171
 
62
- ### ✅ 2. Key Phrase Extraction
172
+ ---
173
+
174
+ ### Key Phrase Extraction
63
175
 
64
176
  Extract frequently mentioned phrases and identify their associated sentiment and segment spread.
65
177
 
66
178
  ```ruby
67
179
  insight = SentimentInsights::Insights::KeyPhrases.new
68
- result = insight.extract(entries, question: question)
180
+ result = insight.extract(entries)
69
181
  ```
70
182
 
183
+ With options:
184
+
185
+ ```ruby
186
+ key_phrase_prompt = <<~PROMPT.strip
187
+ Extract the most important key phrases that represent the main ideas or feedback in the sentence below.
188
+ Ignore stop words and return each key phrase in its natural form, comma-separated.
189
+
190
+ Question: %{question}
191
+
192
+ Text: %{text}
193
+ PROMPT
194
+
195
+ sentiment_prompt = <<~PROMPT
196
+ For each of the following customer responses, classify the sentiment as Positive, Neutral, or Negative, and assign a score between -1.0 (very negative) and 1.0 (very positive).
197
+
198
+ Reply with a numbered list like:
199
+ 1. Positive (0.9)
200
+ 2. Negative (-0.8)
201
+ 3. Neutral (0.0)
202
+ PROMPT
203
+
204
+ insight = SentimentInsights::Insights::KeyPhrases.new
205
+ result = insight.extract(
206
+ entries,
207
+ question: "What are the recurring themes?",
208
+ key_phrase_prompt: key_phrase_prompt,
209
+ sentiment_prompt: sentiment_prompt
210
+ )
211
+ ```
212
+
213
+ #### Available Options (`extract`)
214
+ | Option | Type | Description | Provider |
215
+ |--------------------|---------|------------------------------------------------------------|--------------|
216
+ | `question` | String | Context question to help guide phrase extraction | OpenAI only |
217
+ | `key_phrase_prompt`| String | Custom prompt for extracting key phrases | OpenAI only |
218
+ | `sentiment_prompt` | String | Custom prompt for classifying tone of extracted phrases | OpenAI only |
219
+
220
+ #### 📾 Sample Output
221
+
71
222
  ```ruby
72
223
  {:phrases=>
73
224
  [{:phrase=>"everlane",
@@ -85,15 +236,44 @@ result = insight.extract(entries, question: question)
85
236
  :segment=>{:age=>"25-34", :region=>"West"}}]}
86
237
  ```
87
238
 
88
- ### ✅ 3. Entity Recognition
239
+ ---
89
240
 
90
- Identify named entities like organizations, products, and people, and track them by sentiment and segment.
241
+ ### Entity Extraction
91
242
 
92
243
  ```ruby
93
244
  insight = SentimentInsights::Insights::Entities.new
94
- result = insight.extract(entries, question: question)
245
+ result = insight.extract(entries)
95
246
  ```
96
247
 
248
+ With options:
249
+
250
+ ```ruby
251
+ entity_prompt = <<~PROMPT.strip
252
+ Identify brand names, competitors, and product references in the sentence below.
253
+ Return each as a JSON object with "text" and "type" (e.g., BRAND, PRODUCT, COMPANY).
254
+
255
+ Question: %{question}
256
+
257
+ Sentence: "%{text}"
258
+ PROMPT
259
+
260
+ insight = SentimentInsights::Insights::Entities.new
261
+ result = insight.extract(
262
+ entries,
263
+ question: "Which products or brands are mentioned?",
264
+ prompt: entity_prompt
265
+ )
266
+
267
+ ```
268
+
269
+ #### Available Options (`extract`)
270
+ | Option | Type | Description | Provider |
271
+ |-------------|---------|---------------------------------------------------|--------------|
272
+ | `question` | String | Context question to guide entity extraction | OpenAI only |
273
+ | `prompt` | String | Custom instructions for OpenAI entity extraction | OpenAI only |
274
+
275
+ #### 📾 Sample Output
276
+
97
277
  ```ruby
98
278
  {:entities=>
99
279
  [{:entity=>"everlane",
@@ -126,73 +306,12 @@ result = insight.extract(entries, question: question)
126
306
  "the response was copy-paste and didn't address my issue directly.",
127
307
  :segment=>{:age=>"45-54", :region=>"Midwest"}}]}
128
308
  ```
129
-
130
- ### ✅ 4. Topic Modeling *(Coming Soon)*
131
-
132
- Automatically group similar responses into topics and subthemes.
133
-
134
- ---
135
-
136
- ## 🔌 Supported Providers
137
-
138
- | Feature | OpenAI ✅ | AWS Comprehend ✅ | Sentimental (Local) ⚠️ |
139
- | ------------------ | -------------- | ---------------- | ---------------------- |
140
- | Sentiment Analysis | ✅ | ✅ | ✅ |
141
- | Key Phrases | ✅ | ✅ | ❌ Not supported |
142
- | Entities | ✅ | ✅ | ❌ Not supported |
143
- | Topics | 🔜 Coming Soon | 🔜 Coming Soon | ❌ |
144
-
145
- Legend: ✅ Supported | 🔜 Coming Soon | ❌ Not Available | ⚠️ Partial
146
-
147
- ---
148
-
149
- ## 📅 Example Input
150
-
151
- ```ruby
152
- question = "What did you like or dislike about your recent shopping experience with us?"
153
-
154
- entries = [
155
- {
156
- answer: "I absolutely loved the experience shopping with Everlane. The website is clean,\nproduct descriptions are spot-on, and my jeans arrived two days early with eco-friendly packaging.",
157
- segment: { age: "25-34", region: "West" }
158
- },
159
- {
160
- answer: "The checkout flow on your site was a nightmare. The promo code from your Instagram campaign didn’t work,\nand it kept redirecting me to the homepage. Shopify integration needs a serious fix.",
161
- segment: { age: "35-44", region: "South" }
162
- },
163
- {
164
- answer: "Apple Pay made the mobile checkout super fast. I placed an order while waiting for my coffee at Starbucks.\nGreat job optimizing the app UX—this is a game-changer.",
165
- segment: { age: "25-34", region: "West" }
166
- },
167
- {
168
- answer: "I reached out to your Zendesk support team about a missing package, and while they responded within 24 hours,\nthe response was copy-paste and didn't address my issue directly.",
169
- segment: { age: "45-54", region: "Midwest" }
170
- },
171
- {
172
- answer: "Shipping delays aside, I really liked the personalized note inside the box. Small gestures like that\nmake the Uniqlo brand stand out. Will definitely recommend to friends.",
173
- segment: { age: "25-34", region: "West" }
174
- }
175
- ]
176
- ```
177
-
178
309
  ---
179
310
 
180
- ## 🚀 Quick Start
311
+ ## Provider Options & Custom Prompts
181
312
 
182
- ```ruby
183
- # Install the gem
184
- $ gem install sentiment_insights
185
-
186
- # Configure the provider
187
- SentimentInsights.configure do |config|
188
- config.provider = :openai # or :aws, :sentimental
189
- end
190
-
191
- # Run analysis
192
- insight = SentimentInsights::Insights::Sentiment.new
193
- result = insight.analyze(entries)
194
- puts JSON.pretty_generate(result)
195
- ```
313
+ > ⚠️ All advanced options (`question`, `prompt`, `key_phrase_prompt`, `sentiment_prompt`, `batch_size`) apply only to the `:openai` provider.
314
+ > They are safely ignored for `:aws` and `:sentimental`.
196
315
 
197
316
  ---
198
317
 
@@ -217,7 +336,6 @@ AWS_REGION=us-east-1
217
336
  ## 💎 Ruby Compatibility
218
337
 
219
338
  - **Minimum Ruby version:** 2.7
220
- - Tested on: 2.7, 3.0, 3.1, 3.2
221
339
 
222
340
  ---
223
341
 
@@ -258,8 +376,3 @@ Pull requests welcome! Please open an issue to discuss major changes first.
258
376
  - [AWS Comprehend](https://docs.aws.amazon.com/comprehend/latest/dg/what-is.html)
259
377
  - [Sentimental Gem](https://github.com/7compass/sentimental)
260
378
 
261
- ---
262
-
263
- ## 📢 Questions?
264
-
265
- File an issue or reach out on [GitHub](https://github.com/your-repo)
@@ -12,7 +12,7 @@ module SentimentInsights
12
12
  @logger = Logger.new($stdout)
13
13
  end
14
14
 
15
- def extract_batch(entries, question: nil)
15
+ def extract_batch(entries, question: nil, prompt: nil)
16
16
  responses = []
17
17
  entity_map = Hash.new { |h, k| h[k] = [] }
18
18
 
@@ -17,7 +17,7 @@ module SentimentInsights
17
17
  @logger = Logger.new($stdout)
18
18
  end
19
19
 
20
- def extract_batch(entries, question: nil)
20
+ def extract_batch(entries, question: nil, prompt: nil)
21
21
  responses = []
22
22
  entity_map = Hash.new { |h, k| h[k] = [] }
23
23
 
@@ -26,7 +26,7 @@ module SentimentInsights
26
26
  next if sentence.empty?
27
27
 
28
28
  response_id = "r_#{index + 1}"
29
- entities = extract_entities_from_sentence(sentence)
29
+ entities = extract_entities_from_sentence(sentence, question: question, prompt: prompt)
30
30
 
31
31
  responses << {
32
32
  id: response_id,
@@ -35,6 +35,7 @@ module SentimentInsights
35
35
  }
36
36
 
37
37
  entities.each do |ent|
38
+ next if ent[:text].empty? || ent[:type].empty?
38
39
  key = [ent[:text].downcase, ent[:type]]
39
40
  entity_map[key] << response_id
40
41
  end
@@ -54,13 +55,29 @@ module SentimentInsights
54
55
 
55
56
  private
56
57
 
57
- def extract_entities_from_sentence(text)
58
- prompt = <<~PROMPT
59
- Extract named entities from this sentence. Return them as a JSON array with each item having "text" and "type" (e.g., PERSON, ORGANIZATION, LOCATION, PRODUCT).
60
- Sentence: "#{text}"
58
+ def extract_entities_from_sentence(text, question: nil, prompt: nil)
59
+ # Default prompt with interpolation placeholders
60
+ default_prompt = <<~PROMPT
61
+ Extract named entities from this sentence based on the question.
62
+ Return them as a JSON array with each item having "text" and "type" (e.g., PERSON, ORGANIZATION, LOCATION, PRODUCT).
63
+ %{question}
64
+ Sentence: "%{text}"
61
65
  PROMPT
62
66
 
63
- body = build_request_body(prompt)
67
+ # If a custom prompt is provided, interpolate %{text} and %{question} if present
68
+ if prompt
69
+ interpolated = prompt.dup
70
+ interpolated.gsub!('%{text}', text.to_s)
71
+ interpolated.gsub!('%{question}', question.to_s) if question
72
+ interpolated.gsub!('{text}', text.to_s)
73
+ interpolated.gsub!('{question}', question.to_s) if question
74
+ prompt_to_use = interpolated
75
+ else
76
+ question_line = question ? "Question: #{question}" : ""
77
+ prompt_to_use = default_prompt % { question: question_line, text: text }
78
+ end
79
+
80
+ body = build_request_body(prompt_to_use)
64
81
  response = post_openai(body)
65
82
 
66
83
  begin
@@ -12,7 +12,7 @@ module SentimentInsights
12
12
  @logger = Logger.new($stdout)
13
13
  end
14
14
 
15
- def extract_batch(entries, question: nil)
15
+ def extract_batch(entries, question: nil, key_phrase_prompt: nil, sentiment_prompt: nil)
16
16
  responses = []
17
17
  phrase_map = Hash.new { |h, k| h[k] = [] }
18
18
 
@@ -8,7 +8,7 @@ module SentimentInsights
8
8
  module Clients
9
9
  module KeyPhrases
10
10
  class OpenAIClient
11
- DEFAULT_MODEL = "gpt-3.5-turbo"
11
+ DEFAULT_MODEL = "gpt-3.5-turbo"
12
12
  DEFAULT_RETRIES = 3
13
13
 
14
14
  def initialize(api_key: ENV['OPENAI_API_KEY'], model: DEFAULT_MODEL, max_retries: DEFAULT_RETRIES)
@@ -20,19 +20,19 @@ module SentimentInsights
20
20
  end
21
21
 
22
22
  # Extract key phrases from entries and enrich with sentiment
23
- def extract_batch(entries, question: nil)
23
+ def extract_batch(entries, question: nil, key_phrase_prompt: nil, sentiment_prompt: nil)
24
24
  responses = []
25
25
  phrase_map = Hash.new { |h, k| h[k] = [] }
26
26
 
27
27
  # Fetch sentiments in batch from sentiment client
28
- sentiments = @sentiment_client.analyze_entries(entries, question: question)
28
+ sentiments = @sentiment_client.analyze_entries(entries, question: question, prompt: sentiment_prompt)
29
29
 
30
30
  entries.each_with_index do |entry, index|
31
31
  sentence = entry[:answer].to_s.strip
32
32
  next if sentence.empty?
33
33
 
34
34
  response_id = "r_#{index + 1}"
35
- phrases = extract_phrases_from_sentence(sentence)
35
+ phrases = extract_phrases_from_sentence(sentence, question: question, prompt: key_phrase_prompt)
36
36
 
37
37
  sentiment = sentiments[index] || { label: :neutral }
38
38
 
@@ -61,15 +61,33 @@ module SentimentInsights
61
61
 
62
62
  private
63
63
 
64
- def extract_phrases_from_sentence(text)
65
- prompt = <<~PROMPT
66
- Extract the key phrases from this sentence:
67
- "#{text}"
68
- Return them as a comma-separated list.
64
+ def extract_phrases_from_sentence(text, question: nil, prompt: nil)
65
+ # Default prompt with interpolation placeholders
66
+ default_prompt = <<~PROMPT
67
+ Extract the most important key phrases that represent the main ideas or feedback in the sentence below.
68
+ Ignore stop words and return each key phrase in its natural form, comma-separated.
69
+ %{question}
70
+ Sentence: "%{text}"
69
71
  PROMPT
70
72
 
71
- body = build_request_body(prompt)
73
+ # If a custom prompt is provided, attempt to interpolate %{text} and %{question} if present
74
+ if prompt
75
+ interpolated = prompt.dup
76
+ interpolated.gsub!('%{text}', text.to_s)
77
+ interpolated.gsub!('%{question}', question.to_s) if question
78
+ # For compatibility: if “{text}” is used instead of “%{text}”
79
+ interpolated.gsub!('{text}', text.to_s)
80
+ interpolated.gsub!('{question}', question.to_s) if question
81
+ prompt_to_use = interpolated
82
+ else
83
+ question_line = question ? "Question: #{question}" : ""
84
+ prompt_to_use = default_prompt % { question: question_line, text: text }
85
+ end
86
+
87
+ body = build_request_body(prompt_to_use)
72
88
  response = post_openai(body)
89
+
90
+ # The response is expected as a comma- or newline-separated list of key phrases
73
91
  parse_phrases(response)
74
92
  end
75
93
 
@@ -15,7 +15,7 @@ module SentimentInsights
15
15
  # Analyze a batch of entries using AWS Comprehend.
16
16
  # @param entries [Array<Hash>] each with :answer key
17
17
  # @return [Array<Hash>] each with :label (symbol) and :score (float)
18
- def analyze_entries(entries, question: nil)
18
+ def analyze_entries(entries, question: nil, prompt: nil, batch_size: nil)
19
19
  results = []
20
20
 
21
21
  entries.each_slice(MAX_BATCH_SIZE) do |batch|
@@ -7,7 +7,7 @@ module SentimentInsights
7
7
  module Clients
8
8
  module Sentiment
9
9
  class OpenAIClient
10
- DEFAULT_MODEL = "gpt-3.5-turbo"
10
+ DEFAULT_MODEL = "gpt-3.5-turbo"
11
11
  DEFAULT_RETRIES = 3
12
12
 
13
13
  def initialize(api_key: ENV['OPENAI_API_KEY'], model: DEFAULT_MODEL, max_retries: DEFAULT_RETRIES, return_scores: true)
@@ -18,75 +18,85 @@ module SentimentInsights
18
18
  @logger = Logger.new($stdout)
19
19
  end
20
20
 
21
- def analyze_entries(entries, question: nil)
22
- prompt_content = build_prompt_content(entries, question)
23
- request_body = {
24
- model: @model,
25
- messages: [
26
- { role: "user", content: prompt_content }
27
- ],
28
- temperature: 0.0
29
- }
30
-
31
- uri = URI("https://api.openai.com/v1/chat/completions")
32
- http = Net::HTTP.new(uri.host, uri.port)
33
- http.use_ssl = true
34
-
35
- response_content = nil
36
- attempt = 0
37
-
38
- while attempt < @max_retries
39
- attempt += 1
40
- request = Net::HTTP::Post.new(uri)
41
- request["Content-Type"] = "application/json"
42
- request["Authorization"] = "Bearer #{@api_key}"
43
- request.body = JSON.generate(request_body)
44
-
45
- begin
46
- response = http.request(request)
47
- rescue StandardError => e
48
- @logger.error "OpenAI API request error: #{e.class} - #{e.message}"
49
- raise
21
+ def analyze_entries(entries, question: nil, prompt: nil, batch_size: 50)
22
+ all_sentiments = []
23
+
24
+ entries.each_slice(batch_size) do |batch|
25
+ prompt_content = build_prompt_content(batch, question: question, prompt: prompt)
26
+ request_body = {
27
+ model: @model,
28
+ messages: [
29
+ { role: "user", content: prompt_content }
30
+ ],
31
+ temperature: 0.0
32
+ }
33
+
34
+ uri = URI("https://api.openai.com/v1/chat/completions")
35
+ http = Net::HTTP.new(uri.host, uri.port)
36
+ http.use_ssl = true
37
+
38
+ response_content = nil
39
+ attempt = 0
40
+
41
+ while attempt < @max_retries
42
+ attempt += 1
43
+ request = Net::HTTP::Post.new(uri)
44
+ request["Content-Type"] = "application/json"
45
+ request["Authorization"] = "Bearer #{@api_key}"
46
+ request.body = JSON.generate(request_body)
47
+
48
+ begin
49
+ response = http.request(request)
50
+ rescue StandardError => e
51
+ @logger.error "OpenAI API request error: #{e.class} - #{e.message}"
52
+ raise
53
+ end
54
+
55
+ status = response.code.to_i
56
+ if status == 429
57
+ @logger.warn "Rate limit (HTTP 429) on attempt #{attempt}. Retrying..."
58
+ sleep(2 ** (attempt - 1))
59
+ next
60
+ elsif status != 200
61
+ @logger.error "Request failed (#{status}): #{response.body}"
62
+ raise "OpenAI API Error: #{status}"
63
+ else
64
+ data = JSON.parse(response.body)
65
+ response_content = data.dig("choices", 0, "message", "content")
66
+ break
67
+ end
50
68
  end
51
69
 
52
- status = response.code.to_i
53
- if status == 429
54
- @logger.warn "Rate limit (HTTP 429) on attempt #{attempt}. Retrying..."
55
- sleep(2 ** (attempt - 1))
56
- next
57
- elsif status != 200
58
- @logger.error "Request failed (#{status}): #{response.body}"
59
- raise "OpenAI API Error: #{status}"
60
- else
61
- data = JSON.parse(response.body)
62
- response_content = data.dig("choices", 0, "message", "content")
63
- break
64
- end
70
+ sentiments = parse_sentiments(response_content, batch.size)
71
+ all_sentiments.concat(sentiments)
65
72
  end
66
73
 
67
- parse_sentiments(response_content, entries.size)
74
+ all_sentiments
68
75
  end
69
76
 
70
77
  private
71
78
 
72
- def build_prompt_content(entries, question)
73
- prompt = ""
74
- prompt << "Question: #{question}\n" if question
75
- prompt << <<~INSTRUCTIONS
76
- For each of the following customer responses, classify the sentiment as Positive, Neutral, or Negative, and assign a score between -1.0 (very negative) and 1.0 (very positive).
79
+ def build_prompt_content(entries, question: nil, prompt: nil)
80
+ content = ""
81
+ content << "Question: #{question}\n\n" if question
82
+
83
+ # Use custom instructions or default
84
+ instructions = prompt || <<~DEFAULT
85
+ For each of the following customer responses, classify the sentiment as Positive, Neutral, or Negative, and assign a score between -1.0 (very negative) and 1.0 (very positive).
77
86
 
78
- Reply with a numbered list like:
79
- 1. Positive (0.9)
80
- 2. Negative (-0.8)
81
- 3. Neutral (0.0)
87
+ Reply with a numbered list like:
88
+ 1. Positive (0.9)
89
+ 2. Negative (-0.8)
90
+ 3. Neutral (0.0)
91
+ DEFAULT
82
92
 
83
- INSTRUCTIONS
93
+ content << instructions.strip + "\n\n"
84
94
 
85
95
  entries.each_with_index do |entry, index|
86
- prompt << "#{index + 1}. \"#{entry[:answer]}\"\n"
96
+ content << "#{index + 1}. \"#{entry[:answer]}\"\n"
87
97
  end
88
98
 
89
- prompt
99
+ content
90
100
  end
91
101
 
92
102
  def parse_sentiments(content, expected_count)
@@ -14,7 +14,7 @@ module SentimentInsights
14
14
  # @param entries [Array<Hash>] An array of response hashes (each with :answer).
15
15
  # @param question [String, nil] (unused) Global question context, not needed for local analysis.
16
16
  # @return [Array<Hash>] An array of hashes with sentiment classification and score for each entry.
17
- def analyze_entries(entries, question: nil)
17
+ def analyze_entries(entries, question: nil, prompt: nil, batch_size: nil)
18
18
  puts "Inside sentimental"
19
19
  entries.map do |entry|
20
20
  text = entry[:answer].to_s.strip
@@ -22,11 +22,10 @@ module SentimentInsights
22
22
  # Extract named entities and build summarized output
23
23
  # @param entries [Array<Hash>] each with :answer and optional :segment
24
24
  # @return [Hash] { entities: [...], responses: [...] }
25
- def extract(entries, question: nil)
25
+ def extract(entries, question: nil, prompt: nil)
26
26
  entries = entries.to_a
27
- raw_result = @provider_client.extract_batch(entries, question: question)
27
+ raw_result = @provider_client.extract_batch(entries, question: question, prompt: prompt)
28
28
 
29
- puts "raw_result = #{raw_result}"
30
29
  responses = raw_result[:responses] || []
31
30
  entities = raw_result[:entities] || []
32
31
 
@@ -24,9 +24,9 @@ module SentimentInsights
24
24
  # @param entries [Array<Hash>] each with :answer and optional :segment
25
25
  # @param question [String, nil] optional context
26
26
  # @return [Hash] { phrases: [...], responses: [...] }
27
- def extract(entries, question: nil)
27
+ def extract(entries, question: nil, key_phrase_prompt: nil, sentiment_prompt: nil)
28
28
  entries = entries.to_a
29
- raw_result = @provider_client.extract_batch(entries, question: question)
29
+ raw_result = @provider_client.extract_batch(entries, question: question, key_phrase_prompt: key_phrase_prompt, sentiment_prompt: sentiment_prompt)
30
30
 
31
31
  responses = raw_result[:responses] || []
32
32
  phrases = raw_result[:phrases] || []
@@ -77,4 +77,4 @@ module SentimentInsights
77
77
  end
78
78
  end
79
79
  end
80
- end
80
+ end
@@ -1,5 +1,6 @@
1
1
  require_relative '../clients/sentiment/open_ai_client'
2
2
  require_relative '../clients/sentiment/sentimental_client'
3
+ require_relative '../clients/sentiment/aws_comprehend_client'
3
4
 
4
5
  module SentimentInsights
5
6
  module Insights
@@ -15,7 +16,6 @@ module SentimentInsights
15
16
  when :openai
16
17
  Clients::Sentiment::OpenAIClient.new
17
18
  when :aws
18
- require_relative '../clients/sentiment/aws_comprehend_client'
19
19
  Clients::Sentiment::AwsComprehendClient.new
20
20
  else
21
21
  Clients::Sentiment::SentimentalClient.new
@@ -27,11 +27,11 @@ module SentimentInsights
27
27
  # @param entries [Array<Hash>] An array of response hashes, each with :answer and :segment.
28
28
  # @param question [String, nil] Optional global question text or metadata for context.
29
29
  # @return [Hash] Summary of sentiment analysis (global, segment-wise, top comments, and annotated responses).
30
- def analyze(entries, question: nil)
30
+ def analyze(entries, question: nil, prompt: nil, batch_size: 50)
31
31
  # Ensure entries is an array of hashes with required keys
32
32
  entries = entries.to_a
33
33
  # Get sentiment results for each entry from the provider client
34
- results = @provider_client.analyze_entries(entries, question: question)
34
+ results = @provider_client.analyze_entries(entries, question: question, prompt: prompt, batch_size: batch_size)
35
35
 
36
36
  # Combine original entries with sentiment results
37
37
  annotated_responses = entries.each_with_index.map do |entry, idx|
@@ -1,3 +1,3 @@
1
1
  module SentimentInsights
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -17,8 +17,6 @@ Gem::Specification.new do |spec|
17
17
  if spec.respond_to?(:metadata)
18
18
  spec.metadata["homepage_uri"] = spec.homepage
19
19
  spec.metadata["source_code_uri"] = "https://github.com/mathrailsAI/sentiment_insights"
20
- spec.metadata["changelog_uri"] = "https://github.com/mathrailsAI/sentiment_insights/blob/main/CHANGELOG.md"
21
- # Removed allowed_push_host — usually not needed unless you have a private server
22
20
  else
23
21
  raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
24
22
  end
@@ -32,7 +30,7 @@ Gem::Specification.new do |spec|
32
30
 
33
31
  # Runtime dependencies
34
32
  spec.add_dependency "sentimental", "~> 1.4.0"
35
- spec.add_dependency "aws-sdk-comprehend", "~> 1.98.0"
33
+ spec.add_dependency "aws-sdk-comprehend", ">= 1.98.0"
36
34
 
37
35
  # Development dependencies
38
36
  spec.add_development_dependency "bundler", "~> 2.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sentiment_insights
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - mathrailsAI
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-05-03 00:00:00.000000000 Z
11
+ date: 2025-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sentimental
@@ -28,14 +28,14 @@ dependencies:
28
28
  name: aws-sdk-comprehend
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: 1.98.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: 1.98.0
41
41
  - !ruby/object:Gem::Dependency
@@ -136,7 +136,6 @@ licenses:
136
136
  metadata:
137
137
  homepage_uri: https://github.com/mathrailsAI/sentiment_insights
138
138
  source_code_uri: https://github.com/mathrailsAI/sentiment_insights
139
- changelog_uri: https://github.com/mathrailsAI/sentiment_insights/blob/main/CHANGELOG.md
140
139
  post_install_message:
141
140
  rdoc_options: []
142
141
  require_paths: