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.
- checksums.yaml +4 -4
- data/.cliff.toml +74 -0
- data/.github/workflows/ci.yml +2 -3
- data/.github/workflows/release.yml +56 -0
- data/.standard.yml +4 -0
- data/CHANGELOG.md +22 -0
- data/CLAUDE.md +327 -0
- data/CONTRIBUTING.md +134 -0
- data/Gemfile +1 -0
- data/README.md +290 -105
- data/Rakefile +5 -1
- data/SECURITY.md +42 -349
- data/docs/architecture.md +510 -0
- data/docs/async-background-processing.md +298 -0
- data/docs/configuration.md +556 -226
- data/docs/factory-pattern.md +355 -0
- data/docs/gateway-context-variables.md +171 -0
- data/docs/gateway-development.md +723 -0
- data/docs/getting-started.md +429 -0
- data/docs/instrumentation.md +264 -153
- data/docs/platforms/telegram.md +1013 -0
- data/docs/platforms/ussd.md +693 -0
- data/docs/platforms/whatsapp.md +1395 -0
- data/docs/testing.md +243 -365
- data/examples/custom_session_id_example.rb +119 -0
- data/examples/http_controller.rb +11 -11
- data/examples/intercom_configuration_example.rb +118 -0
- data/examples/intercom_controller.rb +194 -0
- data/examples/multi_tenant_whatsapp_controller.rb +4 -4
- data/examples/ussd_controller.rb +9 -9
- data/examples/whatsapp_controller.rb +2 -2
- data/flow_chat.gemspec +4 -0
- data/lib/flow_chat/{base_app.rb → app.rb} +16 -9
- data/lib/flow_chat/async_job.rb +176 -0
- data/lib/flow_chat/config.rb +10 -30
- data/lib/flow_chat/{base_executor.rb → executor.rb} +6 -11
- data/lib/flow_chat/factory.rb +94 -0
- data/lib/flow_chat/gateway_async_support.rb +106 -0
- data/lib/flow_chat/generic_async_job.rb +30 -0
- data/lib/flow_chat/http/gateway/simple.rb +81 -33
- data/lib/flow_chat/http/renderer.rb +3 -3
- data/lib/flow_chat/instrumentation/setup.rb +1 -1
- data/lib/flow_chat/instrumentation.rb +23 -0
- data/lib/flow_chat/intercom/client.rb +155 -0
- data/lib/flow_chat/intercom/configuration.rb +149 -0
- data/lib/flow_chat/intercom/gateway/intercom_api.rb +396 -0
- data/lib/flow_chat/intercom/renderer.rb +71 -0
- data/lib/flow_chat/phone_number_util.rb +37 -35
- data/lib/flow_chat/processor.rb +188 -0
- data/lib/flow_chat/renderers/markdown_support.rb +58 -0
- data/lib/flow_chat/session/middleware.rb +25 -9
- data/lib/flow_chat/telegram/client.rb +240 -0
- data/lib/flow_chat/telegram/configuration.rb +118 -0
- data/lib/flow_chat/telegram/gateway/bot_api.rb +300 -0
- data/lib/flow_chat/telegram/middleware/choice_mapper.rb +35 -0
- data/lib/flow_chat/telegram/renderer.rb +125 -0
- data/lib/flow_chat/telegram.rb +7 -0
- data/lib/flow_chat/ussd/gateway/nalo.rb +24 -4
- data/lib/flow_chat/ussd/middleware/pagination.rb +9 -5
- data/lib/flow_chat/ussd/renderer.rb +1 -1
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +144 -13
- data/lib/flow_chat/whatsapp/configuration.rb +1 -1
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +132 -96
- data/lib/flow_chat/whatsapp/id_generator.rb +124 -0
- data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +147 -0
- data/lib/flow_chat/whatsapp/renderer.rb +145 -11
- data/lib/flow_chat.rb +11 -1
- data/lib/tasks/release.rake +165 -0
- metadata +84 -25
- data/docs/flows.md +0 -320
- data/docs/http-gateway-protocol.md +0 -432
- data/docs/images/simulator.png +0 -0
- data/docs/media.md +0 -153
- data/docs/sessions.md +0 -433
- data/docs/ussd-setup.md +0 -322
- data/docs/whatsapp-setup.md +0 -162
- data/examples/whatsapp_message_job.rb +0 -113
- data/lib/flow_chat/base_processor.rb +0 -146
- data/lib/flow_chat/http/app.rb +0 -6
- data/lib/flow_chat/http/middleware/executor.rb +0 -24
- data/lib/flow_chat/http/processor.rb +0 -33
- data/lib/flow_chat/session/rails_session_store.rb +0 -68
- data/lib/flow_chat/ussd/app.rb +0 -6
- data/lib/flow_chat/ussd/gateway/nsano.rb +0 -96
- data/lib/flow_chat/ussd/middleware/executor.rb +0 -24
- data/lib/flow_chat/ussd/processor.rb +0 -39
- data/lib/flow_chat/whatsapp/app.rb +0 -29
- data/lib/flow_chat/whatsapp/middleware/executor.rb +0 -24
- data/lib/flow_chat/whatsapp/processor.rb +0 -32
- 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
|
+
|