flow_chat 0.9.0 → 0.10.1

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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +29 -0
  3. data/.github/workflows/pages.yml +43 -0
  4. data/.gitignore +4 -1
  5. data/CHANGELOG.md +49 -0
  6. data/README.md +166 -295
  7. data/Rakefile +12 -1
  8. data/SECURITY.md +1 -1
  9. data/docs/architecture.md +52 -479
  10. data/docs/async-background-processing.md +31 -265
  11. data/docs/configuration.md +106 -613
  12. data/docs/factory-pattern.md +27 -324
  13. data/docs/gateway-context-variables.md +140 -143
  14. data/docs/gateway-development.md +86 -650
  15. data/docs/getting-started.md +40 -379
  16. data/docs/instrumentation.md +88 -279
  17. data/docs/platforms/instagram.md +278 -0
  18. data/docs/platforms/messenger.md +205 -0
  19. data/docs/platforms/telegram.md +47 -951
  20. data/docs/platforms/ussd.md +38 -653
  21. data/docs/platforms/whatsapp.md +73 -1321
  22. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md +732 -0
  23. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md.tasks.json +58 -0
  24. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md +4064 -0
  25. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md.tasks.json +226 -0
  26. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md +972 -0
  27. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md.tasks.json +88 -0
  28. data/docs/superpowers/specs/2026-07-09-inbound-media-support-design.md +195 -0
  29. data/docs/superpowers/specs/2026-08-10-messenger-instagram-design.md +391 -0
  30. data/docs/testing.md +26 -297
  31. data/examples/http_controller.rb +12 -10
  32. data/examples/intercom_configuration_example.rb +19 -24
  33. data/examples/intercom_controller.rb +8 -20
  34. data/examples/multi_tenant_whatsapp_controller.rb +61 -166
  35. data/examples/simulator_controller.rb +0 -1
  36. data/examples/ussd_controller.rb +86 -158
  37. data/examples/whatsapp_controller.rb +16 -15
  38. data/examples/whatsapp_media_examples.rb +27 -79
  39. data/flow_chat.gemspec +22 -2
  40. data/gemfiles/rails_main.gemfile +18 -0
  41. data/lib/flow_chat/app.rb +129 -11
  42. data/lib/flow_chat/async_job.rb +10 -0
  43. data/lib/flow_chat/choice_titles.rb +95 -0
  44. data/lib/flow_chat/config.rb +124 -1
  45. data/lib/flow_chat/delivery_error.rb +9 -0
  46. data/lib/flow_chat/gateway_async_support.rb +19 -1
  47. data/lib/flow_chat/http/configuration_error.rb +9 -0
  48. data/lib/flow_chat/http/gateway/simple.rb +24 -4
  49. data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
  50. data/lib/flow_chat/input.rb +86 -0
  51. data/lib/flow_chat/instagram/client.rb +32 -0
  52. data/lib/flow_chat/instagram/configuration.rb +147 -0
  53. data/lib/flow_chat/instagram/configuration_error.rb +7 -0
  54. data/lib/flow_chat/instagram/gateway/send_api.rb +63 -0
  55. data/lib/flow_chat/instagram/middleware/choice_mapper.rb +22 -0
  56. data/lib/flow_chat/instagram/renderer.rb +23 -0
  57. data/lib/flow_chat/instrumentation/metrics_collector.rb +6 -1
  58. data/lib/flow_chat/instrumentation.rb +160 -1
  59. data/lib/flow_chat/intercom/client.rb +34 -28
  60. data/lib/flow_chat/intercom/configuration.rb +2 -49
  61. data/lib/flow_chat/intercom/configuration_error.rb +9 -0
  62. data/lib/flow_chat/intercom/gateway/intercom_api.rb +81 -57
  63. data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
  64. data/lib/flow_chat/intercom/renderer.rb +57 -5
  65. data/lib/flow_chat/media.rb +121 -0
  66. data/lib/flow_chat/messenger/client.rb +264 -0
  67. data/lib/flow_chat/messenger/configuration.rb +103 -0
  68. data/lib/flow_chat/messenger/configuration_error.rb +9 -0
  69. data/lib/flow_chat/messenger/gateway/send_api.rb +42 -0
  70. data/lib/flow_chat/messenger/middleware/choice_mapper.rb +185 -0
  71. data/lib/flow_chat/messenger/renderer.rb +150 -0
  72. data/lib/flow_chat/meta/challenge.rb +24 -0
  73. data/lib/flow_chat/meta/choice_ladder.rb +37 -0
  74. data/lib/flow_chat/meta/configuration_error.rb +7 -0
  75. data/lib/flow_chat/meta/gateway_identity.rb +38 -0
  76. data/lib/flow_chat/meta/messaging_gateway.rb +471 -0
  77. data/lib/flow_chat/meta/signature.rb +30 -0
  78. data/lib/flow_chat/meta/signature_validation.rb +66 -0
  79. data/lib/flow_chat/meta/webhook_verification.rb +43 -0
  80. data/lib/flow_chat/named_configuration.rb +65 -0
  81. data/lib/flow_chat/prompt.rb +13 -16
  82. data/lib/flow_chat/renderers/markdown_support.rb +109 -0
  83. data/lib/flow_chat/security.rb +76 -0
  84. data/lib/flow_chat/session/middleware.rb +11 -2
  85. data/lib/flow_chat/simulator/controller.rb +31 -15
  86. data/lib/flow_chat/simulator/views/simulator.html.erb +184 -20
  87. data/lib/flow_chat/telegram/client.rb +47 -4
  88. data/lib/flow_chat/telegram/configuration.rb +2 -42
  89. data/lib/flow_chat/telegram/configuration_error.rb +9 -0
  90. data/lib/flow_chat/telegram/gateway/bot_api.rb +46 -28
  91. data/lib/flow_chat/telegram/middleware/choice_mapper.rb +77 -16
  92. data/lib/flow_chat/telegram/renderer.rb +10 -2
  93. data/lib/flow_chat/text_truncator.rb +75 -0
  94. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
  95. data/lib/flow_chat/version.rb +1 -1
  96. data/lib/flow_chat/whatsapp/client.rb +14 -7
  97. data/lib/flow_chat/whatsapp/configuration.rb +12 -51
  98. data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
  99. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +259 -203
  100. data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +144 -57
  101. data/lib/flow_chat/whatsapp/renderer.rb +121 -60
  102. data/lib/flow_chat.rb +0 -10
  103. data/lib/tasks/release.rake +11 -1
  104. data/site/.nojekyll +0 -0
  105. data/site/.og-card.html +89 -0
  106. data/site/favicon.svg +6 -0
  107. data/site/index.html +209 -0
  108. data/site/og.png +0 -0
  109. metadata +60 -11
  110. data/lib/flow_chat/whatsapp/id_generator.rb +0 -124
