flow_chat 0.9.0 → 0.10.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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pages.yml +43 -0
  3. data/CHANGELOG.md +26 -0
  4. data/README.md +166 -295
  5. data/Rakefile +12 -1
  6. data/SECURITY.md +1 -1
  7. data/docs/architecture.md +52 -479
  8. data/docs/async-background-processing.md +31 -265
  9. data/docs/configuration.md +106 -613
  10. data/docs/factory-pattern.md +27 -324
  11. data/docs/gateway-context-variables.md +140 -143
  12. data/docs/gateway-development.md +86 -650
  13. data/docs/getting-started.md +40 -379
  14. data/docs/instrumentation.md +88 -279
  15. data/docs/platforms/instagram.md +278 -0
  16. data/docs/platforms/messenger.md +205 -0
  17. data/docs/platforms/telegram.md +47 -951
  18. data/docs/platforms/ussd.md +38 -653
  19. data/docs/platforms/whatsapp.md +73 -1321
  20. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md +732 -0
  21. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md.tasks.json +58 -0
  22. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md +4064 -0
  23. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md.tasks.json +226 -0
  24. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md +972 -0
  25. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md.tasks.json +88 -0
  26. data/docs/superpowers/specs/2026-07-09-inbound-media-support-design.md +195 -0
  27. data/docs/superpowers/specs/2026-08-10-messenger-instagram-design.md +391 -0
  28. data/docs/testing.md +26 -297
  29. data/examples/http_controller.rb +12 -10
  30. data/examples/intercom_configuration_example.rb +19 -24
  31. data/examples/intercom_controller.rb +8 -20
  32. data/examples/multi_tenant_whatsapp_controller.rb +61 -166
  33. data/examples/simulator_controller.rb +0 -1
  34. data/examples/ussd_controller.rb +86 -158
  35. data/examples/whatsapp_controller.rb +16 -15
  36. data/examples/whatsapp_media_examples.rb +27 -79
  37. data/lib/flow_chat/app.rb +129 -11
  38. data/lib/flow_chat/choice_titles.rb +95 -0
  39. data/lib/flow_chat/config.rb +124 -1
  40. data/lib/flow_chat/delivery_error.rb +9 -0
  41. data/lib/flow_chat/http/configuration_error.rb +9 -0
  42. data/lib/flow_chat/http/gateway/simple.rb +24 -4
  43. data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
  44. data/lib/flow_chat/input.rb +86 -0
  45. data/lib/flow_chat/instagram/client.rb +32 -0
  46. data/lib/flow_chat/instagram/configuration.rb +147 -0
  47. data/lib/flow_chat/instagram/configuration_error.rb +7 -0
  48. data/lib/flow_chat/instagram/gateway/send_api.rb +63 -0
  49. data/lib/flow_chat/instagram/middleware/choice_mapper.rb +22 -0
  50. data/lib/flow_chat/instagram/renderer.rb +23 -0
  51. data/lib/flow_chat/instrumentation/metrics_collector.rb +6 -1
  52. data/lib/flow_chat/instrumentation.rb +160 -1
  53. data/lib/flow_chat/intercom/client.rb +34 -28
  54. data/lib/flow_chat/intercom/configuration.rb +2 -49
  55. data/lib/flow_chat/intercom/configuration_error.rb +9 -0
  56. data/lib/flow_chat/intercom/gateway/intercom_api.rb +81 -57
  57. data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
  58. data/lib/flow_chat/intercom/renderer.rb +57 -5
  59. data/lib/flow_chat/media.rb +121 -0
  60. data/lib/flow_chat/messenger/client.rb +264 -0
  61. data/lib/flow_chat/messenger/configuration.rb +103 -0
  62. data/lib/flow_chat/messenger/configuration_error.rb +9 -0
  63. data/lib/flow_chat/messenger/gateway/send_api.rb +42 -0
  64. data/lib/flow_chat/messenger/middleware/choice_mapper.rb +185 -0
  65. data/lib/flow_chat/messenger/renderer.rb +150 -0
  66. data/lib/flow_chat/meta/challenge.rb +24 -0
  67. data/lib/flow_chat/meta/choice_ladder.rb +37 -0
  68. data/lib/flow_chat/meta/configuration_error.rb +7 -0
  69. data/lib/flow_chat/meta/gateway_identity.rb +38 -0
  70. data/lib/flow_chat/meta/messaging_gateway.rb +468 -0
  71. data/lib/flow_chat/meta/signature.rb +30 -0
  72. data/lib/flow_chat/meta/signature_validation.rb +66 -0
  73. data/lib/flow_chat/meta/webhook_verification.rb +43 -0
  74. data/lib/flow_chat/named_configuration.rb +65 -0
  75. data/lib/flow_chat/prompt.rb +13 -16
  76. data/lib/flow_chat/renderers/markdown_support.rb +109 -0
  77. data/lib/flow_chat/security.rb +76 -0
  78. data/lib/flow_chat/session/middleware.rb +11 -2
  79. data/lib/flow_chat/simulator/controller.rb +31 -15
  80. data/lib/flow_chat/simulator/views/simulator.html.erb +184 -20
  81. data/lib/flow_chat/telegram/client.rb +47 -4
  82. data/lib/flow_chat/telegram/configuration.rb +2 -42
  83. data/lib/flow_chat/telegram/configuration_error.rb +9 -0
  84. data/lib/flow_chat/telegram/gateway/bot_api.rb +46 -28
  85. data/lib/flow_chat/telegram/middleware/choice_mapper.rb +77 -16
  86. data/lib/flow_chat/telegram/renderer.rb +10 -2
  87. data/lib/flow_chat/text_truncator.rb +75 -0
  88. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
  89. data/lib/flow_chat/version.rb +1 -1
  90. data/lib/flow_chat/whatsapp/client.rb +14 -7
  91. data/lib/flow_chat/whatsapp/configuration.rb +12 -51
  92. data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
  93. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +254 -203
  94. data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +144 -57
  95. data/lib/flow_chat/whatsapp/renderer.rb +121 -60
  96. data/lib/flow_chat.rb +0 -10
  97. data/site/.nojekyll +0 -0
  98. data/site/.og-card.html +89 -0
  99. data/site/favicon.svg +6 -0
  100. data/site/index.html +209 -0
  101. data/site/og.png +0 -0
  102. metadata +51 -3
  103. data/lib/flow_chat/whatsapp/id_generator.rb +0 -124
