flow_chat 0.8.1 → 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 +154 -0
  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/simulator_controller.rb +21 -1
  31. data/examples/ussd_controller.rb +10 -10
  32. data/examples/whatsapp_controller.rb +2 -2
  33. data/flow_chat.gemspec +4 -0
  34. data/lib/flow_chat/{base_app.rb → app.rb} +28 -9
  35. data/lib/flow_chat/async_job.rb +176 -0
  36. data/lib/flow_chat/config.rb +23 -28
  37. data/lib/flow_chat/{base_executor.rb → executor.rb} +7 -12
  38. data/lib/flow_chat/factory.rb +94 -0
  39. data/lib/flow_chat/gateway_async_support.rb +106 -0
  40. data/lib/flow_chat/generic_async_job.rb +30 -0
  41. data/lib/flow_chat/http/gateway/simple.rb +125 -0
  42. data/lib/flow_chat/http/renderer.rb +41 -0
  43. data/lib/flow_chat/instrumentation/setup.rb +1 -1
  44. data/lib/flow_chat/instrumentation.rb +25 -0
  45. data/lib/flow_chat/intercom/client.rb +155 -0
  46. data/lib/flow_chat/intercom/configuration.rb +149 -0
  47. data/lib/flow_chat/intercom/gateway/intercom_api.rb +396 -0
  48. data/lib/flow_chat/intercom/renderer.rb +71 -0
  49. data/lib/flow_chat/phone_number_util.rb +49 -0
  50. data/lib/flow_chat/processor.rb +188 -0
  51. data/lib/flow_chat/renderers/markdown_support.rb +58 -0
  52. data/lib/flow_chat/session/cache_session_store.rb +1 -17
  53. data/lib/flow_chat/session/middleware.rb +43 -26
  54. data/lib/flow_chat/simulator/controller.rb +17 -5
  55. data/lib/flow_chat/simulator/views/simulator.html.erb +220 -8
  56. data/lib/flow_chat/telegram/client.rb +240 -0
  57. data/lib/flow_chat/telegram/configuration.rb +118 -0
  58. data/lib/flow_chat/telegram/gateway/bot_api.rb +300 -0
  59. data/lib/flow_chat/telegram/middleware/choice_mapper.rb +35 -0
  60. data/lib/flow_chat/telegram/renderer.rb +125 -0
  61. data/lib/flow_chat/telegram.rb +7 -0
  62. data/lib/flow_chat/ussd/gateway/nalo.rb +27 -11
  63. data/lib/flow_chat/ussd/middleware/pagination.rb +9 -5
  64. data/lib/flow_chat/ussd/renderer.rb +1 -1
  65. data/lib/flow_chat/version.rb +1 -1
  66. data/lib/flow_chat/whatsapp/client.rb +144 -13
  67. data/lib/flow_chat/whatsapp/configuration.rb +1 -1
  68. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +144 -106
  69. data/lib/flow_chat/whatsapp/id_generator.rb +124 -0
  70. data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +147 -0
  71. data/lib/flow_chat/whatsapp/renderer.rb +145 -11
  72. data/lib/flow_chat.rb +11 -1
  73. data/lib/tasks/release.rake +165 -0
  74. metadata +88 -21
  75. data/docs/flows.md +0 -320
  76. data/docs/images/simulator.png +0 -0
  77. data/docs/media.md +0 -153
  78. data/docs/sessions.md +0 -433
  79. data/docs/ussd-setup.md +0 -322
  80. data/docs/whatsapp-setup.md +0 -162
  81. data/examples/whatsapp_message_job.rb +0 -113
  82. data/lib/flow_chat/base_processor.rb +0 -145
  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 -98
  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,298 @@