@@ -1,355 +1,58 @@
1
- # Factory Pattern
1
+ # Factory pattern
2
2
 
3
- The Factory pattern provides centralized processor configuration, eliminating duplication between webhook controllers and background jobs.
3
+ A factory is a named block that builds and runs a processor. You register it once, then call it by name from both the webhook and the background job. This keeps the processor configuration in one place, so the two contexts cannot drift apart.
4
4
 
5
- ## Quick Start
5
+ Without a factory, a webhook that enqueues a background job needs the same processor setup written twice: once in the controller and once in the job. A factory removes the duplication.
6
6
 
7
- ### 1. Register Factories
7
+ ## Registering and executing
8
8
 
9
- Register your processor configurations once in an initializer:
9
+ Register the factory in an initializer:
10
10
 
11
11
  ```ruby
12
12
  # config/initializers/flow_chat.rb
13
-
14
- FlowChat::Factory.register :whatsapp do |controller|
13
+ FlowChat::Factory.register(:whatsapp) do |controller|
15
14
  processor = FlowChat::Processor.new(controller) do |config|
16
15
  config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
17
16
  config.use_session_store FlowChat::Session::CacheSessionStore
18
- config.use_session_config(boundaries: [:flow])
17
+ config.use_async(factory: :whatsapp)
19
18
  end
20
19
  processor.run(WhatsAppFlow, :start)
21
20
  end
22
-
23
- FlowChat::Factory.register :intercom do |controller|
24
- processor = FlowChat::Processor.new(controller) do |config|
25
- config.use_gateway FlowChat::Intercom::Gateway::IntercomApi
26
- config.use_session_store FlowChat::Session::CacheSessionStore
27
- config.use_session_config(boundaries: [:conversation], identifier: :conversation_id)
28
- end
29
- processor.run(IntercomFlow, :start)
30
- end
31
21
  ```
