flow_chat 0.8.2 → 0.9.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/.cliff.toml +74 -0
  3. data/.github/workflows/ci.yml +2 -3
  4. data/.github/workflows/release.yml +56 -0
  5. data/.standard.yml +4 -0
  6. data/CHANGELOG.md +22 -0
  7. data/CLAUDE.md +327 -0
  8. data/CONTRIBUTING.md +134 -0
  9. data/Gemfile +1 -0
  10. data/README.md +290 -105
  11. data/Rakefile +5 -1
  12. data/SECURITY.md +42 -349
  13. data/docs/architecture.md +510 -0
  14. data/docs/async-background-processing.md +298 -0
  15. data/docs/configuration.md +556 -226
  16. data/docs/factory-pattern.md +355 -0
  17. data/docs/gateway-context-variables.md +171 -0
  18. data/docs/gateway-development.md +723 -0
  19. data/docs/getting-started.md +429 -0
  20. data/docs/instrumentation.md +264 -153
  21. data/docs/platforms/telegram.md +1013 -0
  22. data/docs/platforms/ussd.md +693 -0
  23. data/docs/platforms/whatsapp.md +1395 -0
  24. data/docs/testing.md +243 -365
  25. data/examples/custom_session_id_example.rb +119 -0
  26. data/examples/http_controller.rb +11 -11
  27. data/examples/intercom_configuration_example.rb +118 -0
  28. data/examples/intercom_controller.rb +194 -0
  29. data/examples/multi_tenant_whatsapp_controller.rb +4 -4
  30. data/examples/ussd_controller.rb +9 -9
  31. data/examples/whatsapp_controller.rb +2 -2
  32. data/flow_chat.gemspec +4 -0
  33. data/lib/flow_chat/{base_app.rb → app.rb} +16 -9
  34. data/lib/flow_chat/async_job.rb +176 -0
  35. data/lib/flow_chat/config.rb +10 -30
  36. data/lib/flow_chat/{base_executor.rb → executor.rb} +6 -11
  37. data/lib/flow_chat/factory.rb +94 -0
  38. data/lib/flow_chat/gateway_async_support.rb +106 -0
  39. data/lib/flow_chat/generic_async_job.rb +30 -0
  40. data/lib/flow_chat/http/gateway/simple.rb +81 -33
  41. data/lib/flow_chat/http/renderer.rb +3 -3
  42. data/lib/flow_chat/instrumentation/setup.rb +1 -1
  43. data/lib/flow_chat/instrumentation.rb +23 -0
  44. data/lib/flow_chat/intercom/client.rb +155 -0
  45. data/lib/flow_chat/intercom/configuration.rb +149 -0
  46. data/lib/flow_chat/intercom/gateway/intercom_api.rb +396 -0
  47. data/lib/flow_chat/intercom/renderer.rb +71 -0
  48. data/lib/flow_chat/phone_number_util.rb +37 -35
  49. data/lib/flow_chat/processor.rb +188 -0
  50. data/lib/flow_chat/renderers/markdown_support.rb +58 -0
  51. data/lib/flow_chat/session/middleware.rb +25 -9
  52. data/lib/flow_chat/telegram/client.rb +240 -0
  53. data/lib/flow_chat/telegram/configuration.rb +118 -0
  54. data/lib/flow_chat/telegram/gateway/bot_api.rb +300 -0
  55. data/lib/flow_chat/telegram/middleware/choice_mapper.rb +35 -0
  56. data/lib/flow_chat/telegram/renderer.rb +125 -0
  57. data/lib/flow_chat/telegram.rb +7 -0
  58. data/lib/flow_chat/ussd/gateway/nalo.rb +24 -4
  59. data/lib/flow_chat/ussd/middleware/pagination.rb +9 -5
  60. data/lib/flow_chat/ussd/renderer.rb +1 -1
  61. data/lib/flow_chat/version.rb +1 -1
  62. data/lib/flow_chat/whatsapp/client.rb +144 -13
  63. data/lib/flow_chat/whatsapp/configuration.rb +1 -1
  64. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +132 -96
  65. data/lib/flow_chat/whatsapp/id_generator.rb +124 -0
  66. data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +147 -0
  67. data/lib/flow_chat/whatsapp/renderer.rb +145 -11
  68. data/lib/flow_chat.rb +11 -1
  69. data/lib/tasks/release.rake +165 -0
  70. metadata +84 -25
  71. data/docs/flows.md +0 -320
  72. data/docs/http-gateway-protocol.md +0 -432
  73. data/docs/images/simulator.png +0 -0
  74. data/docs/media.md +0 -153
  75. data/docs/sessions.md +0 -433
  76. data/docs/ussd-setup.md +0 -322
  77. data/docs/whatsapp-setup.md +0 -162
  78. data/examples/whatsapp_message_job.rb +0 -113
  79. data/lib/flow_chat/base_processor.rb +0 -146
  80. data/lib/flow_chat/http/app.rb +0 -6
  81. data/lib/flow_chat/http/middleware/executor.rb +0 -24
  82. data/lib/flow_chat/http/processor.rb +0 -33
  83. data/lib/flow_chat/session/rails_session_store.rb +0 -68
  84. data/lib/flow_chat/ussd/app.rb +0 -6
  85. data/lib/flow_chat/ussd/gateway/nsano.rb +0 -96
  86. data/lib/flow_chat/ussd/middleware/executor.rb +0 -24
  87. data/lib/flow_chat/ussd/processor.rb +0 -39
  88. data/lib/flow_chat/whatsapp/app.rb +0 -29
  89. data/lib/flow_chat/whatsapp/middleware/executor.rb +0 -24
  90. data/lib/flow_chat/whatsapp/processor.rb +0 -32
  91. data/lib/flow_chat/whatsapp/send_job_support.rb +0 -79
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Example: Custom Session ID Configuration with Proc
4
+ #
5
+ # This example demonstrates how to use a custom proc for session ID generation
6
+ # in FlowChat applications. The proc allows complete customization of how
7
+ # session IDs are generated based on context data.
8
+
9
+ require "flow_chat"
10
+
11
+ class CustomSessionIdController < ApplicationController
12
+ def ussd_endpoint
13
+ processor = FlowChat::Processor.new(self) do |config|
14
+ config.use_gateway FlowChat::Ussd::Gateway::Nalo
15
+ config.use_session_store FlowChat::Session::CacheSessionStore
16
+
17
+ # Example 1: Custom session ID using block/proc
18
+ config.use_session_config do |context|
19
+ # Create a custom session ID based on your business logic
20
+ user_phone = context["request.msisdn"]
21
+ flow_name = context["flow.name"]
22
+ gateway = context["request.gateway"]
23
+ timestamp = Time.current.strftime("%Y%m%d")
24
+
25
+ # Custom format: flow_gateway_date_hashedphone
26
+ "#{flow_name}_#{gateway}_#{timestamp}_#{hash_phone(user_phone)}"
27
+ end
28
+ end
29
+
30
+ processor.run(SurveyFlow, :main_menu)
31
+ end
32
+
33
+ def whatsapp_endpoint
34
+ processor = FlowChat::Processor.new(self) do |config|
35
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
36
+ config.use_session_store FlowChat::Session::CacheSessionStore
37
+
38
+ # Example 2: Multi-tenant session IDs
39
+ config.use_session_config do |context|
40
+ tenant_id = extract_tenant_from_request(context)
41
+ user_id = context["request.user_id"] || context["request.msisdn"]
42
+ flow_name = context["flow.name"]
43
+
44
+ "tenant_#{tenant_id}_flow_#{flow_name}_user_#{hash_identifier(user_id)}"
45
+ end
46
+ end
47
+
48
+ processor.run(CustomerSupportFlow, :handle_inquiry)
49
+ end
50
+
51
+ def http_endpoint
52
+ processor = FlowChat::Processor.new(self) do |config|
53
+ config.use_gateway FlowChat::Http::Gateway::Simple
54
+ config.use_session_store FlowChat::Session::CacheSessionStore
55
+
56
+ # Example 3: API session with custom expiration tracking
57
+ config.use_session_config do |context|
58
+ api_key = context.controller.request.headers["X-API-Key"]
59
+ request_id = context["request.id"]
60
+
61
+ # Include API key hash for session isolation per API client
62
+ "api_#{hash_identifier(api_key)}_req_#{request_id}"
63
+ end
64
+ end
65
+
66
+ processor.run(ApiFlow, :handle_request)
67
+ end
68
+
69
+ private
70
+
71
+ def hash_phone(phone)
72
+ require "digest"
73
+ Digest::SHA256.hexdigest(phone)[0, 8]
74
+ end
75
+
76
+ def hash_identifier(identifier)
77
+ require "digest"
78
+ Digest::SHA256.hexdigest(identifier.to_s)[0, 8]
79
+ end
80
+
81
+ def extract_tenant_from_request(context)
82
+ # Extract tenant from subdomain or header
83
+ request = context.controller&.request
84
+ return "default" unless request
85
+
86
+ host = request.host
87
+ subdomain = host.split(".").first
88
+ subdomain if subdomain != "www"
89
+ end
90
+ end
91
+
92
+ # Example flows for demonstration
93
+ class SurveyFlow < FlowChat::Flow
94
+ def main_menu
95
+ app.screen(:menu) { |p| p.ask "Welcome! Choose an option:", choices: ["Survey", "Exit"] }
96
+ end
97
+ end
98
+
99
+ class CustomerSupportFlow < FlowChat::Flow
100
+ def handle_inquiry
101
+ app.screen(:inquiry) { |p| p.ask "How can we help you today?" }
102
+ end
103
+ end
104
+
105
+ class ApiFlow < FlowChat::Flow
106
+ def handle_request
107
+ app.screen(:request) { |p| p.ask "API request received. Provide data:" }
108
+ end
109
+ end
110
+
111
+ # Note: The custom session ID proc approach provides:
112
+ # - Complete control over session ID format
113
+ # - Access to full context (request data, flow info, etc.)
114
+ # - Ability to implement complex business logic
115
+ # - Support for multi-tenancy, API authentication, etc.
116
+ #
117
+ # The proc should return a string that will be used as the session ID.
118
+ # Make sure the returned ID is unique for your use case to avoid
119
+ # session collisions.
@@ -6,10 +6,10 @@
6
6
  # Usage:
