flow_chat 0.6.1 → 0.8.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +44 -0
  3. data/.gitignore +2 -1
  4. data/README.md +85 -1229
  5. data/docs/configuration.md +360 -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/sessions.md +433 -0
  11. data/docs/testing.md +475 -0
  12. data/docs/ussd-setup.md +322 -0
  13. data/docs/whatsapp-setup.md +162 -0
  14. data/examples/multi_tenant_whatsapp_controller.rb +9 -37
  15. data/examples/simulator_controller.rb +13 -22
  16. data/examples/ussd_controller.rb +41 -41
  17. data/examples/whatsapp_controller.rb +32 -125
  18. data/examples/whatsapp_media_examples.rb +68 -336
  19. data/examples/whatsapp_message_job.rb +5 -3
  20. data/flow_chat.gemspec +6 -2
  21. data/lib/flow_chat/base_processor.rb +79 -2
  22. data/lib/flow_chat/config.rb +31 -5
  23. data/lib/flow_chat/context.rb +13 -1
  24. data/lib/flow_chat/instrumentation/log_subscriber.rb +176 -0
  25. data/lib/flow_chat/instrumentation/metrics_collector.rb +197 -0
  26. data/lib/flow_chat/instrumentation/setup.rb +155 -0
  27. data/lib/flow_chat/instrumentation.rb +70 -0
  28. data/lib/flow_chat/prompt.rb +20 -20
  29. data/lib/flow_chat/session/cache_session_store.rb +73 -7
  30. data/lib/flow_chat/session/middleware.rb +130 -12
  31. data/lib/flow_chat/session/rails_session_store.rb +36 -1
  32. data/lib/flow_chat/simulator/controller.rb +8 -8
  33. data/lib/flow_chat/simulator/views/simulator.html.erb +5 -5
  34. data/lib/flow_chat/ussd/gateway/nalo.rb +31 -0
  35. data/lib/flow_chat/ussd/gateway/nsano.rb +36 -2
  36. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +109 -0
  37. data/lib/flow_chat/ussd/middleware/executor.rb +24 -2
  38. data/lib/flow_chat/ussd/middleware/pagination.rb +87 -7
  39. data/lib/flow_chat/ussd/processor.rb +16 -4
  40. data/lib/flow_chat/ussd/renderer.rb +1 -1
  41. data/lib/flow_chat/version.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 +121 -34
  45. data/lib/flow_chat/whatsapp/middleware/executor.rb +24 -2
  46. data/lib/flow_chat/whatsapp/processor.rb +7 -1
  47. data/lib/flow_chat/whatsapp/renderer.rb +4 -9
  48. data/lib/flow_chat.rb +23 -0
  49. metadata +23 -12
  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
  57. data/lib/flow_chat/ussd/middleware/resumable_session.rb +0 -39