data/docs/architecture.md CHANGED
@@ -1,510 +1,83 @@
1
- # FlowChat Architecture
1
+ # Architecture
2
2
 
3
- FlowChat is built around a **composition-based architecture** with **pluggable gateways** that enables unified conversational interfaces across multiple platforms. This document explains the core architectural decisions and how they enable FlowChat's flexibility.
3
+ FlowChat turns a stateless webhook into a stateful conversation by re-running your flow method from the top on every request and replaying its answers out of a session. This document explains the pieces that make that work.
4
4
 
5
- ## Design Principles
5
+ ## The middleware stack
6
6
 
7
- ### 1. Platform Abstraction
7
+ A `FlowChat::Processor` builds one middleware stack per request, in a fixed order. Each layer wraps the next and calls it:
8
8
 
9
- FlowChat abstracts platform differences through a unified API, allowing the same flow code to work across USSD, WhatsApp, Telegram, HTTP, and custom platforms.
10
-
11
- ```ruby
12
- # This exact code works on ALL platforms
13
- def survey_flow
14
- name = app.screen(:name) { |p| p.ask "What's your name?" }
15
- rating = app.screen(:rating) { |p| p.select "Rate us:", ["1", "2", "3", "4", "5"] }
16
- app.say "Thanks #{name}! You rated us #{rating}/5"
17
- end
18
- ```
19
-
20
- ### 2. Composition Over Inheritance
21
-
22
- Rather than platform-specific classes, FlowChat uses composition to configure behavior:
23
-
24
- ```ruby
25
- # Old approach (inheritance) - DEPRECATED
26
- class UssdApp < FlowChat::BaseApp; end
27
- class WhatsappApp < FlowChat::BaseApp; end
28
-
29
- # New approach (composition) - CURRENT
30
- processor = FlowChat::Processor.new(self) do |config|
31
- config.use_gateway FlowChat::Ussd::Gateway::Nalo # Platform behavior
32
- config.use_session_store FlowChat::Session::CacheSessionStore # Session behavior
33
- config.use_middleware CustomLoggingMiddleware # Custom behavior
34
- end
35
9
  ```