7
7
  # POST /http/webhook
8
8
  # Content-Type: application/json
9
- #
9
+ #
10
10
  # {
11
11
  # "session_id": "unique_session_123",
12
- # "user_id": "user_456",
12
+ # "user_id": "user_456",
13
13
  # "input": "Hello"
14
14
  # }
15
15
  #
@@ -29,10 +29,10 @@ class HttpController < ApplicationController
29
29
  skip_forgery_protection
30
30
 
31
31
  def webhook
32
- processor = FlowChat::Http::Processor.new(self) do |config|
32
+ processor = FlowChat::Processor.new(self) do |config|
33
33
  config.use_gateway FlowChat::Http::Gateway::Simple
34
34
  config.use_session_store FlowChat::Session::CacheSessionStore
35
-
35
+
36
36
  # Configure session management
37
37
  config.use_session_config(
38
38
  boundaries: [:flow, :platform],
@@ -56,9 +56,9 @@ class WelcomeFlow < FlowChat::Flow
56
56
 
57
57
  age = app.screen(:age) do |prompt|
58
58
  prompt.ask "Nice to meet you, #{name}! How old are you?",
59
- validate: ->(input) {
59
+ validate: ->(input) {
60
60
  return "Please enter a number" unless input.match?(/^\d+$/)
61
- return "Age must be between 1 and 120" unless (1..120).include?(input.to_i)
61
+ return "Age must be between 1 and 120" unless (1..120).cover?(input.to_i)
62
62
  nil
63
63
  },
64
64
  transform: ->(input) { input.to_i }
@@ -67,7 +67,7 @@ class WelcomeFlow < FlowChat::Flow
67
67
  preferences = app.screen(:preferences) do |prompt|
68
68
  prompt.select "What are you interested in?", {
69
69
  "tech" => "Technology",
70
- "sports" => "Sports",
70
+ "sports" => "Sports",
71
71
  "music" => "Music",
72
72
  "travel" => "Travel"
73
73
  }
@@ -78,7 +78,7 @@ class WelcomeFlow < FlowChat::Flow
78
78
  app.say "Name: #{name}"
79
79
  app.say "Age: #{age}"
80
80
  app.say "Interest: #{preferences.capitalize}"
81
-
81
+
82
82
  # Ask if they want to continue
83
83
  continue = app.screen(:continue) do |prompt|
84
84
  prompt.yes? "Would you like to explore more features?"
@@ -128,7 +128,7 @@ class WelcomeFlow < FlowChat::Flow
128
128
  prompt.ask "Enter your email address:",
129
129
  validate: ->(input) {
130
130
  return "Email is required" if input.blank?
131
- return "Invalid email format" unless input.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
131
+ return "Invalid email format" unless input.match?(/\A[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+\z/i)
132
132
  nil
133
133
  },
134
134
  transform: ->(input) { input.downcase.strip }
@@ -149,6 +149,6 @@ class WelcomeFlow < FlowChat::Flow
149
149
  app.say "• This is visit ##{counter} in this session"
150
150
  app.say "• Session started at: #{timestamp}"
151
151
  app.say "• Session data persists across HTTP requests"
152
- app.say "• Session ID: #{app.session.context['session.id']}"
152
+ app.say "• Session ID: #{app.session.context["session.id"]}"
153
153
  end
154
- end
154
+ end
@@ -0,0 +1,118 @@
1
+ # Example configuration for using FlowChat with Intercom
2
+
3
+ # 1. Rails Credentials Configuration (recommended)
4
+ # Run: rails credentials:edit
5
+ # Add to config/credentials.yml.enc:
6
+
7
+ # intercom:
8
+ # access_token: "your_intercom_access_token_here"
9
+ # client_secret: "your_intercom_client_secret_here" # For webhook signature validation
10
+ # admin_id: "your_admin_id_here" # Required - admin ID for sending messages
11
+ # skip_signature_validation: false # Set to true for development/testing
12
+
13
+ # 2. Environment Variables Configuration (alternative)
14
+ # Set these environment variables:
15
+
16
+ # INTERCOM_ACCESS_TOKEN=your_intercom_access_token_here
17
+ # INTERCOM_CLIENT_SECRET=your_intercom_client_secret_here
18
+ # INTERCOM_ADMIN_ID=your_admin_id_here
19
+ # INTERCOM_SKIP_SIGNATURE_VALIDATION=false
20
+
21
+ # 3. Named Configuration Example (for multi-tenant apps)
22
+ class MultiTenantIntercomSetup
23
+ def self.setup_configurations
24
+ # Main company configuration
25
+ FlowChat::Intercom::Configuration.new("main") do |config|
26
+ config.access_token = Rails.application.credentials.dig(:intercom, :main, :access_token)
27
+ config.client_secret = Rails.application.credentials.dig(:intercom, :main, :client_secret)
28
+ config.admin_id = Rails.application.credentials.dig(:intercom, :main, :admin_id)
29
+ end
30
+
31
+ # Enterprise customer configuration
32
+ FlowChat::Intercom::Configuration.new("enterprise") do |config|
33
+ config.access_token = Rails.application.credentials.dig(:intercom, :enterprise, :access_token)
34
+ config.client_secret = Rails.application.credentials.dig(:intercom, :enterprise, :client_secret)
35
+ config.admin_id = Rails.application.credentials.dig(:intercom, :enterprise, :admin_id)
36
+ end
37
+ end
38
+ end
39
+
40
+ # 4. Routes Configuration
41
+ # Add to config/routes.rb:
42
+
43
+ # Rails.application.routes.draw do
44
+ # # Intercom webhook endpoint - supports both HEAD (for URL validation) and POST (for events)
45
+ # match '/intercom/webhook', to: 'intercom#webhook', via: [:head, :post]
46
+ #
47
+ # # Multiple tenant support (if needed)
48
+ # match '/intercom/:tenant/webhook', to: 'intercom#webhook', via: [:head, :post]
49
+ # end
50
+
51
+ # 5. Controller with Named Configuration
52
+ class TenantAwareIntercomController < ApplicationController
53
+ skip_forgery_protection
54
+
55
+ def webhook
56
+ # Get tenant-specific configuration
57
+ tenant = params[:tenant] || "main"
58
+ FlowChat::Intercom::Configuration.get(tenant)
59
+
60
+ processor = FlowChat::Processor.new(self) do |config|
61
+ config.use_gateway FlowChat::Intercom::Gateway::IntercomApi, config
62
+ config.use_session_store FlowChat::Session::CacheSessionStore
63
+ config.use_session_config(
64
+ boundaries: [:flow, :url], # Separate sessions per tenant
65
+ identifier: :conversation_id
66
+ )
67
+ end
68
+
69
+ processor.run CustomerSupportFlow, :handle_conversation
70
+ end
71
+ end
72
+
73
+ # 6. Intercom Webhook Setup Instructions
74
+ # To set up webhooks in Intercom:
75
+ #
76
+ # 1. Go to your Intercom Developer Hub
77
+ # 2. Navigate to Configure → Webhooks
78
+ # 3. Add your webhook endpoint:
79
+ # - Endpoint URL: https://yourdomain.com/intercom/webhook (must use HTTPS)
80
+ # - Intercom will send a HEAD request to validate your URL
81
+ # - Your app automatically responds with 200 OK
82
+ #
83
+ # 4. Subscribe to webhook topics:
84
+ # - conversation.user.created (new conversations)
85
+ # - conversation.user.replied (user replies)
86
+ #
87
+ # 5. Configure signature validation:
88
+ # - Find your app's Client Secret in Basic Information
89
+ # - Add this as client_secret in your Rails credentials
90
+ # - Webhooks are validated using X-Hub-Signature header with SHA1 HMAC
91
+ #
92
+ # 6. Test the webhook:
93
+ # - Send a test message in Intercom
94
+ # - Verify your endpoint receives the webhook notification
95
+ # - Check logs for any signature validation issues
96
+
97
+ # 7. Finding Your Admin ID (Required)
98
+ # To find your admin ID for message sending, use this Rails console command:
99
+ #
100
+ # rails console
101
+ # ```
102
+ # config = FlowChat::Intercom::Configuration.from_credentials
103
+ # client = FlowChat::Intercom::Client.new(config)
104
+ # result = client.list_admins
105
+ # result["admins"].each { |admin| puts "#{admin["name"]} (#{admin["email"]}) - ID: #{admin["id"]}" }
106
+ # ```
107
+ #
108
+ # This will display all admins with their IDs. Copy the ID of the admin you want to use for sending messages.
109
+ #
110
+ # Alternative methods:
111
+ # 1. Go to your Intercom Developer Hub (https://developers.intercom.com/)
112
+ # 2. Navigate to your app → Configure → Basic Information
113
+ # 3. Find the Admin ID in the app details
114
+ #
115
+ # The admin_id is required for sending messages from FlowChat to Intercom conversations.
116
+ # The API uses this to identify which admin is sending the message.
117
+ #
118
+ # Note: The old bot_user_id configuration is no longer needed.
@@ -0,0 +1,194 @@
1
+ # Example controller showing how to use the Intercom gateway
2
+ # This controller handles webhooks from Intercom for conversation events
3
+
4
+ class IntercomController < ApplicationController
5
+ skip_forgery_protection
6
+
7
+ # POST /intercom/webhook
8
+ # Handle Intercom webhook notifications for conversation events
9
+ def webhook
10
+ processor = FlowChat::Processor.new(self) do |config|
11
+ # Use Intercom gateway with automatic configuration loading
12
+ config.use_gateway FlowChat::Intercom::Gateway::IntercomApi
13
+
14
+ # Use cache-based session storage for longer-lived sessions
15
+ config.use_session_store FlowChat::Session::CacheSessionStore
16
+
17
+ # Configure session boundaries - use conversation ID for session isolation
18
+ config.use_session_config(
19
+ boundaries: [:conversation], # Each conversation gets its own session
20
+ identifier: :conversation_id # Use conversation ID as session key
21
+ )
22
+ end
23
+
24
+ # Run the customer support flow
25
+ processor.run CustomerSupportFlow, :handle_conversation
26
+ end
27
+ end
28
+
29
+ # Example flow for handling customer conversations via Intercom
30
+ class CustomerSupportFlow < FlowChat::Flow
31
+ def handle_conversation
32
+ # Greet the user
33
+ greeting = if first_message?
34
+ "Hello! I'm here to help you. How can I assist you today?"
35
+ else
36
+ "Thanks for your message! Let me help you with that."
37
+ end
38
+
39
+ # Get the user's inquiry
40
+ inquiry = app.screen(:inquiry) do |prompt|
41
+ prompt.ask greeting
42
+ end
43
+
44
+ # Categorize the inquiry and provide appropriate response
45
+ category = categorize_inquiry(inquiry)
46
+
47
+ case category
48
+ when :technical_support
49
+ handle_technical_support
50
+ when :billing
51
+ handle_billing_inquiry
52
+ when :general
53
+ handle_general_inquiry
54
+ when :escalate
55
+ escalate_to_human("Complex issue requiring human attention")
56
+ else
57
+ app.say "I understand you need help. Let me connect you with one of our team members who can assist you better."
58
+ escalate_to_human("Unrecognized inquiry type")
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def first_message?
65
+ # Check if this is the first message in the conversation
66
+ app.session.get("message_count").nil?
67
+ end
68
+
69
+ def categorize_inquiry(inquiry)
70
+ # Simple keyword-based categorization
71
+ # In a real app, you might use AI/ML for better categorization
72
+ text = inquiry.downcase
73
+
74
+ if text.include?("bug") || text.include?("error") || text.include?("not working")
75
+ :technical_support
76
+ elsif text.include?("bill") || text.include?("payment") || text.include?("refund")
77
+ :billing
78
+ elsif text.include?("urgent") || text.include?("important") || text.include?("asap")
79
+ :escalate
80
+ else
81
+ :general
82
+ end
83
+ end
84
+
85
+ def handle_technical_support
86
+ solution = app.screen(:tech_solution) do |prompt|
87
+ prompt.select "I can help you with technical issues! What type of problem are you experiencing?", {
88
+ "login" => "Can't log in",
89
+ "performance" => "App is slow",
90
+ "features" => "Feature not working",
91
+ "other" => "Something else"
92
+ }
93
+ end
94
+
95
+ case solution
96
+ when "login"
97
+ provide_login_help
98
+ when "performance"
99
+ provide_performance_tips
100
+ when "features"
101
+ provide_feature_help
102
+ when "other"
103
+ escalate_to_human("Technical issue requiring human review")
104
+ end
105
+ end
106
+
107
+ def handle_billing_inquiry
108
+ app.say "For billing questions, I'm connecting you with our billing team who can access your account securely."
109
+ escalate_to_human("Billing inquiry", team_id: "billing_team_id")
110
+ end
111
+
112
+ def handle_general_inquiry
113
+ satisfaction = app.screen(:satisfaction) do |prompt|
114
+ prompt.select "I hope I was able to help! How would you rate your experience?", {
115
+ "1" => "😞 Poor",
116
+ "2" => "😐 Okay",
117
+ "3" => "😊 Good",
118
+ "4" => "😍 Excellent"
119
+ }
120
+ end
121
+
122
+ if satisfaction.to_i >= 3
123
+ app.say "Thank you for the positive feedback! Feel free to reach out if you need anything else."
124
+ else
125
+ app.say "I'm sorry the experience wasn't better. Let me connect you with a team member who can help."
126
+ escalate_to_human("User reported poor experience")
127
+ end
128
+ end
129
+
130
+ def provide_login_help
131
+ app.say <<~MESSAGE
132
+ Here are some steps to try for login issues:
133
+
134
+ 1. Make sure you're using the correct email address
135
+ 2. Try resetting your password using the "Forgot Password" link
136
+ 3. Clear your browser cache and cookies
137
+ 4. Try logging in from an incognito/private window
138
+
139
+ If none of these work, I'll connect you with our technical support team.
140
+ MESSAGE
141
+
142
+ still_stuck = app.screen(:login_resolved) do |prompt|
143
+ prompt.yes? "Did these steps help resolve your login issue?"
144
+ end
145
+
146
+ unless still_stuck
147
+ escalate_to_human("Login issue not resolved by self-service steps")
148
+ end
149
+ end
150
+
151
+ def provide_performance_tips
152
+ app.say <<~MESSAGE
153
+ Here are some tips to improve app performance:
154
+
155
+ 1. Close other browser tabs or apps
156
+ 2. Check your internet connection
157
+ 3. Try refreshing the page
158
+ 4. Update your browser to the latest version
159
+
160
+ If the problem persists, our technical team can investigate further.
161
+ MESSAGE
162
+ end
163
+
164
+ def provide_feature_help
165
+ feature = app.screen(:which_feature) do |prompt|
166
+ prompt.ask "Which feature are you having trouble with? Please describe what you're trying to do."
167
+ end
168
+
169
+ app.say "Thanks for the details about #{feature}. Let me connect you with our product team who can provide specific guidance."
170
+ escalate_to_human("Feature support request: #{feature}")
171
+ end
172
+
173
+ def escalate_to_human(reason, team_id: nil)
174
+ conversation_id = app.context["request.conversation_id"]
175
+
176
+ # Business logic: Use Intercom manager for conversation control
177
+ if app.gateway == :intercom_api
178
+ # Access the gateway's client and manager for escalation
179
+ # This would be implemented by exposing these through the gateway
180
+ app.say "I'm connecting you with one of our team members who can provide more personalized assistance. They'll be with you shortly!"
181
+
182
+ # Business logic would handle:
183
+ # - Remove AI_HANDLING tag
184
+ # - Add ESCALATED tag
185
+ # - Assign to appropriate team
186
+ # - Set priority based on reason
187
+ # - Add internal note for context
188
+
189
+ FlowChat.logger.info { "CustomerSupportFlow: Escalating conversation #{conversation_id} - Reason: #{reason}" }
190
+ else
191
+ app.say "Let me transfer you to a human agent who can help you further."
192
+ end
193
+ end
194
+ end
@@ -9,7 +9,7 @@ class MultiTenantWhatsappController < ApplicationController
9
9
  tenant = determine_tenant(request)
10
10
  whatsapp_config = get_whatsapp_config_for_tenant(tenant)
11
11
 
12
- processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: !Rails.env.production?) do |config|
12
+ processor = FlowChat::Processor.new(self, enable_simulator: !Rails.env.production?) do |config|
13
13
  config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, whatsapp_config
14
14
  config.use_session_store FlowChat::Session::CacheSessionStore
15
15
  end
@@ -96,7 +96,7 @@ class DatabaseWhatsappController < ApplicationController
96
96
  config.app_secret = business_account.whatsapp_app_secret
97
97
  end
98
98
 
99
- processor = FlowChat::Whatsapp::Processor.new(self) do |config|
99
+ processor = FlowChat::Processor.new(self) do |config|
100
100
  config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, whatsapp_config
101
101
  config.use_session_store FlowChat::Session::CacheSessionStore
102
102
  end
@@ -135,7 +135,7 @@ class EnvironmentWhatsappController < ApplicationController
135
135
  FlowChat::Whatsapp::Configuration.from_credentials
136
136
  end
137
137
 
138
- processor = FlowChat::Whatsapp::Processor.new(self) do |config|
138
+ processor = FlowChat::Processor.new(self) do |config|
139
139
  config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, whatsapp_config
140
140
  config.use_session_store FlowChat::Session::CacheSessionStore
141
141
  end
@@ -193,7 +193,7 @@ class CustomWhatsappController < ApplicationController
193
193
  my_config.app_secret = "your_app_secret"
194
194
  my_config.business_account_id = "your_business_account_id"
195
195
 
196
- processor = FlowChat::Whatsapp::Processor.new(self) do |config|
196
+ processor = FlowChat::Processor.new(self) do |config|
197
197
  config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, my_config
198
198
  config.use_session_store FlowChat::Session::CacheSessionStore
199
199
  end
@@ -5,10 +5,10 @@ class UssdController < ApplicationController
5
5
  skip_forgery_protection
6
6
 
7
7
  def process_request
8
- processor = FlowChat::Ussd::Processor.new(self) do |config|
8
+ processor = FlowChat::Processor.new(self) do |config|
9
9
  config.use_gateway FlowChat::Ussd::Gateway::Nalo
10
- # Use Rails session for USSD (shorter sessions)
11
- config.use_session_store FlowChat::Session::RailsSessionStore
10
+ # Use cache session store for USSD
11
+ config.use_session_store FlowChat::Session::CacheSessionStore
12
12
 
13
13
  # Enable durable sessions (optional)
14
14
  config.use_durable_sessions # Configures flow+platform isolation with durable sessions
@@ -194,9 +194,9 @@ class UssdController < ApplicationController
194
194
  FlowChat::Config.ussd.pagination_next_option = "#"
195
195
  FlowChat::Config.ussd.pagination_back_option = "*"
196
196
 
197
- processor = FlowChat::Ussd::Processor.new(self) do |config|
197
+ processor = FlowChat::Processor.new(self) do |config|
198
198
  config.use_gateway FlowChat::Ussd::Gateway::Nalo
199
- config.use_session_store FlowChat::Session::RailsSessionStore
199
+ config.use_session_store FlowChat::Session::CacheSessionStore
200
200
  end
201
201
 
202
202
  processor.run WelcomeFlow, :main_page
@@ -228,12 +228,12 @@ class UssdController < ApplicationController
228
228
  skip_forgery_protection
229
229
 
230
230
  def process_request
231
- processor = FlowChat::Ussd::Processor.new(self) do |config|
231
+ processor = FlowChat::Processor.new(self) do |config|
232
232
  config.use_gateway FlowChat::Ussd::Gateway::Nalo
233
- config.use_session_store FlowChat::Session::RailsSessionStore
233
+ config.use_session_store FlowChat::Session::CacheSessionStore
234
234
  config.use_middleware LoggingMiddleware # Add custom logging
235
235
  config.use_durable_sessions # Enable durable sessions
236
-
236
+
237
237
  # Or configure session boundaries explicitly:
238
238
  # config.use_session_config(
239
239
  # boundaries: [:flow, :platform], # which boundaries to enforce
@@ -252,7 +252,7 @@ class UssdController < ApplicationController
252
252
  skip_forgery_protection
253
253
 
254
254
  def process_request
255
- processor = FlowChat::Ussd::Processor.new(self) do |config|
255
+ processor = FlowChat::Processor.new(self) do |config|
256
256
  config.use_gateway FlowChat::Ussd::Gateway::Nalo
257
257
  # Use cache store for longer session persistence
258
258
  config.use_session_store FlowChat::Session::CacheSessionStore
@@ -6,7 +6,7 @@ class WhatsappController < ApplicationController
6
6
  skip_forgery_protection
7
7
 
8
8
  def webhook
9
- processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: Rails.env.development?) do |config|
9
+ processor = FlowChat::Processor.new(self, enable_simulator: Rails.env.development?) do |config|
10
10
  config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
11
11
  config.use_session_store FlowChat::Session::CacheSessionStore
12
12
  end
@@ -25,7 +25,7 @@ class CustomWhatsappController < ApplicationController
25
25
  def webhook
26
26
  custom_config = build_whatsapp_config
27
27
 
28
- processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: !Rails.env.production?) do |config|
28
+ processor = FlowChat::Processor.new(self, enable_simulator: !Rails.env.production?) do |config|
29
29
  config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, custom_config
