flow_chat 0.6.0 → 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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +44 -0
  3. data/.gitignore +2 -1
  4. data/README.md +84 -1229
  5. data/docs/configuration.md +337 -0
  6. data/docs/flows.md +320 -0
  7. data/docs/images/simulator.png +0 -0
  8. data/docs/instrumentation.md +216 -0
  9. data/docs/media.md +153 -0
  10. data/docs/testing.md +475 -0
  11. data/docs/ussd-setup.md +306 -0
  12. data/docs/whatsapp-setup.md +162 -0
  13. data/examples/multi_tenant_whatsapp_controller.rb +9 -37
  14. data/examples/simulator_controller.rb +9 -18
  15. data/examples/ussd_controller.rb +32 -38
  16. data/examples/whatsapp_controller.rb +32 -125
  17. data/examples/whatsapp_media_examples.rb +68 -336
  18. data/examples/whatsapp_message_job.rb +5 -3
  19. data/flow_chat.gemspec +6 -2
  20. data/lib/flow_chat/base_processor.rb +48 -2
  21. data/lib/flow_chat/config.rb +5 -0
  22. data/lib/flow_chat/context.rb +13 -1
  23. data/lib/flow_chat/instrumentation/log_subscriber.rb +176 -0
  24. data/lib/flow_chat/instrumentation/metrics_collector.rb +197 -0
  25. data/lib/flow_chat/instrumentation/setup.rb +155 -0
  26. data/lib/flow_chat/instrumentation.rb +70 -0
  27. data/lib/flow_chat/prompt.rb +20 -20
  28. data/lib/flow_chat/session/cache_session_store.rb +73 -7
  29. data/lib/flow_chat/session/middleware.rb +37 -4
  30. data/lib/flow_chat/session/rails_session_store.rb +36 -1
  31. data/lib/flow_chat/simulator/controller.rb +7 -7
  32. data/lib/flow_chat/ussd/app.rb +1 -1
  33. data/lib/flow_chat/ussd/gateway/nalo.rb +30 -0
  34. data/lib/flow_chat/ussd/gateway/nsano.rb +33 -0
  35. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +109 -0
  36. data/lib/flow_chat/ussd/middleware/executor.rb +24 -2
  37. data/lib/flow_chat/ussd/middleware/pagination.rb +87 -7
  38. data/lib/flow_chat/ussd/processor.rb +14 -0
  39. data/lib/flow_chat/ussd/renderer.rb +1 -1
  40. data/lib/flow_chat/version.rb +1 -1
  41. data/lib/flow_chat/whatsapp/app.rb +1 -1
  42. data/lib/flow_chat/whatsapp/client.rb +99 -12
  43. data/lib/flow_chat/whatsapp/configuration.rb +35 -4
  44. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +128 -54
  45. data/lib/flow_chat/whatsapp/middleware/executor.rb +24 -2
  46. data/lib/flow_chat/whatsapp/processor.rb +8 -0
  47. data/lib/flow_chat/whatsapp/renderer.rb +4 -9
  48. data/lib/flow_chat.rb +23 -0
  49. metadata +22 -11
  50. data/.travis.yml +0 -6
  51. data/app/controllers/demo_controller.rb +0 -101
  52. data/app/flow_chat/demo_restaurant_flow.rb +0 -889
  53. data/config/routes_demo.rb +0 -59
  54. data/examples/initializer.rb +0 -86
  55. data/examples/media_prompts_examples.rb +0 -27
  56. data/images/ussd_simulator.png +0 -0
@@ -25,17 +25,17 @@ class WelcomeFlow < FlowChat::Flow
25
25
  def main_page
26
26
  # Welcome the user
27
27
  name = app.screen(:name) do |prompt|
28
- prompt.ask "Welcome to our service! What's your name?",
28
+ prompt.ask "Welcome! What's your name?",
29
29
  transform: ->(input) { input.strip.titleize }
30
30
  end
31
31
 
32
32
  # Show main menu with numbered options (USSD style)
33
33
  choice = app.screen(:main_menu) do |prompt|