32
22
 
33
- ### 2. Use in Controllers
34
-
35
- Execute factories directly in webhook controllers:
23
+ Execute it from the webhook controller:
36
24
 
37
25
  ```ruby
38
- # app/controllers/webhooks_controller.rb
39
- class WebhooksController < ApplicationController
40
- def whatsapp
41
- FlowChat::Factory.execute(:whatsapp, controller: self)
42
- end
43
-
44
- def intercom
45
- FlowChat::Factory.execute(:intercom, controller: self)
46
- end
47
- end
48
- ```
49
-
50
- ### 3. Use with Async (Recommended)
51
-
52
- Register factory with async support for background processing:
26
+ # app/controllers/whatsapp_controller.rb
27
+ class WhatsappController < ApplicationController
28
+ skip_forgery_protection
53
29
 
54
- ```ruby
55
- # config/initializers/flow_chat.rb
56
- FlowChat::Factory.register :whatsapp do |controller|
57
- processor = FlowChat::Processor.new(controller) do |config|
58
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
59
- config.use_session_store FlowChat::Session::CacheSessionStore
60
- config.use_session_config(boundaries: [:flow])
61
- config.use_async(factory: :whatsapp) # Self-referencing for async
62
- end
63
- processor.run(WhatsAppFlow, :start)
64
- end
65
-
66
- # app/controllers/webhooks_controller.rb
67
- class WebhooksController < ApplicationController
68
- def whatsapp
30
+ def webhook
69
31
  FlowChat::Factory.execute(:whatsapp, controller: self)
70
32
  end
71
33
  end
72
34
  ```
73
35
 
74
- **How it works:**
75
- 1. Webhook receives request
76
- 2. `use_async(factory: :whatsapp)` automatically uses `GenericAsyncJob`
77
- 3. Job is enqueued with `factory: :whatsapp` parameter
78
- 4. Background worker executes `FlowChat::Factory.execute(:whatsapp, controller: background_controller)`
79
- 5. Flow processes in background with same configuration
80
-
81
- ## Benefits
82
-
83
- ### 1. Single Source of Truth
84
-
85
- Define processor configuration once, use everywhere:
86
-
87
- ```ruby
88
- # Before: Duplicated configuration
89
- # Webhook controller
90
- processor = FlowChat::Processor.new(self) do |config|
91
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
92
- config.use_session_store FlowChat::Session::CacheSessionStore
93
- config.use_session_config(boundaries: [:flow])
94
- end
95
-
96
- # Background job (duplicate!)
97
- processor = FlowChat::Processor.new(controller) do |config|
98
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
99
- config.use_session_store FlowChat::Session::CacheSessionStore
100
- config.use_session_config(boundaries: [:flow])
101
- end
102
-
103
- # After: Single registration
104
- FlowChat::Factory.register :whatsapp do |controller|
105
- processor = FlowChat::Processor.new(controller) do |config|
106
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
107
- config.use_session_store FlowChat::Session::CacheSessionStore
108
- config.use_session_config(boundaries: [:flow])
109
- end
110
- processor.run(WhatsAppFlow, :start)
111
- end
112
-
113
- # Use everywhere with one line
114
- FlowChat::Factory.execute(:whatsapp, controller: self)
115
- ```
116
-
117
- ### 2. No Custom Job Classes
118
-
119
- For simple cases, use `GenericAsyncJob` automatically:
120
-
121
- ```ruby
122
- # No need to create MyFlowJob class
123
- config.use_async(factory: :whatsapp)
124
- ```
125
-
126
- ### 3. Context-Aware Execution
127
-
128
- Same factory works in both webhook and background contexts:
129
-
130
- ```ruby
131
- # Webhook context - processes inline or enqueues
132
- FlowChat::Factory.execute(:whatsapp, controller: webhook_controller)
133
-
134
- # Background context - always processes inline
135
- FlowChat::Factory.execute(:whatsapp, controller: background_controller)
136
- ```
137
-
138
- Gateways automatically detect the context and prevent double-enqueueing.
139
-
140
- ### 4. Easy Testing
141
-
142
- Test factories in isolation:
143
-
144
- ```ruby
145
- RSpec.describe "WhatsApp Factory" do
146
- it "executes successfully" do
147
- controller = mock_controller
148
- expect {
149
- FlowChat::Factory.execute(:whatsapp, controller: controller)
150
- }.not_to raise_error
151
- end
152
- end
153
- ```
154
-
155
- ## API Reference
156
-
157
- ### Registration
158
-
159
- **`FlowChat::Factory.register(name, &block)`**
160
-
161
- Register a factory with a given name.
162
-
163
- ```ruby
164
- FlowChat::Factory.register :my_flow do |controller|
165
- # Build and run processor
166
- processor = FlowChat::Processor.new(controller) do |config|
167
- # ... configuration
168
- end
169
- processor.run(MyFlow, :start)
170
- end
171
- ```
172
-
173
- **Parameters:**
174
- - `name` (Symbol): Unique factory identifier
175
- - `block` (Proc): Factory block receiving `controller`
176
-
177
- **Returns:** `nil`
178
-
179
- ### Execution
180
-
181
- **`FlowChat::Factory.execute(name, controller:)`**
182
-
183
- Execute a registered factory.
184
-
185
- ```ruby
186
- FlowChat::Factory.execute(:my_flow, controller: self)
187
- ```
36
+ The block receives a controller and returns whatever `processor.run` returns. Executing an unregistered name raises `FlowChat::Factory::FactoryNotFoundError`.
188
37
 