36
-
37
- ### 3. Pluggable Gateways
38
-
39
- Each platform can have multiple gateway implementations, making FlowChat extensible:
40
-
41
- ```ruby
42
- # Built-in USSD gateway
43
- config.use_gateway FlowChat::Ussd::Gateway::Nalo
44
-
45
- # Example custom USSD gateways (you would build these)
46
- config.use_gateway YourCompany::Ussd::Gateway::Africaist
47
- config.use_gateway YourCompany::Ussd::Gateway::MTN
48
-
49
- # Built-in WhatsApp gateway
50
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
51
-
52
- # Example custom WhatsApp gateways (you would build these)
53
- config.use_gateway YourCompany::Whatsapp::Gateway::OnPremise
54
- config.use_gateway YourCompany::Whatsapp::Gateway::Twilio
55
-
56
- # Example custom platforms (you would build these)
57
- config.use_gateway YourCompany::Sms::Gateway::Twilio
58
- config.use_gateway YourCompany::Voice::Gateway::Plivo
59
- config.use_gateway YourCompany::Slack::Gateway::BoltJS
10
+ Gateway
11
+ -> Session::Middleware
12
+ -> platform middleware (and your custom middleware)
13
+ -> Executor
14
+ -> your Flow
60
15
  ```
61
16
 
62
- ## Core Components
63
-
64
- ### Processor
65
-
66
- The `FlowChat::Processor` is the central orchestrator that builds and executes the middleware stack.
67
-
68
- ```ruby
69
- processor = FlowChat::Processor.new(controller) do |config|
70
- config.use_gateway GatewayClass, *gateway_args
71
- config.use_session_store SessionStoreClass
72
- config.use_middleware CustomMiddleware
73
- config.use_session_config(boundaries: [:flow], identifier: :msisdn)
74
- end
75
-
76
- processor.run FlowClass, :action_method
77
- ```
17
+ The order is set in `Processor#create_middleware_stack`:
78
18
 