30
30
  config.use_session_store FlowChat::Session::CacheSessionStore
31
31
  end
data/flow_chat.gemspec CHANGED
@@ -17,6 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
18
18
 
19
19
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
20
+ spec.metadata["rubygems_mfa_required"] = "true"
20
21
 
21
22
  spec.metadata["homepage_uri"] = spec.homepage
22
23
  spec.metadata["source_code_uri"] = spec.homepage
@@ -36,4 +37,7 @@ Gem::Specification.new do |spec|
36
37
  spec.add_dependency "actionpack", ">= 6"
37
38
  spec.add_dependency "phonelib"
38
39
  spec.add_dependency "ibsciss-middleware", "~> 0.4.2"
40
+ spec.add_dependency "intercom", "~> 4.2"
41
+ spec.add_dependency "reverse_markdown", "~> 3.0"
42
+ spec.add_dependency "kramdown", "~> 2.4"
39
43
  end
@@ -1,10 +1,9 @@
1
1
  module FlowChat
2
- class BaseApp
3
- attr_reader :session, :input, :context, :navigation_stack
2
+ class App
3
+ attr_reader :input, :context, :navigation_stack
4
4
 
5
5
  def initialize(context)
6
6
  @context = context
7
- @session = context.session
8
7
  @input = context.input
