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/README.md CHANGED
@@ -3,392 +3,263 @@
3
3
  [![CI](https://github.com/radioactive-labs/flow_chat/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/radioactive-labs/flow_chat/actions/workflows/ci.yml)
4
4
  [![Gem Version](https://badge.fury.io/rb/flow_chat.svg)](https://badge.fury.io/rb/flow_chat)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
- [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%202.3.0-red.svg)](https://www.ruby-lang.org/)
7
- [![Rails](https://img.shields.io/badge/rails-%3E%3D%206.0-red.svg)](https://rubyonrails.org/)
6
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.0-red.svg)](https://www.ruby-lang.org/)
8
7
 
9
- FlowChat is a powerful Rails framework for building sophisticated conversational interfaces across **multiple platforms** with a **pluggable gateway architecture**. Create interactive flows with menus, prompts, validation, media support, and session management using a unified, intuitive API that works across USSD, WhatsApp, Telegram, HTTP, and any custom platforms you build.
8
+ **Write a conversation as an ordinary Ruby method. FlowChat runs it across stateless webhooks, on every messaging channel.**
10
9
 
11
- ## โœจ Key Features
12
-
13
- - **๐Ÿ”„ Unified API**: Single codebase that works across all platforms
14
- - **๐Ÿ”Œ Pluggable Gateways**: Extensible architecture supporting multiple backends per platform
15
- - **๐Ÿ“ฑ Multi-Platform**: Out of the box support for USSD, WhatsApp, Telegram, HTTP, and more, with simulator for testing
16
- - **๐ŸŽฏ Screen-Based Navigation**: Intuitive screen() method for building conversational flows
17
- - **๐Ÿ’พ Advanced Session Management**: Flexible session boundaries and storage options
18
- - **๐Ÿ”ง Middleware Architecture**: Extensible middleware system for custom processing
19
- - **๐ŸŽจ Rich Prompts**: Support for text, media, selections, yes/no prompts, validation, and transformations
20
- - **๐Ÿ“Š Built-in Instrumentation**: Comprehensive logging and metrics collection
21
- - **๐Ÿงช Testing Support**: Built-in simulator for development and testing
22
- - **๐Ÿข Multi-Tenancy**: In-built support for custom configuration per tenant and URL-based isolation
23
- - **๐Ÿš€ Background Processing**: Job queue support for WhatsApp messaging
24
-
25
- ## ๐Ÿš€ Quick Start
26
-
27
- ### Installation
28
-
29
- Add FlowChat to your Rails application:
30
-
31
- ```ruby
32
- # Gemfile
33
- gem 'flow_chat'
34
- ```
35
-
36
- ```bash
37
- bundle install
38
- ```
39
-
40
- ### Define your flow
10
+ A USSD or chat webhook is stateless: each message arrives as an isolated POST with no memory of the ones before it. The usual answer is a hand-rolled state machine: persist the current step, switch on it when the next message comes in, run the transition, persist the next step, repeat. FlowChat replaces that machine with a session and a replay engine, so you write the flow as a synchronous script that reads top to bottom.
41
11
 
42
12
  ```ruby
43
- # app/flow_chat/welcome_flow.rb
44
-
45
- class WelcomeFlow < FlowChat::Flow
13
+ class RegistrationFlow < FlowChat::Flow
46
14
  def main_page
47
- name = app.screen(:name) do |prompt|
48
- prompt.ask "Welcome! What's your name?",
49
- transform: ->(input) { input.strip.titleize }
50
- end
15
+ name = app.screen(:name) { |prompt| prompt.ask "What's your name?" }
51
16
 
52
- choice = app.screen(:main_menu) do |prompt|
53
- prompt.select "Hi #{name}! Choose:", {
54
- "1" => "Account Info",
55
- "2" => "Make Payment",
56
- "3" => "Support"
57
- }
17
+ email = app.screen(:email) do |prompt|
18
+ prompt.ask "Your email?", validate: ->(input) { "Invalid email" unless input.include?("@") }
58
19
  end
59
20
 
60
- case choice
61
- when "1"
62
- show_account_info
63
- when "2"
64
- make_payment
65
- when "3"
66
- app.say "Call us: 123-456-7890"
67
- end
21
+ app.say "Welcome #{name}!"
68
22
  end
69
-
70
- # ... implement your flow methods
71
23
  end
72
24
  ```
73
25
 
74
- ### Basic USSD Application
26
+ Each `screen` returns its stored answer when one exists and re-prompts when it does not. The method runs top to bottom on every turn and blocks on the first screen that has no answer yet. The same flow runs unchanged on USSD, WhatsApp, Messenger, Instagram, Telegram, and HTTP, with per-platform rendering.
75
27
 
76
- ```ruby
77
- # app/controllers/ussd_controller.rb
78
- class UssdController < ApplicationController
79
- skip_forgery_protection
80
-
81
- def process_request
82
- processor = FlowChat::Processor.new(self) do |config|
83
- config.use_gateway FlowChat::Ussd::Gateway::Nalo
84
- config.use_session_store FlowChat::Session::CacheSessionStore
85
- end
28
+ ## How the replay engine works
86
29
 
87
- processor.run WelcomeFlow, :main_page
88
- end
89
- end
90
- ```
30
+ There is no saved program counter. On each webhook FlowChat rebuilds the app from the request and re-runs your flow method from the first line.
91
31
 
92
- ### Basic WhatsApp Application
32
+ `app.screen(key)` is the fast-forward mechanism. When the session already holds a value for `key`, `screen` returns `session.get(key)` immediately and never yields. When it does not, `screen` yields a `FlowChat::Prompt`. If that prompt calls `ask` (or `select`, `yes?`) and no input is available yet, it raises `FlowChat::Interrupt::Prompt`. The interrupt unwinds the flow back to the Executor, which turns it into a rendered prompt and returns the response to the gateway.
93
33
 
94
- ```ruby
95
- # app/controllers/whatsapp_controller.rb
96
- class WhatsappController < ApplicationController
97
- skip_forgery_protection
34
+ The next webhook replays the same method from the top. Every screen that already has a stored answer is skipped in place. The screen that raised last time now sees the incoming input, runs its `validate` and `transform`, stores the accepted value with `session.set(key, value)`, and returns it, so execution falls through into the next screen. The flow advances one screen per turn without ever holding state between requests beyond the session hash.
98
35
 
99
- def webhook
100
- processor = FlowChat::Processor.new(self) do |config|
101
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
102
- config.use_session_store FlowChat::Session::CacheSessionStore
103
- end
36
+ Two rules follow from this design:
104
37
 
105
- processor.run WelcomeFlow, :main_page
106
- end
107
- end
108
- ```
38
+ - One inbound message is consumed by one screen per turn. Once a screen takes the turn's input, later screens in the same run see no input and will prompt.
39
+ - A given screen key may be presented only once per run. Re-entering a key raises `ArgumentError`; use distinct keys or `app.go_back` to revisit.
109
40
 
110
- ### Basic Telegram Application
41
+ ## Installation
111
42
 
112
- ```ruby
113
- # app/controllers/telegram_controller.rb
114
- class TelegramController < ApplicationController
115
- skip_forgery_protection
116
-
117
- def webhook
118
- processor = FlowChat::Processor.new(self) do |config|
119
- config.use_gateway FlowChat::Telegram::Gateway::BotApi
120
- config.use_session_store FlowChat::Session::CacheSessionStore
121
- end
43
+ Add the gem to your Gemfile:
122
44
 
123
- processor.run WelcomeFlow, :main_page
124
- end
125
- end
45
+ ```ruby
46
+ gem "flow_chat"
126
47
  ```
127
48
 
128
- ### Basic HTTP API Application
49
+ Then run `bundle install`. FlowChat requires Ruby 3.0 or newer.
129
50
 
130
- ```ruby
131
- # app/controllers/api_controller.rb
132
- class ApiController < ApplicationController
133
- def chat
134
- processor = FlowChat::Processor.new(self) do |config|
135
- config.use_gateway FlowChat::Http::Gateway::Simple
136
- config.use_session_store FlowChat::Session::CacheSessionStore
137
- end
51
+ The cache-backed session store needs a cache. Set it once during boot, for example in an initializer:
138
52
 
139
- processor.run WelcomeFlow, :main_page
140
- end
141
- end
53
+ ```ruby
54
+ FlowChat::Config.cache = Rails.cache
142
55
  ```
143
- > Same WelcomeFlow works across ALL platforms!
144
56
 
57
+ Without a cache configured, `FlowChat::Session::CacheSessionStore` raises when it tries to read or write a session. There are no migrations and no generators to run.
145
58
 
146
- ## ๐Ÿ—๏ธ Architecture
59
+ ## Wiring a platform
147
60
 
148
- FlowChat uses a **composition-based architecture** with these core components:
61
+ Build a processor, choose a gateway and session store, and run your flow:
149
62
 
150
- - **Processor**: Orchestrates request processing through middleware stack
151
- - **Gateway**: Platform-specific request/response handling (Nalo, WhatsApp Cloud API)
152
- - **App**: Unified application interface with screen-based navigation
153
- - **Session**: Flexible session management with configurable boundaries
154
- - **Middleware**: Extensible processing pipeline
63
+ ```ruby
64
+ processor = FlowChat::Processor.new(self) do |config|
65
+ config.use_gateway FlowChat::Ussd::Gateway::Nalo
66
+ config.use_session_store FlowChat::Session::CacheSessionStore
67
+ end
155
68
 
156
- ```
157
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
158
- โ”‚ Gateway โ”‚ -> โ”‚ Session โ”‚ -> โ”‚ Custom โ”‚
159
- โ”‚ โ”‚ โ”‚ Middleware โ”‚ โ”‚ Middleware โ”‚
160
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
161
- โ”‚
162
- v
163
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
164
- โ”‚ Flow/Action โ”‚ <- โ”‚ Executor โ”‚ <- โ”‚ App โ”‚
165
- โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚
166
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
69
+ processor.run RegistrationFlow, :main_page
167
70
  ```
168
71
 
169
- ## ๐Ÿ”Œ Pluggable Gateway Architecture
72
+ Point your controller's webhook action at this code (`self` is the controller). Each platform has its own gateway class:
170
73
 
171
- FlowChat's power comes from its pluggable gateway system. Each platform can have multiple gateway implementations:
74
+ | Platform | Gateway class | Platform symbol | Rendering |
75
+ |---|---|---|---|
76
+ | USSD | `FlowChat::Ussd::Gateway::Nalo` | `:ussd` | Numbered text menus, pagination, media as URL text |
77
+ | WhatsApp | `FlowChat::Whatsapp::Gateway::CloudApi` | `:whatsapp` | Reply buttons (<=3 choices), lists (>3), rich media |
78
+ | Messenger | `FlowChat::Messenger::Gateway::SendApi` | `:messenger` | Quick replies, carousel, numbered text |
79
+ | Instagram | `FlowChat::Instagram::Gateway::SendApi` | `:instagram` | Quick replies, carousel, always numbered |
80
+ | Telegram | `FlowChat::Telegram::Gateway::BotApi` | `:telegram` | Inline keyboards, rich media, HTML |
81
+ | HTTP | `FlowChat::Http::Gateway::Simple` | `:http` | JSON responses (testing, custom clients) |
82
+ | Intercom | `FlowChat::Intercom::Gateway::IntercomApi` | `:intercom` | Live-chat replies |
172
83
 
173
- ```ruby
174
- # Create your own SMS gateway
175
- class MyCompany::Sms::Gateway::Twilio
176
- def initialize(app, config)
177
- @app = app
178
- @config = config
179
- end
84
+ The same `RegistrationFlow` runs on any row by changing only `config.use_gateway`. To build a gateway for a platform not listed here, see [docs/gateway-development.md](docs/gateway-development.md).
180
85
 
181
- def call(context)
182
- # Parse Twilio webhook, set context values
183
- context["request.msisdn"] = params["From"]
184
- context["request.platform"] = :sms
185
- context.input = params["Body"]
186
-
187
- # Process through middleware
188
- type, prompt, choices, media = @app.call(context)
189
-
190
- # Send response via Twilio API
191
- send_sms_response(prompt, to: context["request.msisdn"])
192
- end
86
+ ## Building flows
193
87
 
194
- # Optional: Configure platform-specific middleware
195
- def self.configure_middleware_stack(builder, custom_middleware)
196
- builder.use MyCompany::Sms::MessageTransformMiddleware
197
- builder.use custom_middleware
198
- end
199
- end
88
+ A flow is a class that inherits `FlowChat::Flow` and reads and writes the conversation through `app`. Each unit of interaction is a screen:
200
89
 
201
- # Use your custom gateway
202
- processor = FlowChat::Processor.new(self) do |config|
203
- config.use_gateway MyCompany::Sms::Gateway::Twilio, sms_config
204
- end
90
+ ```ruby
91
+ value = app.screen(:key) { |prompt| prompt.ask "..." }
205
92
  ```
206
93
 
207
- ## ๐Ÿ“š Documentation
94
+ The block receives a `FlowChat::Prompt`. Its methods:
208
95
 
209
- ### Getting Started
210
- - [**Getting Started Guide**](docs/getting-started.md) - Comprehensive setup and first app
211
- - [**Architecture Overview**](docs/architecture.md) - Deep dive into FlowChat's design
212
- - [**Configuration**](docs/configuration.md) - Complete configuration reference
96
+ | Method | Signature | Behavior |
97
+ |---|---|---|
98
+ | `ask` | `ask(msg, choices: nil, transform: nil, validate: nil, media: nil)` | Prompt for free input; validate, then transform, then return the value |
99
+ | `select` | `select(msg, choices, media: nil, error_message: "Invalid selection:")` | Prompt for one of `choices`; returns the chosen key |
100
+ | `yes?` | `yes?(msg)` | A `select` over Yes/No; returns `true` or `false` |
101
+ | `say` | `say(msg, media: nil)` | Send a terminal message and end the flow |
213
102
 
214
- ### Platform Guides
215
- - [**USSD Development**](docs/platforms/ussd.md) - USSD-specific features and examples
216
- - [**WhatsApp Development**](docs/platforms/whatsapp.md) - WhatsApp Business API integration
217
- - [**Telegram Development**](docs/platforms/telegram.md) - Telegram Bot API integration
218
- - [**HTTP Development**](docs/platforms/http.md) ๐Ÿšง - API endpoints and webhooks
219
- - [**Multi-Platform Apps**](docs/platforms/multi-platform.md) ๐Ÿšง - Building unified experiences
103
+ Details:
220
104
 
221
- ### Advanced Topics
222
- - [**Gateway Development**](docs/gateway-development.md) - Creating custom gateways and platforms
223
- - [**Session Management**](docs/session-management.md) ๐Ÿšง - Session boundaries and storage
224
- - [**Middleware Development**](docs/middleware.md) ๐Ÿšง - Creating custom middleware
225
- - [**Testing & Simulation**](docs/testing.md) - Testing strategies and simulator usage
226
- - [**Background Jobs**](docs/background-jobs.md) ๐Ÿšง - Async processing for WhatsApp
227
- - [**Instrumentation**](docs/instrumentation.md) - Monitoring, logging, and error tracking
105
+ - `validate` is a callable that returns an error string when the input is rejected and `nil` when it passes. The received argument behaves like the input text (`input.include?("@")`, `input.to_i`, `input.strip`), and also exposes `media`, `location`, and `contact` for attachment-carrying turns.
106
+ - `transform` is a callable that maps the accepted input to the value `screen` stores and returns.
107
+ - `choices` may be an Array (`["Yes", "No"]`) or a Hash (`{ "1" => "Account", "2" => "Support" }`). With a Hash, `select` returns the key.
108
+ - Passing `media:` together with more than 3 `choices` raises `ArgumentError`. Use media or a longer choice list, not both.
228
109
 
229
- ### API Reference
230
- - [**Core API**](docs/api-reference/core.md) ๐Ÿšง - Processor, App, Flow classes
231
- - [**Prompts & Validation**](docs/api-reference/prompts.md) ๐Ÿšง - Interactive prompts and validation
232
- - [**Gateways**](docs/api-reference/gateways.md) ๐Ÿšง - Platform gateway interfaces
233
- - [**Session Stores**](docs/api-reference/session-stores.md) ๐Ÿšง - Session storage options
110
+ `app.say` outside a block ends the flow from anywhere:
111
+
112
+ ```ruby
113
+ def main_page
114
+ confirmed = app.screen(:confirm) { |prompt| prompt.yes? "Place the order?" }
115
+ app.say("Cancelled.") unless confirmed
116
+ # ...
117
+ end
118
+ ```
234
119
 
235
- ## ๐ŸŽฏ Core Concepts
120
+ To send the user back to the previous screen, call `app.go_back`. It clears the current screen's stored answer and restarts the flow from the top, so the earlier screen prompts again. Remember that a screen key may appear only once per run: reuse of a key in a single pass raises `ArgumentError`.
236
121
 
237
- ### Screen-Based Navigation
122
+ ## Media and rich input
238
123
 
239
- Build conversational flows using the intuitive `screen()` method:
124
+ An inbound turn is a `FlowChat::Input`. Read what the user sent through these accessors on `app`:
240
125
 
241
- ```ruby
242
- def registration_flow
243
- # Each screen automatically handles state and navigation
244
- email = app.screen(:email) do |prompt|
245
- prompt.ask "Enter your email:",
246
- validate: ->(input) {
247
- "Invalid email" unless input.include?("@")
248
- }
249
- end
126
+ | Accessor | Returns |
127
+ |---|---|
128
+ | `app.text` | The message text (`""` when the turn carries only an attachment) |
129
+ | `app.media` | An Array of `FlowChat::Media` (empty array when none) |
130
+ | `app.location` | The shared location payload, or `nil` |
131
+ | `app.contact` | The shared contact card, or `nil` |
132
+ | `app.attachment_type` | `:media`, `:location`, `:contact`, or `nil` |
250
133
 
251
- name = app.screen(:name) do |prompt|
252
- prompt.ask "Enter your full name:",
253
- transform: ->(input) { input.strip.titleize }
254
- end
134
+ Reading an inbound photo:
255
135
 
256
- # Confirmation screen with rich prompts
257
- confirmed = app.screen(:confirm) do |prompt|
258
- prompt.yes? "Confirm registration for #{name} (#{email})?"
136
+ ```ruby
137
+ def main_page
138
+ app.screen(:photo) do |prompt|
139
+ prompt.ask "Send a photo of the receipt."
259
140
  end
260
141
 
261
- if confirmed
262
- create_user(name, email)
263
- app.say "Welcome #{name}! Registration complete."
264
- else
265
- app.say "Registration cancelled."
142
+ photo = app.media.first
143
+ if photo
144
+ photo.type # => :image (a Symbol; normalized across platforms)
145
+ photo.mime_type # => "image/jpeg"
146
+ photo.caption # => the caption text, or nil
147
+ bytes = photo.download # raw bytes, or nil on failure
148
+ link = photo.url # a fetchable URL, or nil
266
149
  end
267
150
  end
268
151
  ```
269
152
 
270
- ### Multi-Platform Compatibility
153
+ `app.media` is always an Array, so read a single item with `app.media.first` and check for `nil`; the type is a Symbol, read with `app.media.first&.type`. A caption-less photo still answers a screen: an attachment counts as submitted even when the text is blank.
271
154
 
272
- Write once, run everywhere:
155
+ To send media outbound, pass `media:` to `ask` or `say`:
273
156
 
274
157
  ```ruby
275
- class MenuFlow < FlowChat::Flow
276
- def main_menu
277
- choice = app.screen(:menu) do |prompt|
278
- prompt.select "Choose an option:", {
279
- "info" => "๐Ÿ“‹ Information", # Rich for WhatsApp
280
- "help" => "โ“ Help", # Falls back to text for USSD
281
- "exit" => "๐Ÿ‘‹ Exit"
282
- }
283
- end
284
-
285
- # Same logic works on both platforms
286
- handle_choice(choice)
287
- end
288
- end
158
+ app.say "Here you go", media: { type: :image, url: "https://example.com/receipt.png" }
289
159
  ```
290
160
 
291
- ### Flexible Session Management
161
+ Caveat: a `Media` object that has been deserialized from a session store or a background job has lost its live platform client, so `url` and `download` return `nil`. If you need the bytes, call `download` eagerly in the same request that received the media, before the value round-trips through a session or job.
292
162
 
293
- Configure sessions for your use case:
163
+ ## Sessions
294
164
 
295
- ```ruby
296
- # Durable sessions across timeouts
297
- processor = FlowChat::Processor.new(self) do |config|
298
- config.use_durable_sessions # Uses user identifier (usually phone number) for session ID
299
- end
165
+ The default session boundaries are `[:flow, :gateway, :platform]`: a session is scoped to one flow, on one gateway, on one platform. Convenience methods adjust this:
300
166
 
301
- # Cross-platform sessions
302
- processor = FlowChat::Processor.new(self) do |config|
303
- config.use_cross_platform_sessions # Share sessions between platforms (e.g. USSD & WhatsApp)
304
- end
167
+ | Method | Effect |
168
+ |---|---|
169
+ | `use_durable_sessions` | Key the session on a stable per-user identifier so it survives USSD session-id rotation across a conversation |
170
+ | `use_cross_platform_sessions` | Narrow the boundary to flow only, so one session is shared across platforms (for example USSD and WhatsApp) |
171
+ | `use_url_isolation` | Add a `:url` boundary for per-tenant or per-host isolation |
172
+ | `use_session_config(boundaries:, identifier:, hash_identifiers:, &block)` | Set boundaries, the identifier, and identifier hashing directly for full control |
305
173
 
306
- # URL-based multi-tenancy
174
+ ```ruby
307
175
  processor = FlowChat::Processor.new(self) do |config|
308
- config.use_url_isolation # tenant1.app.com vs tenant2.app.com
176
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
177
+ config.use_session_store FlowChat::Session::CacheSessionStore
178
+ config.use_durable_sessions
309
179
  end
310
180
  ```
311
181
 
312
- ## ๐Ÿ› ๏ธ Supported Platforms & Gateways
182
+ See [docs/configuration.md](docs/configuration.md) for the full set of session options.
313
183
 
314
- | Platform | Available Gateways | Features |
315
- |----------|-------------------|----------|
316
- | **USSD** | `Nalo` โœ…, Custom | Pagination, choice mapping, session management |
317
- | **WhatsApp** | `CloudApi` โœ…, Custom | Rich media, buttons, lists, templates, background jobs |
318
- | **Telegram** | `BotApi` โœ…, Custom | Inline keyboards, rich media, callbacks, group chats |
319
- | **HTTP** | `Simple` โœ…, Custom | Testing, webhooks, API endpoints, JSON responses |
320
- | **Simulator** | Built-in โœ… | Development testing, conversation replay, flow debugging |
321
- | **Custom** | *Your Gateway* | Implement any platform by creating a gateway class |
184
+ ## Platform differences
322
185
 
323
- *โœ… = Included with FlowChat*
186
+ The flow code is the same everywhere, but each platform imposes limits that the rendering respects and that you should keep in mind:
324
187
 
325
- ### Gateway Examples
188
+ | Platform | Limits and behavior |
189
+ |---|---|
190
+ | USSD | Pages are split at 140 characters by default; media is rendered as a text line containing a URL, not an inline attachment; async processing is not available. |
191
+ | WhatsApp | Reply-button titles are truncated near 20 characters and list titles near 24; a list section holds at most 10 rows. WhatsApp's 24-hour customer-service window and its template requirement are not abstracted away: you manage message templates yourself. |
192
+ | Messenger | Up to 13 choices render as quick replies, up to 30 as a carousel (10 elements, 3 buttons each), and above that as numbered text. Neither platform abstracts the 24-hour messaging window; a send outside it is attempted and Meta's rejection surfaces as an API error. |
193
+ | Instagram | Same quick-reply and carousel caps as Messenger (13, then 30), but both surfaces render on the mobile app only, not desktop or web, so Instagram always lists the options as numbered text as well. Text is capped at 1000 bytes UTF-8, not characters. |
194
+ | Telegram | Choice taps arrive as callback queries; choices render as inline keyboards; message text supports HTML formatting. |
195
+ | HTTP | Requests and responses are JSON; each request must supply `session_id` and `user_id`. |
326
196
 
327
- ```ruby
328
- # Built-in gateways (included with FlowChat)
329
- config.use_gateway FlowChat::Ussd::Gateway::Nalo
330
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, whatsapp_config
331
- config.use_gateway FlowChat::Telegram::Gateway::BotApi, telegram_config
332
- config.use_gateway FlowChat::Http::Gateway::Simple
333
-
334
- # Custom gateway examples (you would build these)
335
- config.use_gateway MyCompany::Sms::Gateway::Twilio, twilio_config
336
- config.use_gateway MyCompany::Slack::Gateway::BoltJS, slack_config
337
- ```
338
-
339
- ## ๐Ÿ“ฆ Example Applications
197
+ Platform guides: [docs/platforms/ussd.md](docs/platforms/ussd.md), [docs/platforms/whatsapp.md](docs/platforms/whatsapp.md), [docs/platforms/messenger.md](docs/platforms/messenger.md), [docs/platforms/instagram.md](docs/platforms/instagram.md), [docs/platforms/telegram.md](docs/platforms/telegram.md).
340
198
 
341
- - [**USSD Banking App**](docs/examples/ussd-banking.md) ๐Ÿšง - Complete banking flow with validation
342
- - [**WhatsApp Customer Service**](docs/examples/whatsapp-support.md) ๐Ÿšง - Media support and templates
343
- - [**HTTP API Chatbot**](docs/examples/http-api.md) ๐Ÿšง - JSON API for web/mobile integration
344
- - [**Multi-Platform E-commerce**](docs/examples/multi-platform-shop.md) ๐Ÿšง - Unified shopping across USSD, WhatsApp, and HTTP
345
- - [**Multi-Tenant SaaS**](docs/examples/multi-tenant.md) ๐Ÿšง - URL-based tenant isolation
346
- - [**Custom Gateway Example**](docs/examples/custom-gateway.md) ๐Ÿšง - Building a Telegram gateway
199
+ ## Background processing
347
200
 
348
- ## ๐Ÿงช Testing & Development
349
-
350
- FlowChat includes a built-in simulator interface for easy development and testing:
201
+ For platforms with an outbound API, you can acknowledge the webhook immediately and run the flow in a background job. Register a factory once, then call it from the webhook:
351
202
 
352
203
  ```ruby
353
- # Simulator is automatically enabled in development
354
- processor = FlowChat::Processor.new(self) do |config|
355
- config.use_gateway FlowChat::Ussd::Gateway::Nalo # or any gateway
204
+ # config/initializers/flow_chat.rb
205
+ FlowChat::Factory.register(:whatsapp) do |controller|
206
+ processor = FlowChat::Processor.new(controller) do |config|
207
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
208
+ config.use_session_store FlowChat::Session::CacheSessionStore
209
+ config.use_async(factory: :whatsapp)
210
+ end
211
+ processor.run RegistrationFlow, :main_page
356
212
  end
357
213
 
358
- # Explicitly control simulator mode if needed
359
- processor = FlowChat::Processor.new(self, enable_simulator: false) do |config|
360
- # Disable simulator even in development
214
+ # app/controllers/whatsapp_controller.rb
215
+ def webhook
216
+ FlowChat::Factory.execute(:whatsapp, controller: self)
361
217
  end
362
218
  ```
363
219
 
364
- The simulator provides a web interface for testing your flows during development. It works the same regardless of which gateway you're using, allowing you to test your conversational logic before deploying to actual platforms.
220
+ The webhook enqueues `GenericAsyncJob` and returns; the job re-runs the same factory in the background, where the gateway detects the background context and processes the flow inline before sending the reply through the platform API. USSD does not support async, since it depends on a synchronous request-response cycle. See [docs/factory-pattern.md](docs/factory-pattern.md) and [docs/async-background-processing.md](docs/async-background-processing.md).
365
221
 
366
- **Learn more**: [Testing & Simulation Guide](docs/testing.md)
222
+ ## Instrumentation
367
223
 
368
- ## ๐Ÿค Contributing
224
+ FlowChat emits `ActiveSupport::Notifications` events for flow execution, session lifecycle, and gateway activity. `FlowChat.metrics` collects counters over those events. See [docs/instrumentation.md](docs/instrumentation.md).
369
225
 
370
- We welcome contributions!
226
+ ## Testing
371
227
 
372
- 1. Fork the repository
373
- 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
374
- 3. Commit your changes (`git commit -m 'Add amazing feature'`)
375
- 4. Push to the branch (`git push origin feature/amazing-feature`)
376
- 5. Open a Pull Request
228
+ FlowChat ships a web simulator for driving flows locally without a real gateway, useful during development. It requires `FlowChat::Config.simulator_secret` to be set. See [docs/testing.md](docs/testing.md).
377
229
 
378
- ## ๐Ÿ“„ License
230
+ ## Documentation
379
231
 
380
- FlowChat is released under the [MIT License](LICENSE.txt).
232
+ Guides:
233
+
234
+ - [Getting started](docs/getting-started.md)
235
+ - [Configuration](docs/configuration.md)
236
+ - [Testing](docs/testing.md)
237
+
238
+ Platforms:
381
239
 
382
- ## ๐Ÿ†˜ Support
240
+ - [USSD](docs/platforms/ussd.md)
241
+ - [WhatsApp](docs/platforms/whatsapp.md)
242
+ - [Messenger](docs/platforms/messenger.md)
243
+ - [Instagram](docs/platforms/instagram.md)
244
+ - [Telegram](docs/platforms/telegram.md)
383
245
 
384
- - **Documentation**: Comprehensive guides in the [docs/](docs/) directory
385
- - **Issues**: [GitHub Issues](https://github.com/radioactive-labs/flow_chat/issues)
386
- - **Discussions**: [GitHub Discussions](https://github.com/radioactive-labs/flow_chat/discussions)
246
+ Internals and advanced:
387
247
 
388
- ## ๐Ÿข Commercial Support
248
+ - [Architecture](docs/architecture.md)
249
+ - [Building a gateway](docs/gateway-development.md)
250
+ - [Gateway context variables](docs/gateway-context-variables.md)
251
+ - [Async and background processing](docs/async-background-processing.md)
252
+ - [Factory pattern](docs/factory-pattern.md)
253
+ - [Instrumentation](docs/instrumentation.md)
389
254
 
390
- FlowChat is developed by [Radioactive Labs](https://github.com/radioactive-labs). Commercial support, custom development, and consulting services are available.
255
+ ## Development
391
256
 
392
- ---
257
+ Run the test suite with `rake test`. To run a single file, use `ruby -Itest test/unit/some_test.rb`.
393
258
 
394
- **Ready to build amazing conversational experiences?** Check out the [Getting Started Guide](docs/getting-started.md) to create your first FlowChat application.
259
+ ## Contributing
260
+
261
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/radioactive-labs/flow_chat). Please add tests for behavior changes and keep them passing.
262
+
263
+ ## License
264
+
265
+ FlowChat is released under the [MIT License](LICENSE.txt).
data/Rakefile CHANGED
@@ -8,7 +8,18 @@ Dir.glob("lib/tasks/**/*.rake").each { |r| load r }
8
8
  Rake::TestTask.new(:test) do |t|
9
9
  t.libs << "test"
10
10
  t.libs << "lib"
11
- t.test_files = FileList["test/**/*_test.rb"]
11
+ # test/performance holds benchmarks, which assert against wall-clock timings and
12
+ # so fail on a loaded machine for reasons that have nothing to do with the code
13
+ # under test. A suite that cries wolf is a suite people stop reading, so they
14
+ # run under `rake benchmark` instead of here.
15
+ t.test_files = FileList["test/**/*_test.rb"].exclude("test/performance/**/*_test.rb")
16
+ end
17
+
18
+ desc "Run the performance benchmarks (timing-sensitive, not part of rake test)"
19
+ Rake::TestTask.new(:benchmark) do |t|
20
+ t.libs << "test"
21
+ t.libs << "lib"
22
+ t.test_files = FileList["test/performance/**/*_test.rb"]
12
23
  end
13
24
 
14
25
  task default: %i[test standard]
data/SECURITY.md CHANGED
@@ -13,7 +13,7 @@ please upgrade before reporting an issue to confirm it still reproduces.
13
13
 
14
14
  | Version | Supported |
15
15
  | ------- | ------------------ |
16
- | Latest release (`0.9.x`) | :white_check_mark: |
16
+ | Latest release (`0.10.x`) | :white_check_mark: |
17
17
  | Older releases | :x: |
18
18
 
19
19
  ## Reporting a Vulnerability