1
+ # Async Background Processing
2
+
3
+ FlowChat supports asynchronous background processing to decouple flow execution from webhook request-response cycles. Webhook endpoints respond immediately (< 100ms) while flows process in background jobs.
4
+
5
+ ## Quick Start
6
+
7
+ ### Option 1: Using Factory Pattern (Recommended)
8
+
9
+ The cleanest approach uses the Factory pattern for centralized configuration:
10
+
11
+ ```ruby
12
+ # config/initializers/flow_chat.rb
13
+ FlowChat::Factory.register :whatsapp do |controller|
14
+ processor = FlowChat::Processor.new(controller) do |config|
15
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
16
+ config.use_session_store FlowChat::Session::CacheSessionStore
17
+ config.use_session_config(boundaries: [:flow])
18
+ config.use_async(factory: :whatsapp) # Self-referencing for async
19
+ end
20
+ processor.run(WhatsAppFlow, :start)
21
+ end
22
+
23
+ # app/controllers/webhooks_controller.rb
24
+ class WebhooksController < ApplicationController
25
+ def whatsapp
26
+ FlowChat::Factory.execute(:whatsapp, controller: self)
27
+ end
28
+ end
29
+ ```
30
+
31
+ **Benefits:**
32
+ - No custom job class needed
33
+ - Configuration defined once in initializer
34
+ - Works seamlessly in both webhook and background contexts
35
+
36
+ See [Factory Pattern Documentation](factory-pattern.md) for more details.
37
+
38
+ ### Option 2: Custom Job Class
39
+
40
+ For advanced use cases, create a custom job class:
41
+
42
+ ```ruby
43
+ # app/jobs/my_flow_job.rb
44
+ class MyFlowJob < FlowChat::AsyncJob
45
+ def execute(controller, **job_params)
46
+ # Access job params passed from use_async
47
+ deployment_id = job_params[:deployment_id]
48
+
49
+ # Build and run your processor
50
+ processor = FlowChat::Processor.new(controller) do |config|
51
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
52
+ config.use_session_store FlowChat::Session::CacheSessionStore
53
+ config.use_session_config(boundaries: [:flow])
54
+ end
55
+
56
+ processor.run MyFlow, :start
57
+ end
58
+ end
59
+
60
+ # app/controllers/webhooks_controller.rb
61
+ class WebhooksController < ApplicationController
62
+ def whatsapp
63
+ processor = FlowChat::Processor.new(self) do |config|
64
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
65
+ config.use_session_store FlowChat::Session::CacheSessionStore
66
+ config.use_async MyFlowJob, deployment_id: params[:deployment_id]
67
+ end
68
+ processor.run(MyFlow, :start)
69
+ end
70
+ end
71
+ ```
72
+
73
+ ## Configuration
74
+
75
+ ### Session Store
76
+
77
+ Use a centralized cache store e.g. `CacheSessionStore` for async processing:
78
+
79
+ ```ruby
80
+ config.use_session_store FlowChat::Session::CacheSessionStore
81
+ ```
82
+
83
+ ### ActiveJob Backend
84
+
85
+ ```ruby
86
+ # config/application.rb
87
+ config.active_job.queue_adapter = :sidekiq # or :resque, :delayed_job
88
+ ```
89
+
90
+ ### Queue Configuration
91
+
92
+ Set queue in job class:
93
+
94
+ ```ruby
95
+ class MyFlowJob < FlowChat::AsyncJob
96
+ queue_as :support_flows
97
+
98
+ def execute(controller)
99
+ # ...
100
+ end
101
+ end
102
+ ```
103
+
104
+ Configure priorities:
105
+
106
+ ```ruby
107
+ # config/sidekiq.yml
108
+ :queues:
109
+ - [critical, 2]
110
+ - [support_flows, 1]
111
+ - [default, 1]
112
+ ```
113
+
114
+ ## Passing Parameters to Jobs
115
+
116
+ ### Job Params
117
+
118
+ When using custom job classes, you can pass additional parameters via `use_async`:
119
+
120
+ ```ruby
121
+ # In controller
122
+ config.use_async(MyFlowJob, deployment_id: 123, flow_name: "SupportFlow")
123
+
124
+ # In job execute method
125
+ class MyFlowJob < FlowChat::AsyncJob
126
+ def execute(controller, **job_params)
127
+ deployment_id = job_params[:deployment_id] # => 123
128
+ flow_name = job_params[:flow_name] # => "SupportFlow"
129
+
130
+ # Use params for business logic
131
+ processor = FlowChat::Processor.new(controller) do |config|
132
+ # ... configuration
133
+ end
134
+ processor.run(MyFlow, :start)
135
+ end
136
+ end
137
+ ```
138
+
139
+ **Note:** Request params (from `controller.params`) are automatically available in the background job via the reconstructed `controller.params`. Job params are for additional data specific to job execution logic.
140
+
141
+ ### Factory with GenericAsyncJob
142
+
143
+ When using `factory:` param, additional params are ignored by `GenericAsyncJob` but can be used by the factory:
144
+
145
+ ```ruby
146
+ config.use_async(factory: :whatsapp, extra_data: "value")
147
+ # extra_data is passed to perform but not used by GenericAsyncJob
148
+ ```
149
+
150
+ ## Gateway Support
151
+
152
+ | Gateway | Async Support | Nil Response Handling |
153
+ |---------|--------------|----------------------|
154
+ | WhatsApp Cloud API | ✅ | ✅ Returns silently |
155
+ | Intercom API | ✅ | ✅ Returns silently |
156
+ | HTTP Simple | ✅ | ✅ Returns `{type: :skip}` JSON |
157
+ | USSD Nalo | ❌ (synchronous protocol) | ❌ Requires immediate response |
158
+
159
+ ### Middleware Nil Response Handling
160
+
161
+ Async-capable gateways support middleware that returns `nil` instead of a response tuple. This is useful for middleware that handles responses directly (e.g., `AgentHandoffMiddleware`):
162
+
163
+ ```ruby
164
+ class AgentHandoffMiddleware
165
+ def call(context)
166
+ if should_handoff_to_agent?
167
+ # Middleware handles response directly
168
+ send_to_agent(context)
169
+ return nil # Signal that response was handled
170
+ end
171
+
172
+ @app.call(context) # Continue to next middleware
173
+ end
174
+ end
175
+ ```
176
+
177
+ When middleware returns `nil`:
178
+ - **HTTP Simple**: Returns JSON response `{type: :skip, session_id, user_id, timestamp}`
179
+ - **WhatsApp/Intercom**: Returns silently (message already sent by middleware)
180
+ - **USSD**: Not supported - synchronous protocol requires immediate response
181
+
182
+ **Note**: USSD cannot support middleware that returns nil because the USSD protocol requires an immediate synchronous response to every request.
183
+
184
+ ## How It Works
185
+
186
+ ### Execution Paths
187
+
188
+ FlowChat's async system automatically routes requests through one of three paths:
189
+
190
+ #### 1. Webhook → Async Job (async enabled, webhook context)
191
+
192
+ ```
193
+ 1. Webhook request arrives at controller
194
+ 2. Processor.run() called with use_async configured
195
+ 3. Gateway checks: should_enqueue_async?
196
+ - Not in background? ✅
197
+ - Async enabled? ✅
198
+ - Gateway supports async? ✅
199
+ 4. Gateway serializes request context:
200
+ - params (session_id, user_id, input, etc.)
201
+ - method (POST/GET)
202
+ - headers (Content-Type, User-Agent)
203
+ 5. Gateway enqueues job: JobClass.perform_later(request_context: {...})
204
+ 6. Gateway returns immediately (< 100ms)
205
+ 7. Background worker picks up job
206
+ 8. AsyncJob.perform creates BackgroundController from request_context
207
+ 9. User's execute() called with controller
208
+ 10. Flow processes normally with reconstructed request
209
+ ```
210
+
211
+ #### 2. Background Job → Inline (async enabled, background context)
212
+
213
+ ```
214
+ 1. Background job executes
215
+ 2. AsyncJob.perform creates BackgroundController
216
+ 3. User's execute() creates new processor
217
+ 4. Processor.run() called
218
+ 5. Gateway checks: should_enqueue_async?
219
+ - Not in background? ❌ (BackgroundController detected)
220
+ - Async enabled? ✅
221
+ - Gateway supports async? ✅
222
+ 6. Gateway processes inline (prevents double-enqueueing)
223
+ 7. Flow executes, calls controller.render (no-op in background)
224
+ 8. Job completes
225
+ ```
226
+
227
+ #### 3. Webhook → Inline (async not configured)
228
+
229
+ ```
230
+ 1. Webhook request arrives
231
+ 2. Processor.run() called without use_async
232
+ 3. Gateway checks: should_enqueue_async?
233
+ - Not in background? ✅
234
+ - Async enabled? ❌
235
+ - Gateway supports async? ✅
236
+ 4. Gateway processes inline
237
+ 5. Flow executes
238
+ 6. Response returned to webhook provider
239
+ ```
240
+
241
+ ### Key Components
242
+
243
+ **AsyncJob**
244
+ - Base class users inherit from
245
+ - `perform(request_context:, **job_params)` entry point
246
+ - Creates `BackgroundController` from serialized request
247
+ - Calls user's `execute(controller, **job_params)` method
248
+
249
+ **GenericAsyncJob**
250
+ - Built-in job that uses Factory pattern
251
+ - Automatically used when `use_async(factory: :name)` is called
252
+ - Executes registered factory in background context
253
+ - Validates factory is registered before execution
254
+
255
+ **BackgroundController**
256
+ - Duck-types as Rails controller
257
+ - Provides `request`, `params`, `render`, `head` interface
258
+ - `params` delegates to `request.params` (mimics Rails controller)
259
+ - `render` and `head` are no-ops in background context
260
+ - `is_a?(FlowChat::BackgroundController)` returns true for detection
261
+
262
+ **BackgroundRequest**
263
+ - Reconstructs full Rails request interface from serialized data
264
+ - Provides `params`, `method`, `headers`, `host`, `path`, `remote_ip` accessors
265
+ - HTTP method predicates: `post?`, `get?`, `head?`
266
+ - Rails compatibility methods: `request_method`, `user_agent`, `ssl?`
267
+ - Request body support: `body` returns object with `read()` and `rewind()` methods
268
+ - Body content is serialized from webhook and reconstructed in background
269
+ - Returns empty hash for `cookies` (not available in background context)
270
+
271
+ **GatewayAsyncSupport**
272
+ - Concern mixed into all gateways
273
+ - `should_enqueue_async?` decision logic
274
+ - `in_background?` detection via `BackgroundController`
275
+ - `enqueue_async_job` serializes and enqueues
276
+ - `async_supported?` override for gateways like USSD
277
+
278
+ ### Decision Logic
279
+
280
+ The gateway's `should_enqueue_async?` method checks three conditions:
281
+
282
+ ```ruby
283
+ def should_enqueue_async?
284
+ processor = @context["processor"]
285
+
286
+ !in_background? && # Prevent double-enqueueing
287
+ processor&.async_enabled? && # User opted in
288
+ async_supported? # Gateway allows it
289
+ end
290
+ ```
291
+
292
+ **All three must be true** to enqueue. If any condition is false, the flow processes inline.
293
+
294
+ This ensures:
295
+ - Background jobs never enqueue another job
296
+ - Users explicitly opt in per processor
297
+ - Synchronous protocols (USSD) always process inline
298
+