189
- **Parameters:**
190
- - `name` (Symbol): Factory name to execute
191
- - `controller` (Object): Rails controller or BackgroundController
38
+ ## How it pairs with async
192
39
 
193
- **Returns:** Result of factory block
40
+ Notice the factory references itself: `config.use_async(factory: :whatsapp)`. That closes the loop.
194
41
 
195
- **Raises:**
196
- - `FlowChat::Factory::FactoryNotFoundError` if factory not registered
42
+ 1. The webhook calls `Factory.execute(:whatsapp, controller: self)`.
43
+ 2. The factory builds a processor with `use_async(factory: :whatsapp)`, so the gateway enqueues `FlowChat::GenericAsyncJob` with `factory: :whatsapp` and returns immediately.
44
+ 3. The background job calls `Factory.execute(:whatsapp, controller: background_controller)` again.
45
+ 4. This time the gateway is running in the background, so it processes the flow inline and sends the reply.
197
46
 
198
- ### Introspection
47
+ The same factory builds the processor in both passes, so there is one definition of the gateway, session store, and flow. See [async-background-processing.md](async-background-processing.md) for what happens inside the job.
199
48
 
200
- **`FlowChat::Factory.registered?(name)`**
49
+ ## Other methods
201
50
 
202
- Check if factory is registered.
203
-
204
- ```ruby
205
- FlowChat::Factory.registered?(:whatsapp) # => true
206
- ```
207
-
208
- **`FlowChat::Factory.registered_factories`**
209
-
210
- Get all registered factory names.
211
-
212
- ```ruby
213
- FlowChat::Factory.registered_factories # => [:whatsapp, :intercom, :ussd]
214
- ```
215
-
216
- **`FlowChat::Factory.clear!`**
217
-
218
- Clear all registered factories (primarily for testing).
219
-
220
- ```ruby
221
- FlowChat::Factory.clear!
222
- ```
223
-
224
- ## Advanced Patterns
225
-
226
- ### Environment-Specific Factories
227
-
228
- Create different configurations per environment:
229
-
230
- ```ruby
231
- if Rails.env.production?
232
- FlowChat::Factory.register :whatsapp do |controller|
233
- processor = FlowChat::Processor.new(controller) do |config|
234
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
235
- config.use_session_store FlowChat::Session::CacheSessionStore
236
- end
237
- processor.run(ProductionFlow, :start)
238
- end
239
- else
240
- FlowChat::Factory.register :whatsapp do |controller|
241
- processor = FlowChat::Processor.new(controller) do |config|
242
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
243
- config.use_session_store FlowChat::Session::RailsSessionStore
244
- end
245
- processor.run(DevelopmentFlow, :start)
246
- end
247
- end
248
- ```
249
-
250
- ### Multiple Variants
251
-
252
- Register multiple factories for the same platform:
253
-
254
- ```ruby
255
- FlowChat::Factory.register :whatsapp_support do |controller|
256
- processor = FlowChat::Processor.new(controller) do |config|
257
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
258
- config.use_session_store FlowChat::Session::CacheSessionStore
259
- end
260
- processor.run(SupportFlow, :start)
261
- end
262
-
263
- FlowChat::Factory.register :whatsapp_sales do |controller|
264
- processor = FlowChat::Processor.new(controller) do |config|
265
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
266
- config.use_session_store FlowChat::Session::CacheSessionStore
267
- end
268
- processor.run(SalesFlow, :start)
269
- end
270
-
271
- # Use based on routing
272
- def whatsapp
273
- factory = params[:department] == 'sales' ? :whatsapp_sales : :whatsapp_support
274
- FlowChat::Factory.execute(factory, controller: self)
275
- end
276
- ```
277
-
278
- ### Accessing Request Data
279
-
280
- Factories receive the controller, which includes request data:
281
-
282
- ```ruby
283
- FlowChat::Factory.register :whatsapp do |controller|
284
- # Access request params
285
- user_id = controller.params[:user_id]
286
- platform = controller.request.headers["User-Agent"]
287
-
288
- processor = FlowChat::Processor.new(controller) do |config|
289
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
290
- config.use_session_store FlowChat::Session::CacheSessionStore
291
- end
292
-
293
- # Route to different flows based on params
294
- flow = user_id.start_with?('premium_') ? PremiumFlow : StandardFlow
295
- processor.run(flow, :start)
296
- end
297
- ```
298
-
299
- ## Migration Guide
300
-
301
- ### From Duplicate Configuration
302
-
303
- **Before:**
304
- ```ruby
305
- # Webhook controller
306
- class WebhooksController < ApplicationController
307
- def whatsapp
308
- processor = FlowChat::Processor.new(self) do |config|
309
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
310
- config.use_session_store FlowChat::Session::CacheSessionStore
311
- config.use_async(WhatsAppFlowJob)
312
- end
313
- processor.run(WhatsAppFlow, :start)
314
- end
315
- end
316
-
317
- # Background job (duplicate!)
318
- class WhatsAppFlowJob < FlowChat::AsyncJob
319
- def execute(controller, **job_params)
320
- processor = FlowChat::Processor.new(controller) do |config|
321
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
322
- config.use_session_store FlowChat::Session::CacheSessionStore
323
- end
324
- processor.run(WhatsAppFlow, :start)
325
- end
326
- end
327
- ```
328
-
329
- **After:**
330
- ```ruby
331
- # config/initializers/flow_chat.rb
332
- FlowChat::Factory.register :whatsapp do |controller|
333
- processor = FlowChat::Processor.new(controller) do |config|
334
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
335
- config.use_session_store FlowChat::Session::CacheSessionStore
336
- config.use_async(factory: :whatsapp) # Self-referencing for async
337
- end
338
- processor.run(WhatsAppFlow, :start)
339
- end
340
-
341
- # Webhook controller - ONE LINE!
342
- class WebhooksController < ApplicationController
343
- def whatsapp
344
- FlowChat::Factory.execute(:whatsapp, controller: self)
345
- end
346
- end
347
-
348
- # No custom job class needed!
349
- ```
51
+ - `FlowChat::Factory.registered?(:whatsapp)` returns whether a name is registered.
52
+ - `FlowChat::Factory.registered_factories` lists the registered names.
53
+ - `FlowChat::Factory.clear!` removes all registrations (useful in tests).
350
54
 
351
- ## See Also
55
+ ## Related
352
56
 
353
- - [Async Background Processing](async-background-processing.md) - Using factories with async jobs
354
- - [Session Management](session-management.md) - Configuring session stores in factories
355
- - [Gateways](gateways.md) - Platform-specific gateway configuration
57
+ - [Async and background processing](async-background-processing.md)
58
+ - [Configuration](configuration.md#async-processing)