data/docs/sessions.md ADDED
@@ -0,0 +1,433 @@
1
+ # Session Management
2
+
3
+ FlowChat provides a powerful and flexible session management system that enables persistent conversational state across multiple requests. This document covers the architecture, configuration, and best practices for session management.
4
+
5
+ ## Overview
6
+
7
+ Sessions in FlowChat store user conversation state between requests, enabling:
8
+
9
+ - **Multi-step workflows** - Collect information across multiple prompts
10
+ - **Context preservation** - Remember user inputs and conversation history
11
+ - **Cross-platform consistency** - Same session behavior across USSD and WhatsApp
12
+ - **Privacy protection** - Automatic phone number hashing for security
13
+ - **Flexible isolation** - Control session boundaries per deployment needs
14
+
15
+ ## Architecture
16
+
17
+ The session system is built around three core components:
18
+
19
+ ### 1. Session Configuration (`FlowChat::Config::SessionConfig`)
20
+
21
+ Controls how session IDs are generated and sessions are isolated:
22
+
23
+ ```ruby
24
+ # Global session configuration
25
+ FlowChat::Config.session.boundaries = [:flow, :gateway, :platform] # isolation boundaries
26
+ FlowChat::Config.session.hash_phone_numbers = true # privacy protection
27
+ FlowChat::Config.session.identifier = nil # platform chooses default
28
+ ```
29
+
30
+ ### 2. Session Middleware (`FlowChat::Session::Middleware`)
31
+
32
+ Automatically manages session creation and ID generation based on configuration:
33
+
34
+ - Generates consistent session IDs based on boundaries and identifiers
35
+ - Creates session store instances for each request
36
+ - Handles platform-specific defaults (USSD = ephemeral, WhatsApp = durable)
37
+ - Provides instrumentation events for monitoring
38
+
39
+ ### 3. Session Stores
40
+
41
+ Store actual session data with different persistence strategies:
42
+
43
+ - **`FlowChat::Session::CacheSessionStore`** - Uses Rails cache (recommended)
44
+ - **`FlowChat::Session::RailsSessionStore`** - Uses Rails session (limited)
45
+
46
+ ## Session Boundaries
47
+
48
+ Boundaries control how session IDs are constructed, determining when sessions are shared vs. isolated:
49
+
50
+ ### Available Boundaries
51
+
52
+ - **`:flow`** - Separate sessions per flow class
53
+ - **`:platform`** - Separate sessions per platform (ussd, whatsapp)
54
+ - **`:gateway`** - Separate sessions per gateway
55
+ - **`:url`** - Separate sessions per request URL (host + path)
56
+ - **`[]`** - Global sessions (no boundaries)
57
+
58
+ ### Examples
59
+
60
+ ```ruby
61
+ # Default: Full isolation
62
+ FlowChat::Config.session.boundaries = [:flow, :gateway, :platform]
63
+ # Session ID: "registration_flow:nalo:ussd:abc123"
64
+
65
+ # Flow isolation only
66
+ FlowChat::Config.session.boundaries = [:flow]
67
+ # Session ID: "registration_flow:abc123"
68
+
69
+ # Platform isolation only
70
+ FlowChat::Config.session.boundaries = [:platform]
71
+ # Session ID: "ussd:abc123"
72
+
73
+ # Global sessions
74
+ FlowChat::Config.session.boundaries = []
75
+ # Session ID: "abc123"
76
+ ```
77
+
78
+ ## Session Identifiers
79
+
80
+ Identifiers determine what makes a session unique:
81
+
82
+ ### Available Identifiers
83
+
84
+ - **`nil`** - Platform chooses default (recommended)
85
+ - USSD: `:request_id` (ephemeral sessions)
86
+ - WhatsApp: `:msisdn` (durable sessions)
87
+ - **`:msisdn`** - Use phone number (durable sessions)
88
+ - **`:request_id`** - Use request ID (ephemeral sessions)
89
+
90
+ ### Platform Defaults
91
+
92
+ ```ruby
93
+ # USSD: ephemeral sessions by default
94
+ identifier: :request_id
95
+ # New session each time USSD times out
96
+
97
+ # WhatsApp: durable sessions by default
98
+ identifier: :msisdn
99
+ # Same session resumes across conversations
100
+ ```
101
+
102
+ ### Phone Number Hashing
103
+
104
+ When using `:msisdn` identifier, phone numbers are automatically hashed for privacy:
105
+
106
+ ```ruby
107
+ FlowChat::Config.session.hash_phone_numbers = true # default
108
+ # "+256700123456" becomes "a1b2c3d4" (8-character hash)
109
+
110
+ FlowChat::Config.session.hash_phone_numbers = false
111
+ # "+256700123456" used directly (not recommended for production)
112
+ ```
113
+
114
+ ## Configuration Examples
115
+
116
+ ### Basic USSD Configuration
117
+
118
+ ```ruby
119
+ processor = FlowChat::Ussd::Processor.new(self) do |config|
120
+ config.use_gateway FlowChat::Ussd::Gateway::Nalo
121
+ config.use_session_store FlowChat::Session::CacheSessionStore
122
+
123
+ # Use shorthand for standard durable sessions
124
+ config.use_durable_sessions
125
+ end
126
+ ```
127
+
128
+ ### Custom Session Configuration
129
+
130
+ ```ruby
131
+ processor = FlowChat::Ussd::Processor.new(self) do |config|
132
+ config.use_gateway FlowChat::Ussd::Gateway::Nalo
133
+ config.use_session_store FlowChat::Session::CacheSessionStore
134
+
135
+ # Explicit session configuration
136
+ config.use_session_config(
137
+ boundaries: [:flow, :platform], # isolate by flow and platform
138
+ hash_phone_numbers: true, # hash phone numbers for privacy
139
+ identifier: :msisdn # use phone number for durable sessions
140
+ )
141
+ end
142
+ ```
143
+
144
+ ### Cross-gateway Sessions
145
+
146
+ ```ruby
147
+ processor = FlowChat::Ussd::Processor.new(self) do |config|
148
+ config.use_gateway FlowChat::Ussd::Gateway::Nalo
149
+ config.use_session_store FlowChat::Session::CacheSessionStore
150
+
151
+ # Allow sessions to work across different gateways
152
+ config.use_session_config(boundaries: [:flow, :platform])
153
+ end
154
+ ```
155
+
156
+ ### Cross-Platform Sessions
157
+
158
+ ```ruby
159
+ processor = FlowChat::Ussd::Processor.new(self) do |config|
160
+ config.use_gateway FlowChat::Ussd::Gateway::Nalo
161
+ config.use_session_store FlowChat::Session::CacheSessionStore
162
+
163
+ # Allow same user to continue on USSD or WhatsApp
164
+ config.use_cross_platform_sessions
165
+ # Equivalent to: boundaries: [:flow]
166
+ end
167
+ ```
168
+
169
+ ### URL-Based Session Isolation
170
+
171
+ Perfect for multi-tenant applications:
172
+
173
+ ```ruby
174
+ processor = FlowChat::Ussd::Processor.new(self) do |config|
175
+ config.use_gateway FlowChat::Ussd::Gateway::Nalo
176
+ config.use_session_store FlowChat::Session::CacheSessionStore
177
+
178
+ # Isolate sessions by URL (great for multi-tenant SaaS)
179
+ config.use_url_isolation
180
+ # Adds :url to existing boundaries
181
+ end
182
+ ```
183
+
184
+ **URL Boundary Examples:**
185
+ - `tenant1.example.com/ussd` vs `tenant2.example.com/ussd` - Different sessions
186
+ - `api.example.com/v1/ussd` vs `api.example.com/v2/ussd` - Different sessions
187
+ - `dev.example.com/ussd` vs `prod.example.com/ussd` - Different sessions
188
+
189
+ **URL Processing:**
190
+ - Combines host + path: `example.com/api/v1/ussd`
191
+ - Sanitizes special characters: `tenant-1.com/ussd` → `tenant_1.com/ussd`
192
+ - Hashes long URLs (>50 chars): `verylongdomain.../path` → `url_a1b2c3d4`
193
+
194
+ ### Global Sessions
195
+
196
+ ```ruby
197
+ processor = FlowChat::Ussd::Processor.new(self) do |config|
198
+ config.use_gateway FlowChat::Ussd::Gateway::Nalo
199
+ config.use_session_store FlowChat::Session::CacheSessionStore
200
+
201
+ # Single session shared across everything
202
+ config.use_session_config(boundaries: [])
203
+ end
204
+ ```
205
+
206
+ ## Session Stores
207
+
208
+ ### Cache Session Store (Recommended)
209
+
210
+ Uses Rails cache backend with automatic TTL management:
211
+
212
+ ```ruby
213
+ config.use_session_store FlowChat::Session::CacheSessionStore
214
+
215
+ # Requires Rails cache to be configured
216
+ # config/environments/production.rb
217
+ config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
218
+ ```
219
+
220
+ **Features:**
221
+ - Automatic session expiration via cache TTL
222
+ - Redis/Memcached support for distributed deployments
223
+ - High performance
224
+ - Memory efficient
225
+
226
+ ### Rails Session Store
227
+
228
+ Uses Rails session for storage (limited scope):
229
+
230
+ ```ruby
231
+ config.use_session_store FlowChat::Session::RailsSessionStore
232
+ ```
233
+
234
+ **Limitations:**
235
+ - Tied to HTTP session lifecycle
236
+ - Limited storage capacity
237
+ - Not suitable for long-running conversations
238
+
239
+ ## Session Data Usage
240
+
241
+ ### In Flows
242
+
243
+ Sessions are automatically available in flows:
244
+
245
+ ```ruby
246
+ class RegistrationFlow < FlowChat::Flow
247
+ def main_page
248
+ # Store data
249
+ app.session.set("step", "registration")
250
+ app.session.set("user_data", {name: "John", age: 25})
251
+
252
+ # Retrieve data
253
+ step = app.session.get("step")
254
+ user_data = app.session.get("user_data")
255
+
256
+ # Check existence
257
+ if app.session.get("completed")
258
+ app.say "Registration already completed!"
259
+ return
260
+ end
261
+
262
+ # Continue flow...
263
+ name = app.screen(:name) { |prompt| prompt.ask "Name?" }
264
+ app.session.set("name", name)
265
+ end
266
+ end
267
+ ```
268
+
269
+ ### Session Store API
270
+
271
+ All session stores implement a consistent interface:
272
+
273
+ ```ruby
274
+ # Basic operations
275
+ session.set(key, value) # Store data
276
+ value = session.get(key) # Retrieve data
277
+ session.delete(key) # Delete specific key
278
+ session.clear # Clear all session data
279
+ session.destroy # Destroy entire session
280
+
281
+ # Utility methods
282
+ session.exists? # Check if session has any data
283
+ ```
284
+
285
+ ## Best Practices
286
+
287
+ ### 1. Choose Appropriate Boundaries
288
+
289
+ ```ruby
290
+ # High-traffic public services
291
+ boundaries: [:flow, :gateway, :platform] # Full isolation
292
+
293
+ # Single-tenant applications
294
+ boundaries: [:flow] # Simpler, allows cross-platform
295
+
296
+ # Global state services (rare)
297
+ boundaries: [] # Shared state across everything
298
+ ```
299
+
300
+ ### 2. Consider Session Lifecycle
301
+
302
+ ```ruby
303
+ # USSD: Short sessions, frequent timeouts
304
+ identifier: :request_id # New session each timeout (default)
305
+
306
+ # WhatsApp: Long conversations, persistent
307
+ identifier: :msisdn # Resume across days/weeks (default)
308
+
309
+ # Custom requirements
310
+ identifier: :msisdn # Make USSD durable
311
+ identifier: :request_id # Make WhatsApp ephemeral
312
+ ```
313
+
314
+ ### 3. Handle Session Expiration
315
+
316
+ ```ruby
317
+ class RegistrationFlow < FlowChat::Flow
318
+ def main_page
319
+ # Check if session expired
320
+ if app.session.get("user_id").nil?
321
+ restart_registration
322
+ return
323
+ end
324
+
325
+ # Continue existing session
326
+ continue_registration
327
+ end
328
+
329
+ private
330
+
331
+ def restart_registration
332
+ app.say "Session expired. Let's start over."
333
+ # Reset flow to beginning
334
+ end
335
+ end
336
+ ```
337
+
338
+ ### 4. Optimize Session Data
339
+
340
+ ```ruby
341
+ # Store only necessary data
342
+ app.session.set("user_id", 123) # Good: minimal data
343
+ app.session.set("user", user_object) # Avoid: large objects
344
+
345
+ # Clean up when done
346
+ def complete_registration
347
+ app.session.set("completed", true)
348
+ app.session.delete("temp_data") # Clean up temporary data
349
+ end
350
+ ```
351
+
352
+ ### 5. Security Considerations
353
+
354
+ ```ruby
355
+ # Always hash phone numbers in production
356
+ FlowChat::Config.session.hash_phone_numbers = true
357
+
358
+ # Use secure cache backends
359
+ config.cache_store = :redis_cache_store, {
360
+ url: ENV['REDIS_URL'],
361
+ ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }
362
+ }
363
+
364
+ # Set appropriate TTLs
365
+ cache_options = { expires_in: 30.minutes } # Reasonable session timeout
366
+ ```
367
+
368
+ ## Troubleshooting
369
+
370
+ ### Session Not Persisting
371
+
372
+ 1. **Check session store configuration:**
373
+ ```ruby
374
+ # Ensure session store is configured
375
+ config.use_session_store FlowChat::Session::CacheSessionStore
376
+ ```
377
+
378
+ 2. **Verify cache backend:**
379
+ ```ruby
380
+ # Test cache is working
381
+ Rails.cache.write("test", "value")
382
+ puts Rails.cache.read("test") # Should output "value"
383
+ ```
384
+
385
+ 3. **Check session boundaries:**
386
+ ```ruby
387
+ # Debug session ID generation
388
+ FlowChat.logger.level = Logger::DEBUG
389
+ # Look for "Session::Middleware: Generated session ID: ..." messages
390
+ ```
391
+
392
+ ### Different Session IDs
393
+
394
+ 1. **Inconsistent request data:**
395
+ - Verify `request.gateway` is consistent
396
+ - Check `request.platform` is set correctly
397
+ - Ensure `request.msisdn` format is consistent
398
+
399
+ 2. **Boundary configuration mismatch:**
400
+ ```ruby
401
+ # Ensure same boundaries across requests
402
+ config.use_session_config(boundaries: [:flow, :platform])
403
+ ```
404
+
405
+ ### Session Data Lost
406
+
407
+ 1. **Cache expiration:**
408
+ ```ruby
409
+ # Increase TTL if needed
410
+ FlowChat::Config.cache = Rails.cache
411
+ # Configure longer expiration in cache store
412
+ ```
413
+
414
+ 2. **Session ID changes:**
415
+ - Check logs for "Generated session ID" messages
416
+ - Verify identifier consistency (`:msisdn` vs `:request_id`)
417
+
418
+ ## Monitoring and Instrumentation
419
+
420
+ FlowChat emits events for session operations:
421
+
422
+ ```ruby
423
+ # Subscribe to session events
424
+ ActiveSupport::Notifications.subscribe("session.created.flow_chat") do |event|
425
+ Rails.logger.info "Session created: #{event.payload[:session_id]}"
426
+ end
427
+
428
+ # Monitor session usage
429
+ ActiveSupport::Notifications.subscribe(/session\..*\.flow_chat/) do |name, start, finish, id, payload|
430
+ # Track session operations for analytics
431
+ Analytics.track("flowchat.session.#{name.split('.')[1]}", payload)
432
+ end
433
+ ```