9
8
  @navigation_stack = []
10
9
  end
@@ -27,11 +26,11 @@ module FlowChat
27
26
 
28
27
  def go_back
29
28
  return false if navigation_stack.empty?
30
-
29
+
31
30
  @context.input = nil
32
31
  current_screen = navigation_stack.last
33
32
  session.delete(current_screen)
34
-
33
+
35
34
  # Restart the flow from the beginning
36
35
  raise FlowChat::Interrupt::RestartFlow.new
37
36
  end
@@ -47,7 +46,7 @@ module FlowChat
47
46
  def gateway
48
47
  context["request.gateway"]
49
48
  end
50
-
49
+
51
50
  def user_id
52
51
  context["request.user_id"]
53
52
  end
@@ -76,11 +75,19 @@ module FlowChat
76
75
  nil
77
76
  end
78
77
 
78
+ def session
79
+ @context.session
80
+ end
81
+
79
82
  protected
80
83
 
81
- # Platform-specific methods to be overridden
82
84
  def prepare_user_input
83
- input
85
+ user_input = input
86
+ if platform != :ussd && session.get(FlowChat::Input::START).nil?
87
+ session.set(FlowChat::Input::START, user_input)
88
+ user_input = nil
89
+ end
90
+ user_input
84
91
  end
85
92
  end
86
- end
93
+ end