79
- **Responsibilities:**
80
- - Build middleware stack based on configuration
81
- - Manage session configuration
82
- - Coordinate gateway and middleware interaction
83
- - Handle errors and instrumentation
19
+ 1. **Gateway** runs first. It parses the platform's webhook into normalized context values (`request.msisdn`, `context.input`, and so on) and, after the inner stack returns, renders the result back to the platform. See [gateway-development.md](gateway-development.md).
20
+ 2. **`Session::Middleware`** computes the session id from the configured boundaries and identifier, and attaches the session store. See [configuration.md](configuration.md#sessions).
21
+ 3. **Platform middleware** is inserted by the gateway if it defines `configure_middleware_stack`. USSD uses this to add pagination and choice-number mapping, and it is where your custom middleware runs. A gateway without that hook just runs your custom middleware here.
22
+ 4. **Executor** runs last. Nothing runs after it. It instantiates your flow and calls the action.
84
23
 
85
- ### Gateway
24
+ ## The Executor and control flow by exception
86
25
 
87
- Gateways handle platform-specific request parsing and response rendering.
26
+ `FlowChat::Executor` builds a `FlowChat::App`, instantiates your flow with it, and calls the action method. Control flow is driven by exceptions raised from inside the flow and caught here:
88
27
 
89
28
  ```ruby
90
- class MyCustomGateway
91
- def initialize(app, *config_args)
92
- @app = app
93
- @config = config_args.first
94
- end
95
-
96
- def call(context)
97
- # 1. Parse platform-specific request
98
- context["request.msisdn"] = extract_phone_number(request)
99
- context["request.platform"] = :my_platform
100
- context.input = extract_user_input(request)
101
-
102
- # 2. Process through middleware stack
103
- type, prompt, choices, media = @app.call(context)
104
-
105
- # 3. Render platform-specific response
106
- send_platform_response(type, prompt, choices, media)
107
- end
108
-
109
- # Optional: Configure platform-specific middleware
110
- def self.configure_middleware_stack(builder, custom_middleware)
111
- builder.use MyPlatform::SpecialMiddleware
112
- builder.use custom_middleware
113
- builder.use MyPlatform::ResponseMiddleware
114
- end
115
- end
116
- ```
117
-
118
- **Gateway Interface:**
119
- - `initialize(app, *args)` - Set up gateway with app and config
120
- - `call(context)` - Process request through middleware stack
121
- - `self.configure_middleware_stack(builder, custom_middleware)` - Optional middleware configuration
122
-
123
- ### App
124
-
125
- The `FlowChat::App` provides the unified interface that flows use to interact with users.
126
-
127
- ```ruby
128
- class FlowChat::App
129
- def screen(key, &block)
130
- # Automatic state management and navigation
131
- end
132
-
133
- def say(message, media: nil)
134
- # Platform-appropriate message sending
135
- end
136
-
137
- # Context accessors that work across platforms
138
- def msisdn, user_id, platform, gateway
139
- def message_id, timestamp, contact_name, location, media
140
- end
141
- ```
142
-
143
- **Key Features:**
144
- - **Screen-based navigation** with automatic state management
145
- - **Platform-agnostic prompts** that adapt to each platform's capabilities
146
- - **Consistent context** regardless of underlying platform
147
- - **Session integration** for data persistence
148
-
149
- ### Session
150
-
151
- Session management with configurable boundaries and storage backends.
152
-
153
- ```ruby
154
- # Session boundaries control session ID generation
155
- config.use_session_config(
156
- boundaries: [:flow, :platform, :gateway, :url], # Session isolation
157
- identifier: :msisdn, # Session key type
158
- hash_identifiers: true # Privacy protection
159
- )
160
-
161
- # Session stores control where data is stored
162
- config.use_session_store FlowChat::Session::CacheSessionStore # Rails sessions
163
- config.use_session_store FlowChat::Session::CacheSessionStore # Rails cache
164
- config.use_session_store YourCompany::Session::DatabaseStore # Custom store
165
- ```
166
-
167
- ### Middleware
168
-
169
- Extensible processing pipeline for custom logic.
170
-
171
- ```ruby
172
- class CustomMiddleware
173
- def initialize(app)
174
- @app = app
175
- end
176
-
177
- def call(context)
178
- # Before processing
179
- log_request(context)
180
-
181
- # Process through stack
182
- result = @app.call(context)
183
-
184
- # After processing
185
- log_response(context, result)
186
-
187
- result
188
- end
189
- end
190
- ```
191
-
192
- ## Request Processing Flow
193
-
194
- Here's how a request flows through FlowChat:
195
-
196
- ```mermaid
197
- graph TD
198
- A[Platform Request] --> B[Gateway]
199
- B --> C[Parse Request]
200
- C --> D[Set Context]
201
- D --> E[Session Middleware]
202
- E --> F[Custom Middleware]
203
- F --> G[Executor]
204
- G --> H[Flow/Action]
205
- H --> I[App.screen]
206
- I --> J[Prompt Processing]
207
- J --> K[Response Generation]
208
- K --> L[Gateway Response]
209
- L --> M[Platform Response]
210
- ```
211
-
212
- ### Detailed Flow
213
-
214
- 1. **Request Arrives** at platform (USSD, WhatsApp, HTTP)
215
- 2. **Gateway** parses platform-specific request format
216
- 3. **Context Setup** with normalized request data
217
- 4. **Session Middleware** manages session state and boundaries
218
- 5. **Custom Middleware** processes business logic
219
- 6. **Executor** instantiates and calls flow
220
- 7. **Flow Method** uses `app.screen()` for conversation logic
221
- 8. **Prompt Processing** handles user input and validation
222
- 9. **Response Generation** creates platform-agnostic response
223
- 10. **Gateway Rendering** converts to platform-specific format
224
- 11. **Platform Response** sent back to user
225
-
226
- ## Middleware Stack
227
-
228
- The middleware stack is built dynamically based on gateway capabilities:
229
-
230
- ```ruby
231
- def create_middleware_stack
232
- middleware_stack = ::Middleware::Builder.new(name: @gateway_class.name) do |b|
233
- # Gateway always comes first
234
- b.use @gateway_class, *@gateway_args
235
-
236
- # Session middleware sets up session boundaries
237
- b.use FlowChat::Session::Middleware, @session_options
238
-
239
- # Platform-specific middleware (if gateway supports it)
240
- if @gateway_class.respond_to?(:configure_middleware_stack)
241
- @gateway_class.configure_middleware_stack(b, custom_middleware)
242
- else
243
- b.use custom_middleware
244
- end
245
-
246
- # Executor always goes last
247
- b.use FlowChat::Executor
248
- end
249
-
250
- # Conditionally inject logger based on configuration
251
- middleware_stack.inject_logger(FlowChat.logger) if FlowChat::Config.inject_middleware_logger
252
-
253
- middleware_stack
254
- end
29
+ flow = flow_class.new(app)
30
+ flow.send(action)
31
+ raise FlowChat::Interrupt::Terminate, "Unexpected end of flow."
32
+ rescue FlowChat::Interrupt::RestartFlow
33
+ retry
34
+ rescue FlowChat::Interrupt::Prompt => e
35
+ [:prompt, e.prompt, e.choices, e.media]
36
+ rescue FlowChat::Interrupt::Terminate => e
37
+ context.session.destroy
38
+ [:terminal, e.prompt, nil, e.media]
255
39
  ```
256
40
 
257
- ### Platform-Specific Middleware
41
+ The three interrupts live in `FlowChat::Interrupt`:
258
42
 
259
- Gateways can configure their own middleware stack:
43
+ | Interrupt | Raised by | Effect |
44
+ |---|---|---|
45
+ | `Prompt` | `prompt.ask` / `select` / `yes?` when no input is available | Unwinds to the Executor, which returns the prompt for the gateway to render. Carries the message, choices, and media. |
46
+ | `Terminate` | `app.say` and `prompt.say` | Ends the flow, destroys the session, and returns a terminal message. |
47
+ | `RestartFlow` | `app.go_back` | Caught with `retry`, which re-runs the flow from the top against a fresh `App`. |
260
48
 
261
- ```ruby
262
- class FlowChat::Ussd::Gateway::Nalo
263
- def self.configure_middleware_stack(builder, custom_middleware)
264
- builder.use FlowChat::Ussd::Middleware::Pagination
265
- builder.use custom_middleware
266
- builder.use FlowChat::Ussd::Middleware::ChoiceMapper
267
- end
268
- end
269
-
270
- class FlowChat::Whatsapp::Gateway::CloudApi
271
- def self.configure_middleware_stack(builder, custom_middleware)
272
- builder.use FlowChat::Whatsapp::Middleware::MediaProcessor
273
- builder.use custom_middleware
274
- builder.use FlowChat::Whatsapp::Middleware::TemplateRenderer
275
- end
276
- end
277
- ```
278
-
279
- ## Session Architecture
280
-
281
- ### Session ID Generation
282
-
283
- Session IDs are generated based on configurable boundaries:
284
-
285
- ```ruby
286
- # Example session IDs based on boundaries
287
- boundaries: [:flow] # => "survey_flow:user123"
288
- boundaries: [:flow, :platform] # => "survey_flow:ussd:user123"
289
- boundaries: [:flow, :platform, :gateway] # => "survey_flow:ussd:nalo:user123"
290
- boundaries: [:flow, :url] # => "survey_flow:tenant1.app.com:user123"
291
- ```
292
-
293
- ### Session Stores
294
-
295
- Different storage backends for different use cases:
296
-
297
- ```ruby
298
- # Rails sessions (shorter-lived, good for testing)
299
- FlowChat::Session::CacheSessionStore
300
-
301
- # Rails cache (longer-lived, good for production)
302
- FlowChat::Session::CacheSessionStore
303
-
304
- # Custom database store (permanent storage)
305
- class CustomDatabaseStore
306
- def initialize(context)
307
- @context = context
308
- end
309
-
310
- def get(key)
311
- SessionData.find_by(session_id: @context["session.id"], key: key)&.value
312
- end
313
-
314
- def set(key, value)
315
- SessionData.upsert({
316
- session_id: @context["session.id"],
317
- key: key,
318
- value: value
319
- })
320
- end
321
- end
322
- ```
323
-
324
- ## Extensibility Points
325
-
326
- FlowChat provides multiple extension points:
327
-
328
- ### 1. Custom Gateways
329
-
330
- Implement any platform by creating a gateway:
331
-
332
- ```ruby
333
- class YourCompany::Telegram::Gateway::BotAPI
334
- def initialize(app, bot_token)
335
- @app = app
336
- @bot_token = bot_token
337
- end
338
-
339
- def call(context)
340
- # Parse Telegram webhook
341
- update = JSON.parse(context.controller.request.body.read)
342
- message = update.dig("message")
343
-
344
- context["request.user_id"] = message["from"]["id"]
345
- context["request.platform"] = :telegram
346
- context.input = message["text"]
347
-
348
- # Process through stack
349
- type, prompt, choices, media = @app.call(context)
350
-
351
- # Send response via Telegram API
352
- send_telegram_message(prompt, to: context["request.user_id"])
353
- end
354
- end
355
- ```
356
-
357
- ### 2. Custom Middleware
358
-
359
- Add processing logic at any point:
360
-
361
- ```ruby
362
- class AuthenticationMiddleware
363
- def call(context)
364
- user = authenticate_user(context["request.user_id"])
365
- context["current_user"] = user
366
- @app.call(context)
367
- end
368
- end
369
-
370
- class RateLimitingMiddleware
371
- def call(context)
372
- check_rate_limit(context["request.user_id"])
373
- @app.call(context)
374
- end
375
- end
376
- ```
377
-
378
- ### 3. Custom Session Stores
379
-
380
- Implement any storage backend:
381
-
382
- ```ruby
383
- class RedisSessionStore
384
- def get(key)
385
- Redis.current.hget(session_key, key)
386
- end
387
-
388
- def set(key, value)
389
- Redis.current.hset(session_key, key, value)
390
- Redis.current.expire(session_key, 24.hours.to_i)
391
- end
392
- end
393
- ```
394
-
395
- ### 4. Custom Renderers
396
-
397
- Platform-specific rendering logic:
398
-
399
- ```ruby
400
- class MyPlatform::Renderer
401
- def initialize(prompt, choices: nil, media: nil)
402
- @prompt = prompt
403
- @choices = choices
404
- @media = media
405
- end
406
-
407
- def render
408
- {
409
- text: transform_text(@prompt),
410
- buttons: transform_choices(@choices),
411
- attachments: transform_media(@media)
412
- }
413
- end
414
- end
415
- ```
416
-
417
- ## Configuration Architecture
418
-
419
- FlowChat uses a flexible configuration system:
420
-
421
- ```ruby
422
- # Global configuration
423
- FlowChat::Config.logger = Rails.logger
424
- FlowChat::Config.cache = Rails.cache
425
- FlowChat::Config.inject_middleware_logger = true # Default: true in Rails development
426
-
427
- # Platform-specific configuration
428
- FlowChat::Config.ussd.pagination_page_size = 160
429
- FlowChat::Config.whatsapp.message_handling_mode = :background
430
-
431
- # Per-processor configuration
432
- processor = FlowChat::Processor.new(self) do |config|
433
- config.use_gateway GatewayClass, gateway_config
434
- config.use_session_config(boundaries: [:flow], identifier: :user_id)
435
- config.use_middleware CustomMiddleware
436
- end
437
- ```
438
-
439
- ## Error Handling & Instrumentation
440
-
441
- FlowChat includes comprehensive error handling and instrumentation:
442
-
443
- ```ruby
444
- # Instrumentation events
445
- FlowChat.instrument(Events::MESSAGE_RECEIVED, {
446
- from: user_id,
447
- message: input,
448
- platform: :ussd
449
- })
450
-
451
- FlowChat.instrument(Events::FLOW_EXECUTION_START, {
452
- flow_name: "survey_flow",
453
- action: "start"
454
- })
455
-
456
- # Error handling in flows
457
- def payment_flow
458
- begin
459
- process_payment(amount)
460
- rescue PaymentError => e
461
- app.say "Payment failed: #{e.message}"
462
- FlowChat.instrument(Events::PAYMENT_FAILED, {
463
- error: e.message,
464
- user_id: app.user_id
465
- })
466
- end
467
- end
468
- ```
49
+ If the action method returns normally without prompting or terminating, the Executor raises `Terminate` with "Unexpected end of flow", since a flow is expected to either prompt for more input or end with a message.
469
50
 
470
- ## Performance Considerations
51
+ These interrupts subclass `Exception`, not `StandardError`. This is deliberate: a `rescue` in your flow code (a bare `rescue => e` catches `StandardError`) will not swallow a prompt or terminate and break the engine. Avoid `rescue Exception` inside a flow, since that would catch them.
471
52
 
472
- ### Memory Efficiency
53
+ ## The replay model
473
54
 
474
- - **Stateless design**: No application state stored in memory
475
- - **Session isolation**: Users don't affect each other
476
- - **Lazy loading**: Components loaded only when needed
55
+ There is no saved program counter between requests. Each webhook rebuilds the `App` and re-runs the flow method from the first line. Progress is reconstructed from the session:
477
56
 
478
- ### Scalability
57
+ - `app.screen(key)` checks the session for `key`. If a value is stored, it returns immediately without yielding the block. This fast-forwards through every screen already answered.
58
+ - The first screen without a stored answer yields a `Prompt`. If the current turn has input for it, the prompt validates and transforms it, stores the result with `session.set(key, value)`, and returns, so execution falls through to the next screen. If not, the prompt raises `Interrupt::Prompt` and the turn ends there.
479
59
 
480
- - **Horizontal scaling**: Each request is independent
481
- - **Background processing**: Long-running tasks handled asynchronously
482
- - **Caching**: Session data cached for performance
60
+ Two rules follow. One inbound message is consumed by one screen per turn: once a screen takes the input, later screens in the same run see no input and prompt. And a given screen key may be presented only once per run; re-entering a key raises `ArgumentError`. To revisit a screen, use `app.go_back`, which clears the current screen's answer and raises `RestartFlow`.
483
61
 
484
- ### Network Efficiency
62
+ ## The App
485
63
 
486
- - **Platform optimization**: Each gateway optimizes for its platform
487
- - **Compression**: Large responses automatically paginated (USSD)
488
- - **Batching**: Multiple messages combined when possible (WhatsApp)
64
+ `FlowChat::App` is the single object your flow talks to. It wraps the context and exposes:
489
65
 
490
- ## Security
66
+ - `screen(key)`, the unit of interaction.
67
+ - `say(msg, media:)`, to end the flow.
68
+ - `go_back`, to return to the previous screen.
69
+ - Read accessors for the turn: `text`, `media`, `location`, `contact`, `attachment_type`, and identity values `msisdn`, `user_id`, `platform`, `gateway`, `message_id`, `timestamp`, `contact_name`.
491
70
 
492
- ### Session Security
71
+ `Flow` itself is a thin base class: it stores the `app` and nothing else. All conversation logic lives in the methods you write.
493
72
 
494
- - **Identifier hashing**: Phone numbers hashed by default
495
- - **Session boundaries**: Prevent cross-tenant data access
496
- - **Secure storage**: Session stores can encrypt data
73
+ ## Sessions
497
74
 
498
- ### Input Validation
75
+ The session is a key-value store keyed by the session id. `FlowChat::Session::CacheSessionStore` persists it in `FlowChat::Config.cache`, so a session outlives the request. `screen` reads and writes it, `Terminate` destroys it. Boundaries and identifiers, which decide what shares a session, are covered in [configuration.md](configuration.md#sessions).
499
76
 
500
- - **Automatic sanitization**: User input sanitized by default
501
- - **Validation pipelines**: Custom validation in prompts
502
- - **Type safety**: Input transformation prevents type errors
77
+ ## Async
503
78
 
504
- ### Platform Security
79
+ Gateways with an outbound API can run the flow in a background job instead of inline. The gateway detects async support, serializes the request, and enqueues a job that reconstructs a controller and re-runs the same stack in the background. USSD cannot do this, since its protocol needs a synchronous response. See [async-background-processing.md](async-background-processing.md) and [factory-pattern.md](factory-pattern.md).
505
80
 
506
- - **Signature validation**: Webhook signatures verified
507
- - **Rate limiting**: Built-in protection against abuse
508
- - **Access controls**: Gateway-level authentication
81
+ ## Instrumentation
509
82
 
510
- This architecture enables FlowChat to be both powerful and flexible, supporting current platforms while being easily extensible for future platforms and use cases.
83
+ Each stage emits `ActiveSupport::Notifications` events (flow execution, messages received and sent, session lifecycle, pagination, webhook verification). Subscribe to feed metrics and logs into your own backend. See [instrumentation.md](instrumentation.md).