34
- prompt.select "Hi #{name}! Choose an option:", {
34
+ prompt.select "Hi #{name}! Choose:", {
35
35
  "1" => "Account Info",
36
36
  "2" => "Make Payment",
37
37
  "3" => "Get Balance",
38
- "4" => "Customer Support"
38
+ "4" => "Support"
39
39
  }
40
40
  end
41
41
 
@@ -55,54 +55,55 @@ class WelcomeFlow < FlowChat::Flow
55
55
 
56
56
  def show_account_info
57
57
  info_choice = app.screen(:account_info) do |prompt|
58
- prompt.select "Account Information:", {
58
+ prompt.select "Account Info:", {
59
59
  "1" => "Personal Details",
60
- "2" => "Account Balance",
60
+ "2" => "Balance",
61
61
  "3" => "Transaction History",
62
- "0" => "Back to Main Menu"
62
+ "0" => "Main Menu"
63
63
  }
64
64
  end
65
65
 
66
66
  case info_choice
67
67
  when "1"
68
- app.say "Name: John Doe\\nPhone: #{app.phone_number}\\nAccount: Active"
68
+ app.say "Name: John Doe\nPhone: #{app.phone_number}\nStatus: Active"
69
69
  when "2"
70
- app.say "Current Balance: $150.75\\nAvailable Credit: $1,000.00"
70
+ app.say "Balance: $150.75\nCredit: $1,000.00"
71
71
  when "3"
72
- app.say "Last 3 Transactions:\\n1. +$50.00 - Deposit\\n2. -$25.50 - Purchase\\n3. -$15.00 - Transfer"
72
+ app.say "Recent:\n+$50.00 Deposit\n-$25.50 Purchase\n-$15.00 Transfer"
73
73
  when "0"
74
- main_page # Go back to main menu
74
+ main_page
75
75
  end
76
76
  end
77
77
 
78
78
  def make_payment
79
79
  amount = app.screen(:payment_amount) do |prompt|
80
- prompt.ask "Enter amount to pay:",
81
- convert: ->(input) { input.to_f },
82
- validate: ->(amount) {
83
- return "Amount must be greater than 0" unless amount > 0
84
- return "Maximum payment is $500" unless amount <= 500
80
+ prompt.ask "Enter amount:",
81
+ validate: ->(input) {
82
+ amt = input.to_f
83
+ return "Invalid amount" unless amt > 0
84
+ return "Max $500" unless amt <= 500
85
85
  nil
86
- }
86
+ },
87
+ transform: ->(input) { input.to_f }
87
88
  end
88
89
 
89
90
  recipient = app.screen(:payment_recipient) do |prompt|
90
- prompt.ask "Enter recipient phone number:",
91
+ prompt.ask "Recipient phone:",
91
92
  validate: ->(input) {
92
- return "Phone number must be 10 digits" unless input.match?(/\\A\\d{10}\\z/)
93
+ return "10 digits required" unless input.match?(/\A\d{10}\z/)
93
94
  nil
94
95
  }
95
96
  end
96
97
 
97
98
  # Confirmation screen
98
99
  confirmed = app.screen(:payment_confirmation) do |prompt|
99
- prompt.yes? "Pay $#{amount} to #{recipient}?\\nConfirm payment?"
100
+ prompt.yes? "Pay $#{amount} to #{recipient}?"
100
101
  end
101
102
 
102
103
  if confirmed
103
104
  # Process payment (your business logic here)
104
105
  transaction_id = process_payment(amount, recipient)
105
- app.say "Payment successful!\\nTransaction ID: #{transaction_id}\\nAmount: $#{amount}\\nTo: #{recipient}"
106
+ app.say "Payment successful!\nID: #{transaction_id}\nAmount: $#{amount}"
106
107
  else
107
108
  app.say "Payment cancelled"
108
109
  end
@@ -111,16 +112,14 @@ class WelcomeFlow < FlowChat::Flow
111
112
  def get_balance
112
113
  # Simulate balance check
113
114
  balance = check_account_balance(app.phone_number)
114
- app.say "Account Balance\\n\\nAvailable: $#{balance[:available]}\\nPending: $#{balance[:pending]}\\nTotal: $#{balance[:total]}"
115
+ app.say "Balance\n\nAvailable: $#{balance[:available]}\nPending: $#{balance[:pending]}"
115
116
  end
116
117
 
117
118
  def customer_support
118
119
  support_choice = app.screen(:support_menu) do |prompt|
119
- prompt.select "Customer Support:", {
120
- "1" => "Report an Issue",
121
- "2" => "Account Questions",
122
- "3" => "Technical Support",
123
- "4" => "Speak to Agent",
120
+ prompt.select "Support:", {
121
+ "1" => "Report Issue",
122
+ "2" => "Contact Info",
124
123
  "0" => "Main Menu"
125
124
  }
126
125
  end
@@ -129,11 +128,7 @@ class WelcomeFlow < FlowChat::Flow
129
128
  when "1"
130
129
  report_issue
131
130
  when "2"
132
- app.say "For account questions:\\nCall: 123-456-7890\\nEmail: support@company.com\\nHours: 9AM-5PM Mon-Fri"
133
- when "3"
134
- app.say "Technical Support:\\nCall: 123-456-7891\\nEmail: tech@company.com\\n24/7 Support Available"
135
- when "4"
136
- app.say "Connecting you to an agent...\\nPlease call 123-456-7890\\nOr visit our nearest branch"
131
+ app.say "Support:\nCall: 123-456-7890\nEmail: support@company.com\nHours: 9AM-5PM"
137
132
  when "0"
138
133
  main_page
139
134
  end
@@ -141,7 +136,7 @@ class WelcomeFlow < FlowChat::Flow
141
136
 
142
137
  def report_issue
143
138
  issue_type = app.screen(:issue_type) do |prompt|
144
- prompt.select "Select issue type:", {
139
+ prompt.select "Issue type:", {
145
140
  "1" => "Payment Problem",
146
141
  "2" => "Account Access",
147
142
  "3" => "Service Error",
@@ -150,9 +145,9 @@ class WelcomeFlow < FlowChat::Flow
150
145
  end
151
146
 
152
147
  description = app.screen(:issue_description) do |prompt|
153
- prompt.ask "Briefly describe the issue:",
148
+ prompt.ask "Describe issue:",
154
149
  validate: ->(input) {
155
- return "Description must be at least 10 characters" unless input.length >= 10
150
+ return "Min 10 characters" if input.length < 10
156
151
  nil
157
152
  }
158
153
  end
@@ -160,7 +155,7 @@ class WelcomeFlow < FlowChat::Flow
160
155
  # Save the issue (your business logic here)
161
156
  ticket_id = create_support_ticket(issue_type, description, app.phone_number)
162
157
 
163
- app.say "Issue reported successfully!\\n\\nTicket ID: #{ticket_id}\\nWe'll contact you within 24 hours.\\n\\nThank you!"
158
+ app.say "Issue reported!\n\nTicket: #{ticket_id}\nWe'll contact you within 24hrs"
164
159
  end
165
160
 
166
161
  # Helper methods (implement your business logic)
@@ -175,14 +170,13 @@ class WelcomeFlow < FlowChat::Flow
175
170
  # Your balance checking logic here
176
171
  {
177
172
  available: "150.75",
178
- pending: "25.00",
179
- total: "175.75"
173
+ pending: "25.00"
180
174
  }
181
175
  end
182
176
 
183
177
  def create_support_ticket(issue_type, description, phone_number)
184
178
  # Your ticket creation logic here
185
- Rails.logger.info "Support ticket created: #{issue_type} - #{description} from #{phone_number}"
179
+ Rails.logger.info "Ticket: #{issue_type} - #{description} from #{phone_number}"
186
180
  "TICKET#{rand(10000..99999)}"
187
181
  end
188
182
  end
@@ -1,82 +1,38 @@
1
1
  # Example WhatsApp Controller
2
2
  # Add this to your Rails application as app/controllers/whatsapp_controller.rb
3
3
 
4
+ # Basic WhatsApp controller using Rails credentials
4
5
  class WhatsappController < ApplicationController
5
6
  skip_forgery_protection
6
7
 
7
8
  def webhook
8
- # Enable simulator mode for local endpoint testing in development
9
9
  processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: Rails.env.development?) do |config|
10
10
  config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
11
- # Use cache-based session store for longer WhatsApp conversations
12
11
  config.use_session_store FlowChat::Session::CacheSessionStore
13
12
  end
14
13
 
15
14
  processor.run WelcomeFlow, :main_page
16
- rescue FlowChat::Whatsapp::ConfigurationError => e
17
- Rails.logger.error "WhatsApp configuration error: #{e.message}"
18
- head :internal_server_error
19
15
  rescue => e
20
- Rails.logger.error "Unexpected error processing WhatsApp webhook: #{e.message}"
16
+ Rails.logger.error "Error processing WhatsApp webhook: #{e.message}"
21
17
  head :internal_server_error
22
18
  end
23
19
  end
24
20
 
25
- # Example with Custom Configuration and Security
21
+ # Controller with custom configuration
26
22
  class CustomWhatsappController < ApplicationController
27
23
  skip_forgery_protection
28
24
 
29
25
  def webhook
30
- # Create custom WhatsApp configuration for this endpoint
31
- custom_config = FlowChat::Whatsapp::Configuration.new
32
- custom_config.access_token = ENV["MY_WHATSAPP_ACCESS_TOKEN"]
33
- custom_config.phone_number_id = ENV["MY_WHATSAPP_PHONE_NUMBER_ID"]
34
- custom_config.verify_token = ENV["MY_WHATSAPP_VERIFY_TOKEN"]
35
- custom_config.app_id = ENV["MY_WHATSAPP_APP_ID"]
36
- custom_config.app_secret = ENV["MY_WHATSAPP_APP_SECRET"]
37
- custom_config.business_account_id = ENV["MY_WHATSAPP_BUSINESS_ACCOUNT_ID"]
38
-
39
- # Security configuration
40
- custom_config.skip_signature_validation = !Rails.env.production? # Only skip in non-production
41
-
42
- # Enable simulator for local endpoint testing in non-production environments
43
- processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: !Rails.env.production?) do |config|
44
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, custom_config
45
- config.use_session_store FlowChat::Session::CacheSessionStore
46
- end
47
-
48
- processor.run WelcomeFlow, :main_page
49
- rescue FlowChat::Whatsapp::ConfigurationError => e
50
- Rails.logger.error "WhatsApp configuration error: #{e.message}"
51
- head :internal_server_error
52
- rescue => e
53
- Rails.logger.error "Unexpected error processing WhatsApp webhook: #{e.message}"
54
- head :internal_server_error
55
- end
56
- end
57
-
58
- # Example with Environment-Specific Security
59
- class EnvironmentAwareWhatsappController < ApplicationController
60
- skip_forgery_protection
61
-
62
- def webhook
63
- # Configure security based on environment
64
26
  custom_config = build_whatsapp_config
65
-
66
- # Enable simulator for local endpoint testing in development/staging
67
- enable_simulator = Rails.env.development? || Rails.env.staging?
68
27
 
69
- processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: enable_simulator) do |config|
28
+ processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: !Rails.env.production?) do |config|
70
29
  config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, custom_config
71
30
  config.use_session_store FlowChat::Session::CacheSessionStore
72
31
  end
73
32
 
74
33
  processor.run WelcomeFlow, :main_page
75
- rescue FlowChat::Whatsapp::ConfigurationError => e
76
- Rails.logger.error "WhatsApp configuration error: #{e.message}"
77
- head :internal_server_error
78
34
  rescue => e
79
- Rails.logger.error "Unexpected error processing WhatsApp webhook: #{e.message}"
35
+ Rails.logger.error "Error processing WhatsApp webhook: #{e.message}"
80
36
  head :internal_server_error
81
37
  end
82
38
 
@@ -84,63 +40,41 @@ class EnvironmentAwareWhatsappController < ApplicationController
84
40
 
85
41
  def build_whatsapp_config
86
42
  config = FlowChat::Whatsapp::Configuration.new
87
-
43
+
88
44
  case Rails.env
89
- when 'development'
90
- # Development: More relaxed security for easier testing
45
+ when "development", "test"
91
46
  config.access_token = ENV["WHATSAPP_ACCESS_TOKEN"]
92
47
  config.phone_number_id = ENV["WHATSAPP_PHONE_NUMBER_ID"]
93
48
  config.verify_token = ENV["WHATSAPP_VERIFY_TOKEN"]
94
- config.app_id = ENV["WHATSAPP_APP_ID"]
95
- config.app_secret = ENV["WHATSAPP_APP_SECRET"] # Optional in development
96
- config.business_account_id = ENV["WHATSAPP_BUSINESS_ACCOUNT_ID"]
97
- config.skip_signature_validation = true # Skip validation for easier development
98
-
99
- when 'test'
100
- # Test: Use test credentials
101
- config.access_token = "test_token"
102
- config.phone_number_id = "test_phone_id"
103
- config.verify_token = "test_verify_token"
104
- config.app_id = "test_app_id"
105
- config.app_secret = "test_app_secret"
106
- config.business_account_id = "test_business_id"
107
- config.skip_signature_validation = true # Skip validation in tests
108
-
109
- when 'staging', 'production'
110
- # Production: Full security enabled
49
+ config.app_secret = ENV["WHATSAPP_APP_SECRET"]
50
+ config.skip_signature_validation = true # Skip for easier development
51
+
52
+ when "staging", "production"
111
53
  config.access_token = ENV["WHATSAPP_ACCESS_TOKEN"]
112
54
  config.phone_number_id = ENV["WHATSAPP_PHONE_NUMBER_ID"]
113
55
  config.verify_token = ENV["WHATSAPP_VERIFY_TOKEN"]
114
- config.app_id = ENV["WHATSAPP_APP_ID"]
115
- config.app_secret = ENV["WHATSAPP_APP_SECRET"] # Required for security
116
- config.business_account_id = ENV["WHATSAPP_BUSINESS_ACCOUNT_ID"]
56
+ config.app_secret = ENV["WHATSAPP_APP_SECRET"]
117
57
  config.skip_signature_validation = false # Always validate in production
118
-
119
- # Ensure required security configuration is present
58
+
120
59
  if config.app_secret.blank?
121
- raise FlowChat::Whatsapp::ConfigurationError,
122
- "WHATSAPP_APP_SECRET is required for webhook signature validation in #{Rails.env}"
60
+ raise "WHATSAPP_APP_SECRET required for signature validation in #{Rails.env}"
123
61
  end
124
62
  end
125
-
63
+
126
64
  config
127
65
  end
128
66
  end
129
67
 
130
- # Example Flow for WhatsApp
131
- # Add this to your Rails application as app/flow_chat/welcome_flow.rb
132
-
68
+ # Example flow for WhatsApp
133
69
  class WelcomeFlow < FlowChat::Flow
134
70
  def main_page
135
- # Welcome the user
136
71
  name = app.screen(:name) do |prompt|
137
- prompt.ask "Hello! Welcome to our WhatsApp service. What's your name?",
72
+ prompt.ask "Hello! What's your name?",
138
73
  transform: ->(input) { input.strip.titleize }
139
74
  end
140
75
 
141
- # Show main menu
142
76
  choice = app.screen(:main_menu) do |prompt|
143
- prompt.select "Hi #{name}! What can I help you with today?", {
77
+ prompt.select "Hi #{name}! How can I help?", {
144
78
  "info" => "šŸ“‹ Get Information",
145
79
  "support" => "šŸ†˜ Contact Support",
146
80
  "feedback" => "šŸ’¬ Give Feedback"
@@ -149,7 +83,7 @@ class WelcomeFlow < FlowChat::Flow
149
83
 
150
84
  case choice
151
85
  when "info"
152
- show_information_menu
86
+ show_info
153
87
  when "support"
154
88
  contact_support
155
89
  when "feedback"
@@ -159,69 +93,42 @@ class WelcomeFlow < FlowChat::Flow
159
93
 
160
94
  private
161
95
 
162
- def show_information_menu
163
- info_choice = app.screen(:info_menu) do |prompt|
164
- prompt.select "What information do you need?", {
165
- "hours" => "šŸ•’ Business Hours",
166
- "location" => "šŸ“ Our Location",
167
- "services" => "šŸ›  Our Services"
168
- }
169
- end
170
-
171
- case info_choice
172
- when "hours"
173
- app.say "We're open Monday-Friday 9AM-6PM, Saturday 9AM-2PM. Closed Sundays."
174
- when "location"
175
- app.say "šŸ“ We're located at 123 Main Street, City, State 12345"
176
- when "services"
177
- app.say "Here are our main services:\\n\\n🌐 Web Development - Custom websites and applications\\nšŸ“± Mobile Apps - iOS and Android development\\nšŸ”§ Consulting - Technical consulting services"
178
- end
96
+ def show_info
97
+ app.say "šŸ“ Located at 123 Main Street\nšŸ•’ Hours: Mon-Fri 9AM-6PM\nšŸ“ž Call: (555) 123-4567"
179
98
  end
180
99
 
181
100
  def contact_support
182
- # Use standard select menu instead of send_buttons
183
- contact_method = app.screen(:contact_method) do |prompt|
184
- prompt.select "How would you like to contact support?", {
101
+ method = app.screen(:contact_method) do |prompt|
102
+ prompt.select "How would you like to contact us?", {
185
103
  "call" => "šŸ“ž Call Us",
186
- "email" => "šŸ“§ Email Us",
187
- "chat" => "šŸ’¬ Live Chat"
104
+ "email" => "šŸ“§ Email Us"
188
105
  }
189
106
  end
190
107
 
191
- case contact_method
108
+ case method
192
109
  when "call"
193
- app.say "šŸ“ž You can call us at (555) 123-4567"
110
+ app.say "šŸ“ž Call us at (555) 123-4567"
194
111
  when "email"
195
- app.say "šŸ“§ Send us an email at support@example.com"
196
- when "chat"
197
- app.say "šŸ’¬ Our live chat is available on our website: www.example.com"
112
+ app.say "šŸ“§ Email us at support@example.com"
198
113
  end
199
114
  end
200
115
 
201
116
  def collect_feedback
202
117
  rating = app.screen(:rating) do |prompt|
203
- prompt.select "How would you rate our service?", {
204
- "5" => "⭐⭐⭐⭐⭐ Excellent",
205
- "4" => "⭐⭐⭐⭐ Good",
206
- "3" => "⭐⭐⭐ Average",
207
- "2" => "⭐⭐ Poor",
208
- "1" => "⭐ Very Poor"
209
- }
118
+ prompt.select "Rate our service:", ["⭐", "⭐⭐", "⭐⭐⭐", "⭐⭐⭐⭐", "⭐⭐⭐⭐⭐"]
210
119
  end
211
120
 
212
121
  feedback = app.screen(:feedback_text) do |prompt|
213
- prompt.ask "Thank you for the #{rating}-star rating! Please share any additional feedback:"
122
+ prompt.ask "Any additional comments?"
214
123
  end
215
124
 
216
- # Save feedback (implement your logic here)
217
125
  save_feedback(app.phone_number, rating, feedback)
218
-
219
- app.say "Thank you for your feedback! We really appreciate it. šŸ™"
126
+ app.say "Thank you for your feedback! šŸ™"
220
127
  end
221
128
 
222
129
  def save_feedback(phone, rating, feedback)
223
- # Implement your feedback saving logic here
224
- Rails.logger.info "Feedback from #{phone}: #{rating} stars - #{feedback}"
130
+ Rails.logger.info "Feedback from #{phone}: #{rating} - #{feedback}"
131
+ # Add your feedback saving logic here
225
132
  end
226
133
  end
227
134