flow_chat 0.6.1 → 0.7.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/.github/workflows/ci.yml +44 -0
- data/.gitignore +2 -1
- data/README.md +84 -1229
- data/docs/configuration.md +337 -0
- data/docs/flows.md +320 -0
- data/docs/images/simulator.png +0 -0
- data/docs/instrumentation.md +216 -0
- data/docs/media.md +153 -0
- data/docs/testing.md +475 -0
- data/docs/ussd-setup.md +306 -0
- data/docs/whatsapp-setup.md +162 -0
- data/examples/multi_tenant_whatsapp_controller.rb +9 -37
- data/examples/simulator_controller.rb +9 -18
- data/examples/ussd_controller.rb +32 -38
- data/examples/whatsapp_controller.rb +32 -125
- data/examples/whatsapp_media_examples.rb +68 -336
- data/examples/whatsapp_message_job.rb +5 -3
- data/flow_chat.gemspec +6 -2
- data/lib/flow_chat/base_processor.rb +48 -2
- data/lib/flow_chat/config.rb +5 -0
- data/lib/flow_chat/context.rb +13 -1
- data/lib/flow_chat/instrumentation/log_subscriber.rb +176 -0
- data/lib/flow_chat/instrumentation/metrics_collector.rb +197 -0
- data/lib/flow_chat/instrumentation/setup.rb +155 -0
- data/lib/flow_chat/instrumentation.rb +70 -0
- data/lib/flow_chat/prompt.rb +20 -20
- data/lib/flow_chat/session/cache_session_store.rb +73 -7
- data/lib/flow_chat/session/middleware.rb +37 -4
- data/lib/flow_chat/session/rails_session_store.rb +36 -1
- data/lib/flow_chat/simulator/controller.rb +6 -6
- data/lib/flow_chat/ussd/gateway/nalo.rb +30 -0
- data/lib/flow_chat/ussd/gateway/nsano.rb +33 -0
- data/lib/flow_chat/ussd/middleware/choice_mapper.rb +109 -0
- data/lib/flow_chat/ussd/middleware/executor.rb +24 -2
- data/lib/flow_chat/ussd/middleware/pagination.rb +87 -7
- data/lib/flow_chat/ussd/processor.rb +14 -0
- data/lib/flow_chat/ussd/renderer.rb +1 -1
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +99 -12
- data/lib/flow_chat/whatsapp/configuration.rb +35 -4
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +120 -34
- data/lib/flow_chat/whatsapp/middleware/executor.rb +24 -2
- data/lib/flow_chat/whatsapp/processor.rb +8 -0
- data/lib/flow_chat/whatsapp/renderer.rb +4 -9
- data/lib/flow_chat.rb +23 -0
- metadata +22 -11
- data/.travis.yml +0 -6
- data/app/controllers/demo_controller.rb +0 -101
- data/app/flow_chat/demo_restaurant_flow.rb +0 -889
- data/config/routes_demo.rb +0 -59
- data/examples/initializer.rb +0 -86
- data/examples/media_prompts_examples.rb +0 -27
- data/images/ussd_simulator.png +0 -0
@@ -1,404 +1,136 @@
|
|
1
|
-
# WhatsApp Media
|
2
|
-
# This file demonstrates
|
1
|
+
# WhatsApp Media Examples
|
2
|
+
# This file demonstrates media usage with FlowChat's WhatsApp integration
|
3
3
|
|
4
|
-
#
|
5
|
-
# BASIC MEDIA SENDING (Out-of-Band Messaging)
|
6
|
-
# ============================================================================
|
7
|
-
|
8
|
-
# Initialize the WhatsApp client
|
4
|
+
# Basic media sending with WhatsApp Client
|
9
5
|
config = FlowChat::Whatsapp::Configuration.from_credentials
|
10
6
|
client = FlowChat::Whatsapp::Client.new(config)
|
11
7
|
|
12
|
-
# Send
|
13
|
-
client.send_image("+1234567890", "https://example.com/
|
14
|
-
|
15
|
-
|
16
|
-
client.
|
17
|
-
|
18
|
-
# Send an audio file from URL
|
19
|
-
client.send_audio("+1234567890", "https://example.com/audio/greeting.mp3")
|
20
|
-
|
21
|
-
# Send a video from URL with caption
|
22
|
-
client.send_video("+1234567890", "https://example.com/videos/demo.mp4", "Product demo video")
|
23
|
-
|
24
|
-
# Send a sticker from URL
|
25
|
-
client.send_sticker("+1234567890", "https://example.com/stickers/happy.webp")
|
26
|
-
|
27
|
-
# You can also still use existing WhatsApp media IDs
|
28
|
-
client.send_image("+1234567890", "1234567890", "Image from existing media ID")
|
29
|
-
|
30
|
-
# ============================================================================
|
31
|
-
# USING MEDIA IN FLOWS
|
32
|
-
# ============================================================================
|
8
|
+
# Send different media types
|
9
|
+
client.send_image("+1234567890", "https://example.com/image.jpg", "Caption")
|
10
|
+
client.send_document("+1234567890", "https://example.com/doc.pdf", "Document title", "filename.pdf")
|
11
|
+
client.send_audio("+1234567890", "https://example.com/audio.mp3")
|
12
|
+
client.send_video("+1234567890", "https://example.com/video.mp4", "Video caption")
|
13
|
+
client.send_sticker("+1234567890", "https://example.com/sticker.webp")
|
33
14
|
|
34
|
-
|
15
|
+
# Using media in flows
|
16
|
+
class MediaFlow < FlowChat::Flow
|
35
17
|
def main_page
|
36
|
-
# Handle incoming media
|
18
|
+
# Handle incoming media
|
37
19
|
if app.media
|
38
|
-
|
20
|
+
handle_user_media
|
39
21
|
return
|
40
22
|
end
|
41
23
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
24
|
+
# Send media with prompts
|
25
|
+
app.screen(:feedback) do |prompt|
|
26
|
+
prompt.ask "What do you think?",
|
27
|
+
media: {
|
28
|
+
type: :image,
|
29
|
+
url: "https://example.com/product.jpg"
|
30
|
+
}
|
49
31
|
end
|
50
32
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
send_voice_message
|
58
|
-
when "feedback"
|
59
|
-
collect_feedback
|
60
|
-
end
|
33
|
+
# Send media responses
|
34
|
+
app.say "Thanks for your feedback!",
|
35
|
+
media: {
|
36
|
+
type: :video,
|
37
|
+
url: "https://example.com/response.mp4"
|
38
|
+
}
|
61
39
|
end
|
62
40
|
|
63
41
|
private
|
64
42
|
|
65
|
-
def
|
43
|
+
def handle_user_media
|
66
44
|
media_type = app.media["type"]
|
67
|
-
media_id = app.media["id"]
|
68
|
-
|
69
|
-
Rails.logger.info "Received #{media_type} from #{app.phone_number}: #{media_id}"
|
70
45
|
|
71
46
|
case media_type
|
72
47
|
when "image"
|
73
|
-
app.say "Thanks for the image!
|
48
|
+
app.say "Thanks for the image! Processing..."
|
74
49
|
when "document"
|
75
|
-
app.say "
|
50
|
+
app.say "Document received. Reviewing..."
|
76
51
|
when "audio"
|
77
|
-
app.say "Got your voice message!
|
52
|
+
app.say "Got your voice message!"
|
78
53
|
when "video"
|
79
|
-
app.say "
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
def send_product_catalog
|
84
|
-
# Send multiple product images from URLs
|
85
|
-
client = get_whatsapp_client
|
86
|
-
|
87
|
-
app.say "Here's our latest product catalog:"
|
88
|
-
|
89
|
-
# Product images stored in cloud storage (CDN, S3, etc.)
|
90
|
-
product_images = [
|
91
|
-
"https://cdn.example.com/products/product1.jpg",
|
92
|
-
"https://cdn.example.com/products/product2.jpg",
|
93
|
-
"https://cdn.example.com/products/product3.jpg"
|
94
|
-
]
|
95
|
-
|
96
|
-
product_images.each_with_index do |image_url, index|
|
97
|
-
client.send_image(app.phone_number, image_url, "Product #{index + 1}")
|
98
|
-
sleep(0.5) # Small delay to avoid rate limiting
|
99
|
-
end
|
100
|
-
|
101
|
-
app.say "Which product interests you the most?"
|
102
|
-
end
|
103
|
-
|
104
|
-
def send_report
|
105
|
-
# Send a PDF report from cloud storage
|
106
|
-
report_url = generate_report_url # Your method to generate/get report URL
|
107
|
-
|
108
|
-
if report_url
|
109
|
-
client = get_whatsapp_client
|
110
|
-
client.send_document(app.phone_number, report_url, "Your monthly report is ready!")
|
111
|
-
|
112
|
-
app.say "📊 Report sent! Please check the document above."
|
113
|
-
else
|
114
|
-
app.say "Sorry, I couldn't generate the report right now. Please try again later."
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
def send_voice_message
|
119
|
-
# Send a pre-recorded voice message from cloud storage
|
120
|
-
audio_url = "https://cdn.example.com/audio/support_greeting.mp3"
|
121
|
-
|
122
|
-
client = get_whatsapp_client
|
123
|
-
client.send_audio(app.phone_number, audio_url)
|
124
|
-
|
125
|
-
app.say "🎵 Please listen to the voice message above. You can also send me a voice message with your question!"
|
126
|
-
end
|
127
|
-
|
128
|
-
def collect_feedback
|
129
|
-
feedback = app.screen(:feedback_text) do |prompt|
|
130
|
-
prompt.ask "Please share your feedback. You can also send images or documents if needed:"
|
54
|
+
app.say "Video received. Analyzing..."
|
131
55
|
end
|
132
|
-
|
133
|
-
# Save feedback to database
|
134
|
-
save_feedback(feedback, app.phone_number)
|
135
|
-
|
136
|
-
# Send a thank you sticker from cloud storage
|
137
|
-
sticker_url = "https://cdn.example.com/stickers/thanks.webp"
|
138
|
-
client = get_whatsapp_client
|
139
|
-
client.send_sticker(app.phone_number, sticker_url)
|
140
|
-
|
141
|
-
app.say "Thank you for your feedback! We really appreciate it. 🙏"
|
142
|
-
end
|
143
|
-
|
144
|
-
def get_whatsapp_client
|
145
|
-
config = FlowChat::Whatsapp::Configuration.from_credentials
|
146
|
-
FlowChat::Whatsapp::Client.new(config)
|
147
|
-
end
|
148
|
-
|
149
|
-
def generate_report_url
|
150
|
-
# Your report generation logic here
|
151
|
-
# This could return a signed URL from S3, Google Cloud Storage, etc.
|
152
|
-
"https://storage.example.com/reports/monthly_report_#{Time.current.strftime("%Y%m")}.pdf"
|
153
|
-
end
|
154
|
-
|
155
|
-
def save_feedback(feedback, phone_number)
|
156
|
-
# Your feedback saving logic here
|
157
|
-
Rails.logger.info "Feedback from #{phone_number}: #{feedback}"
|
158
56
|
end
|
159
57
|
end
|
160
58
|
|
161
|
-
#
|
162
|
-
|
163
|
-
# ============================================================================
|
164
|
-
|
165
|
-
class WhatsAppMediaService
|
59
|
+
# Media service for out-of-band messaging
|
60
|
+
class MediaService
|
166
61
|
def initialize
|
167
62
|
@config = FlowChat::Whatsapp::Configuration.from_credentials
|
168
63
|
@client = FlowChat::Whatsapp::Client.new(@config)
|
169
64
|
end
|
170
65
|
|
171
|
-
# Send welcome package with multiple media types from URLs
|
172
66
|
def send_welcome_package(phone_number, user_name)
|
173
|
-
|
174
|
-
|
175
|
-
@client.send_image(phone_number, welcome_image_url, "Welcome to our service, #{user_name}! 🎉")
|
176
|
-
|
177
|
-
# Welcome guide document from cloud storage
|
178
|
-
guide_url = "https://storage.example.com/guides/user_guide.pdf"
|
179
|
-
@client.send_document(phone_number, guide_url, "Here's your user guide")
|
180
|
-
|
181
|
-
# Welcome audio message from media server
|
182
|
-
audio_url = "https://media.example.com/audio/welcome.mp3"
|
183
|
-
@client.send_audio(phone_number, audio_url)
|
67
|
+
@client.send_image(phone_number, "https://cdn.example.com/welcome.jpg", "Welcome #{user_name}!")
|
68
|
+
@client.send_document(phone_number, "https://storage.example.com/guide.pdf", "User Guide")
|
184
69
|
end
|
185
70
|
|
186
|
-
# Send order confirmation with invoice from cloud storage
|
187
71
|
def send_order_confirmation(phone_number, order_id, invoice_url)
|
188
|
-
|
189
|
-
@client.
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
"Invoice_#{order_id}.pdf"
|
194
|
-
)
|
195
|
-
|
196
|
-
# Send confirmation buttons
|
197
|
-
@client.send_buttons(
|
198
|
-
phone_number,
|
199
|
-
"Your order has been confirmed! 🛍️",
|
200
|
-
[
|
201
|
-
{id: "track_order", title: "📦 Track Order"},
|
202
|
-
{id: "modify_order", title: "✏️ Modify Order"},
|
203
|
-
{id: "support", title: "💬 Contact Support"}
|
204
|
-
]
|
205
|
-
)
|
72
|
+
@client.send_document(phone_number, invoice_url, "Order ##{order_id} confirmed!", "invoice.pdf")
|
73
|
+
@client.send_buttons(phone_number, "Order confirmed! 🛍️", [
|
74
|
+
{id: "track", title: "Track Order"},
|
75
|
+
{id: "support", title: "Contact Support"}
|
76
|
+
])
|
206
77
|
end
|
207
78
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
@client.
|
212
|
-
|
213
|
-
# Optionally send promotional video from video server
|
214
|
-
if promo_video_url
|
215
|
-
@client.send_video(phone_number, promo_video_url, "Watch this exciting video!")
|
216
|
-
end
|
217
|
-
|
218
|
-
# Follow up with action buttons
|
219
|
-
@client.send_buttons(
|
220
|
-
phone_number,
|
221
|
-
"Don't miss out on this amazing deal!",
|
222
|
-
[
|
223
|
-
{id: "buy_now", title: "🛒 Buy Now"},
|
224
|
-
{id: "more_info", title: "ℹ️ More Info"},
|
225
|
-
{id: "remind_later", title: "⏰ Remind Later"}
|
226
|
-
]
|
227
|
-
)
|
228
|
-
end
|
79
|
+
def process_user_media(media_id, media_type, user_phone)
|
80
|
+
# Download and process media
|
81
|
+
@client.get_media_url(media_id)
|
82
|
+
media_content = @client.download_media(media_id)
|
229
83
|
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
cloud_url = upload_to_cloud_storage(media_content, media_type, media_id)
|
239
|
-
|
240
|
-
# Process based on media type
|
241
|
-
case media_type
|
242
|
-
when "image"
|
243
|
-
process_image(cloud_url, user_phone)
|
244
|
-
when "document"
|
245
|
-
process_document(cloud_url, user_phone)
|
246
|
-
when "audio"
|
247
|
-
process_audio(cloud_url, user_phone)
|
248
|
-
when "video"
|
249
|
-
process_video(cloud_url, user_phone)
|
250
|
-
end
|
251
|
-
|
252
|
-
Rails.logger.info "Successfully processed #{media_type} from #{user_phone}"
|
253
|
-
end
|
254
|
-
rescue => e
|
255
|
-
Rails.logger.error "Error processing media: #{e.message}"
|
256
|
-
@client.send_text(user_phone, "Sorry, I couldn't process your file. Please try again.")
|
257
|
-
end
|
258
|
-
|
259
|
-
# Send personalized content based on user data
|
260
|
-
def send_personalized_content(phone_number, user_id)
|
261
|
-
# Get user's preferred content from your system
|
262
|
-
user_content = fetch_user_content(user_id)
|
263
|
-
|
264
|
-
# Send personalized image
|
265
|
-
if user_content[:image_url]
|
266
|
-
@client.send_image(phone_number, user_content[:image_url], user_content[:image_caption])
|
267
|
-
end
|
268
|
-
|
269
|
-
# Send personalized document
|
270
|
-
if user_content[:document_url]
|
271
|
-
@client.send_document(phone_number, user_content[:document_url], user_content[:document_description])
|
84
|
+
# Process based on type
|
85
|
+
case media_type
|
86
|
+
when "image"
|
87
|
+
process_image(media_content, user_phone)
|
88
|
+
when "document"
|
89
|
+
process_document(media_content, user_phone)
|
90
|
+
when "audio"
|
91
|
+
process_audio(media_content, user_phone)
|
272
92
|
end
|
273
93
|
end
|
274
94
|
|
275
|
-
# Send real-time generated content
|
276
|
-
def send_qr_code(phone_number, data)
|
277
|
-
# Generate QR code and get URL (using your QR service)
|
278
|
-
qr_url = generate_qr_code_url(data)
|
279
|
-
|
280
|
-
@client.send_image(phone_number, qr_url, "Here's your QR code!")
|
281
|
-
end
|
282
|
-
|
283
|
-
# Send chart/graph from analytics service
|
284
|
-
def send_analytics_chart(phone_number, chart_type, period)
|
285
|
-
# Generate chart URL from your analytics service
|
286
|
-
chart_url = generate_analytics_chart_url(chart_type, period)
|
287
|
-
|
288
|
-
@client.send_image(phone_number, chart_url, "#{chart_type.humanize} for #{period}")
|
289
|
-
end
|
290
|
-
|
291
95
|
private
|
292
96
|
|
293
|
-
def
|
294
|
-
# Your
|
295
|
-
|
296
|
-
"https://storage.example.com/uploads/#{media_id}.#{get_file_extension(media_type)}"
|
297
|
-
end
|
298
|
-
|
299
|
-
def process_image(cloud_url, user_phone)
|
300
|
-
# Your image processing logic here
|
301
|
-
@client.send_text(user_phone, "Thanks for the image! I've processed it successfully. ✅")
|
302
|
-
end
|
303
|
-
|
304
|
-
def process_document(cloud_url, user_phone)
|
305
|
-
# Your document processing logic here
|
306
|
-
@client.send_text(user_phone, "Document received and processed! 📄")
|
307
|
-
end
|
308
|
-
|
309
|
-
def process_audio(cloud_url, user_phone)
|
310
|
-
# Your audio processing logic here
|
311
|
-
@client.send_text(user_phone, "Audio message processed! 🎵")
|
97
|
+
def process_image(content, phone)
|
98
|
+
# Your image processing logic
|
99
|
+
@client.send_text(phone, "Image processed successfully! ✅")
|
312
100
|
end
|
313
101
|
|
314
|
-
def
|
315
|
-
# Your
|
316
|
-
@client.send_text(
|
102
|
+
def process_document(content, phone)
|
103
|
+
# Your document processing logic
|
104
|
+
@client.send_text(phone, "Document processed! 📄")
|
317
105
|
end
|
318
106
|
|
319
|
-
def
|
320
|
-
#
|
321
|
-
|
322
|
-
image_url: "https://cdn.example.com/personal/#{user_id}/welcome.jpg",
|
323
|
-
image_caption: "Your personalized welcome image!",
|
324
|
-
document_url: "https://storage.example.com/personal/#{user_id}/guide.pdf",
|
325
|
-
document_description: "Your personalized guide"
|
326
|
-
}
|
327
|
-
end
|
328
|
-
|
329
|
-
def generate_qr_code_url(data)
|
330
|
-
# Generate QR code URL using your service (or external API like QR Server)
|
331
|
-
"https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=#{CGI.escape(data)}"
|
332
|
-
end
|
333
|
-
|
334
|
-
def generate_analytics_chart_url(chart_type, period)
|
335
|
-
# Generate chart URL from your analytics service
|
336
|
-
"https://charts.example.com/api/generate?type=#{chart_type}&period=#{period}"
|
337
|
-
end
|
338
|
-
|
339
|
-
def get_file_extension(media_type)
|
340
|
-
case media_type
|
341
|
-
when "image" then "jpg"
|
342
|
-
when "document" then "pdf"
|
343
|
-
when "audio" then "mp3"
|
344
|
-
when "video" then "mp4"
|
345
|
-
else "bin"
|
346
|
-
end
|
107
|
+
def process_audio(content, phone)
|
108
|
+
# Your audio processing logic
|
109
|
+
@client.send_text(phone, "Audio processed! 🎵")
|
347
110
|
end
|
348
111
|
end
|
349
112
|
|
350
|
-
#
|
351
|
-
# USAGE IN CONTROLLERS
|
352
|
-
# ============================================================================
|
353
|
-
|
113
|
+
# Controller example for media notifications
|
354
114
|
class NotificationController < ApplicationController
|
355
115
|
def send_media_notification
|
356
|
-
service =
|
357
|
-
|
358
|
-
# Send welcome package to new users
|
359
|
-
service.send_welcome_package(params[:phone_number], params[:user_name])
|
360
|
-
|
116
|
+
service = MediaService.new
|
117
|
+
service.send_welcome_package(params[:phone], params[:name])
|
361
118
|
render json: {status: "sent"}
|
362
119
|
end
|
363
120
|
|
364
121
|
def send_order_confirmation
|
365
|
-
service =
|
366
|
-
|
367
|
-
# Get invoice URL from your system (could be from S3, Google Cloud, etc.)
|
368
|
-
invoice_url = get_invoice_url(params[:order_id])
|
369
|
-
|
122
|
+
service = MediaService.new
|
370
123
|
service.send_order_confirmation(
|
371
|
-
params[:
|
124
|
+
params[:phone],
|
372
125
|
params[:order_id],
|
373
|
-
|
126
|
+
generate_invoice_url(params[:order_id])
|
374
127
|
)
|
375
|
-
|
376
|
-
render json: {status: "sent"}
|
377
|
-
end
|
378
|
-
|
379
|
-
def send_promo
|
380
|
-
service = WhatsAppMediaService.new
|
381
|
-
|
382
|
-
# Promotional content from CDN
|
383
|
-
promo_image = "https://cdn.example.com/promos/#{params[:promo_id]}/banner.jpg"
|
384
|
-
promo_video = "https://cdn.example.com/promos/#{params[:promo_id]}/video.mp4"
|
385
|
-
|
386
|
-
service.send_promotion(params[:phone_number], promo_image, promo_video)
|
387
|
-
|
388
|
-
render json: {status: "sent"}
|
389
|
-
end
|
390
|
-
|
391
|
-
def send_qr_code
|
392
|
-
service = WhatsAppMediaService.new
|
393
|
-
service.send_qr_code(params[:phone_number], params[:qr_data])
|
394
|
-
|
395
128
|
render json: {status: "sent"}
|
396
129
|
end
|
397
130
|
|
398
131
|
private
|
399
132
|
|
400
|
-
def
|
401
|
-
# Your logic to get invoice URL from cloud storage
|
133
|
+
def generate_invoice_url(order_id)
|
402
134
|
"https://storage.example.com/invoices/#{order_id}.pdf"
|
403
135
|
end
|
404
136
|
end
|
@@ -15,6 +15,9 @@ end
|
|
15
15
|
class AdvancedWhatsappMessageJob < ApplicationJob
|
16
16
|
include FlowChat::Whatsapp::SendJobSupport
|
17
17
|
|
18
|
+
queue_as :whatsapp_priority
|
19
|
+
retry_on StandardError, wait: 2.seconds, attempts: 3
|
20
|
+
|
18
21
|
def perform(send_data)
|
19
22
|
perform_whatsapp_send(send_data)
|
20
23
|
end
|
@@ -72,18 +75,17 @@ class MultiTenantWhatsappSendJob < ApplicationJob
|
|
72
75
|
|
73
76
|
# Override config resolution for tenant-specific configs
|
74
77
|
def resolve_whatsapp_config(send_data)
|
75
|
-
#
|
78
|
+
# Use tenant-specific config if available
|
76
79
|
tenant_name = extract_tenant_from_phone(send_data[:msisdn])
|
77
80
|
if tenant_name && FlowChat::Whatsapp::Configuration.exists?(tenant_name)
|
78
81
|
return FlowChat::Whatsapp::Configuration.get(tenant_name)
|
79
82
|
end
|
80
83
|
|
81
|
-
# Fallback to default
|
84
|
+
# Fallback to default
|
82
85
|
super
|
83
86
|
end
|
84
87
|
|
85
88
|
def extract_tenant_from_phone(phone)
|
86
|
-
# Extract tenant from phone number prefix or other identifier
|
87
89
|
case phone
|
88
90
|
when /^\+1800/
|
89
91
|
:enterprise
|
data/flow_chat.gemspec
CHANGED
@@ -6,8 +6,12 @@ Gem::Specification.new do |spec|
|
|
6
6
|
spec.authors = ["Stefan Froelich"]
|
7
7
|
spec.email = ["sfroelich01@gmail.com"]
|
8
8
|
|
9
|
-
spec.summary = "
|
10
|
-
spec.description =
|
9
|
+
spec.summary = "Build conversational interfaces for USSD and WhatsApp with Rails"
|
10
|
+
spec.description = <<~DESC
|
11
|
+
FlowChat is a Rails framework for building sophisticated conversational interfaces across USSD and WhatsApp platforms.
|
12
|
+
Create interactive flows with menus, prompts, validation, media support, and session management. Features include
|
13
|
+
multi-tenancy, background job processing, built-in simulator for testing, and comprehensive middleware support.
|
14
|
+
DESC
|
11
15
|
spec.homepage = "https://github.com/radioactive-labs/flow_chat"
|
12
16
|
spec.license = "MIT"
|
13
17
|
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
@@ -2,42 +2,86 @@ require "middleware"
|
|
2
2
|
|
3
3
|
module FlowChat
|
4
4
|
class BaseProcessor
|
5
|
+
include FlowChat::Instrumentation
|
6
|
+
|
5
7
|
attr_reader :middleware
|
6
8
|
|
7
9
|
def initialize(controller, enable_simulator: nil)
|
10
|
+
FlowChat.logger.debug { "BaseProcessor: Initializing processor for controller #{controller.class.name}" }
|
11
|
+
|
8
12
|
@context = FlowChat::Context.new
|
9
13
|
@context["controller"] = controller
|
10
14
|
@context["enable_simulator"] = enable_simulator.nil? ? (defined?(Rails) && Rails.env.local?) : enable_simulator
|
11
15
|
@middleware = ::Middleware::Builder.new(name: middleware_name)
|
12
16
|
|
17
|
+
FlowChat.logger.debug { "BaseProcessor: Simulator mode #{@context["enable_simulator"] ? "enabled" : "disabled"}" }
|
18
|
+
|
13
19
|
yield self if block_given?
|
20
|
+
|
21
|
+
FlowChat.logger.debug { "BaseProcessor: Initialized #{self.class.name} successfully" }
|
14
22
|
end
|
15
23
|
|
16
24
|
def use_gateway(gateway_class, *args)
|
25
|
+
FlowChat.logger.debug { "BaseProcessor: Configuring gateway #{gateway_class.name} with args: #{args.inspect}" }
|
17
26
|
@gateway_class = gateway_class
|
18
27
|
@gateway_args = args
|
19
28
|
self
|
20
29
|
end
|
21
30
|
|
22
31
|
def use_session_store(session_store)
|
32
|
+
FlowChat.logger.debug { "BaseProcessor: Configuring session store #{session_store.class.name}" }
|
23
33
|
@context["session.store"] = session_store
|
24
34
|
self
|
25
35
|
end
|
26
36
|
|
27
37
|
def use_middleware(middleware)
|
38
|
+
FlowChat.logger.debug { "BaseProcessor: Adding middleware #{middleware.class.name}" }
|
28
39
|
@middleware.use middleware
|
29
40
|
self
|
30
41
|
end
|
31
42
|
|
32
43
|
def run(flow_class, action)
|
44
|
+
# Instrument flow execution (this will log via LogSubscriber)
|
45
|
+
instrument(Events::FLOW_EXECUTION_START, {
|
46
|
+
flow_name: flow_class.name.underscore,
|
47
|
+
action: action.to_s,
|
48
|
+
processor_type: self.class.name
|
49
|
+
})
|
50
|
+
|
33
51
|
@context["flow.name"] = flow_class.name.underscore
|
34
52
|
@context["flow.class"] = flow_class
|
35
53
|
@context["flow.action"] = action
|
36
54
|
|
55
|
+
FlowChat.logger.debug { "BaseProcessor: Context prepared for flow #{flow_class.name}" }
|
56
|
+
|
37
57
|
stack = build_middleware_stack
|
38
58
|
yield stack if block_given?
|
39
59
|
|
40
|
-
stack.
|
60
|
+
FlowChat.logger.debug { "BaseProcessor: Executing middleware stack for #{flow_class.name}##{action}" }
|
61
|
+
|
62
|
+
# Instrument flow execution with timing (this will log completion via LogSubscriber)
|
63
|
+
instrument(Events::FLOW_EXECUTION_END, {
|
64
|
+
flow_name: flow_class.name.underscore,
|
65
|
+
action: action.to_s,
|
66
|
+
processor_type: self.class.name
|
67
|
+
}) do
|
68
|
+
stack.call(@context)
|
69
|
+
end
|
70
|
+
rescue => error
|
71
|
+
FlowChat.logger.error { "BaseProcessor: Flow execution failed - #{flow_class.name}##{action}, Error: #{error.class.name}: #{error.message}" }
|
72
|
+
FlowChat.logger.debug { "BaseProcessor: Stack trace: #{error.backtrace.join("\n")}" }
|
73
|
+
|
74
|
+
# Instrument flow execution error (this will log error via LogSubscriber)
|
75
|
+
instrument(Events::FLOW_EXECUTION_ERROR, {
|
76
|
+
flow_name: flow_class.name.underscore,
|
77
|
+
action: action.to_s,
|
78
|
+
processor_type: self.class.name,
|
79
|
+
error_class: error.class.name,
|
80
|
+
error_message: error.message,
|
81
|
+
backtrace: error.backtrace&.first(10)
|
82
|
+
})
|
83
|
+
|
84
|
+
raise
|
41
85
|
end
|
42
86
|
|
43
87
|
protected
|
@@ -58,11 +102,13 @@ module FlowChat
|
|
58
102
|
::Middleware::Builder.new(name: name) do |b|
|
59
103
|
b.use @gateway_class, *@gateway_args
|
60
104
|
configure_middleware_stack(b)
|
61
|
-
end.inject_logger(
|
105
|
+
end.inject_logger(FlowChat.logger)
|
62
106
|
end
|
63
107
|
|
64
108
|
def configure_middleware_stack(builder)
|
65
109
|
raise NotImplementedError, "Subclasses must implement configure_middleware_stack"
|
66
110
|
end
|
111
|
+
|
112
|
+
attr_reader :context
|
67
113
|
end
|
68
114
|
end
|
data/lib/flow_chat/config.rb
CHANGED
data/lib/flow_chat/context.rb
CHANGED
@@ -1,26 +1,38 @@
|
|
1
1
|
module FlowChat
|
2
2
|
class Context
|
3
|
+
include FlowChat::Instrumentation
|
4
|
+
|
3
5
|
def initialize
|
4
6
|
@data = {}.with_indifferent_access
|
7
|
+
|
8
|
+
# Use instrumentation for context creation
|
9
|
+
self.class.instrument(Events::CONTEXT_CREATED, {
|
10
|
+
gateway: @data["request.gateway"]
|
11
|
+
})
|
5
12
|
end
|
6
13
|
|
7
14
|
def [](key)
|
8
|
-
@data[key]
|
15
|
+
value = @data[key]
|
16
|
+
FlowChat.logger.debug { "Context: Getting '#{key}' = #{value.inspect}" } if key != "session.store" # Avoid logging session store object
|
17
|
+
value
|
9
18
|
end
|
10
19
|
|
11
20
|
def []=(key, value)
|
21
|
+
FlowChat.logger.debug { "Context: Setting '#{key}' = #{value.inspect}" } if key != "session.store" && key != "controller" # Avoid logging large objects
|
12
22
|
@data[key] = value
|
13
23
|
end
|
14
24
|
|
15
25
|
def input = @data["request.input"]
|
16
26
|
|
17
27
|
def input=(value)
|
28
|
+
FlowChat.logger.debug { "Context: Setting input = '#{value}'" }
|
18
29
|
@data["request.input"] = value
|
19
30
|
end
|
20
31
|
|
21
32
|
def session = @data["session"]
|
22
33
|
|
23
34
|
def session=(value)
|
35
|
+
FlowChat.logger.debug { "Context: Setting session = #{value.class.name}" }
|
24
36
|
@data["session"] = value
|
25
37
|
end
|
26
38
|
|