flow_chat 0.6.0 → 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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +44 -0
  3. data/.gitignore +2 -1
  4. data/README.md +84 -1229
  5. data/docs/configuration.md +337 -0
  6. data/docs/flows.md +320 -0
  7. data/docs/images/simulator.png +0 -0
  8. data/docs/instrumentation.md +216 -0
  9. data/docs/media.md +153 -0
  10. data/docs/testing.md +475 -0
  11. data/docs/ussd-setup.md +306 -0
  12. data/docs/whatsapp-setup.md +162 -0
  13. data/examples/multi_tenant_whatsapp_controller.rb +9 -37
  14. data/examples/simulator_controller.rb +9 -18
  15. data/examples/ussd_controller.rb +32 -38
  16. data/examples/whatsapp_controller.rb +32 -125
  17. data/examples/whatsapp_media_examples.rb +68 -336
  18. data/examples/whatsapp_message_job.rb +5 -3
  19. data/flow_chat.gemspec +6 -2
  20. data/lib/flow_chat/base_processor.rb +48 -2
  21. data/lib/flow_chat/config.rb +5 -0
  22. data/lib/flow_chat/context.rb +13 -1
  23. data/lib/flow_chat/instrumentation/log_subscriber.rb +176 -0
  24. data/lib/flow_chat/instrumentation/metrics_collector.rb +197 -0
  25. data/lib/flow_chat/instrumentation/setup.rb +155 -0
  26. data/lib/flow_chat/instrumentation.rb +70 -0
  27. data/lib/flow_chat/prompt.rb +20 -20
  28. data/lib/flow_chat/session/cache_session_store.rb +73 -7
  29. data/lib/flow_chat/session/middleware.rb +37 -4
  30. data/lib/flow_chat/session/rails_session_store.rb +36 -1
  31. data/lib/flow_chat/simulator/controller.rb +7 -7
  32. data/lib/flow_chat/ussd/app.rb +1 -1
  33. data/lib/flow_chat/ussd/gateway/nalo.rb +30 -0
  34. data/lib/flow_chat/ussd/gateway/nsano.rb +33 -0
  35. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +109 -0
  36. data/lib/flow_chat/ussd/middleware/executor.rb +24 -2
  37. data/lib/flow_chat/ussd/middleware/pagination.rb +87 -7
  38. data/lib/flow_chat/ussd/processor.rb +14 -0
  39. data/lib/flow_chat/ussd/renderer.rb +1 -1
  40. data/lib/flow_chat/version.rb +1 -1
  41. data/lib/flow_chat/whatsapp/app.rb +1 -1
  42. data/lib/flow_chat/whatsapp/client.rb +99 -12
  43. data/lib/flow_chat/whatsapp/configuration.rb +35 -4
  44. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +128 -54
  45. data/lib/flow_chat/whatsapp/middleware/executor.rb +24 -2
  46. data/lib/flow_chat/whatsapp/processor.rb +8 -0
  47. data/lib/flow_chat/whatsapp/renderer.rb +4 -9
  48. data/lib/flow_chat.rb +23 -0
  49. metadata +22 -11
  50. data/.travis.yml +0 -6
  51. data/app/controllers/demo_controller.rb +0 -101
  52. data/app/flow_chat/demo_restaurant_flow.rb +0 -889
  53. data/config/routes_demo.rb +0 -59
  54. data/examples/initializer.rb +0 -86
  55. data/examples/media_prompts_examples.rb +0 -27
  56. data/images/ussd_simulator.png +0 -0
@@ -1,404 +1,136 @@
1
- # WhatsApp Media Messaging Examples
2
- # This file demonstrates how to send different types of media via WhatsApp using FlowChat
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 an image from URL with caption
13
- client.send_image("+1234567890", "https://example.com/images/product.jpg", "Check out this amazing photo!")
14
-
15
- # Send a document from URL
16
- client.send_document("+1234567890", "https://example.com/reports/monthly_report.pdf", "Here's the monthly report")
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
- class MediaSupportFlow < FlowChat::Flow
15
+ # Using media in flows
16
+ class MediaFlow < FlowChat::Flow
35
17
  def main_page
36
- # Handle incoming media from user
18
+ # Handle incoming media
37
19
  if app.media
38
- handle_received_media
20
+ handle_user_media
39
21
  return
40
22
  end
41
23
 
42
- choice = app.screen(:main_menu) do |prompt|
43
- prompt.select "Welcome! How can I help you?", {
44
- "catalog" => "📷 View Product Catalog",
45
- "report" => "📄 Get Report",
46
- "support" => "🎵 Voice Support",
47
- "feedback" => "📝 Send Feedback"
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
- case choice
52
- when "catalog"
53
- send_product_catalog
54
- when "report"
55
- send_report
56
- when "support"
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 handle_received_media
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! I can see it's a #{media_type} file. Let me process it for you."
48
+ app.say "Thanks for the image! Processing..."
74
49
  when "document"
75
- app.say "I've received your document. I'll review it and get back to you shortly."
50
+ app.say "Document received. Reviewing..."
76
51
  when "audio"
77
- app.say "Got your voice message! I'll listen to it and respond appropriately."
52
+ app.say "Got your voice message!"
78
53
  when "video"
79
- app.say "Thanks for the video! I'll analyze it and provide feedback."
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
- # ADVANCED MEDIA SERVICE CLASS
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
- # Welcome image from CDN
174
- welcome_image_url = "https://cdn.example.com/welcome/banner.jpg"
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
- # Send invoice document from cloud storage
189
- @client.send_document(
190
- phone_number,
191
- invoice_url,
192
- "Order ##{order_id} confirmed! Here's your invoice.",
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
- # Send promotional content from CDN
209
- def send_promotion(phone_number, promo_image_url, promo_video_url = nil)
210
- # Send promotional image from CDN
211
- @client.send_image(phone_number, promo_image_url, "🔥 Special offer just for you!")
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
- # Handle media uploads with processing
231
- def process_uploaded_media(media_id, media_type, user_phone)
232
- # Download the media from WhatsApp
233
- media_url = @client.get_media_url(media_id)
234
- media_content = @client.download_media(media_id) if media_url
235
-
236
- if media_content
237
- # Upload to your cloud storage (S3, Google Cloud, etc.)
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 upload_to_cloud_storage(content, media_type, media_id)
294
- # Your cloud storage upload logic here
295
- # Return the public URL of the uploaded file
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 process_video(cloud_url, user_phone)
315
- # Your video processing logic here
316
- @client.send_text(user_phone, "Video processed successfully! 🎥")
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 fetch_user_content(user_id)
320
- # Fetch personalized content URLs from your database
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 = WhatsAppMediaService.new
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 = WhatsAppMediaService.new
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[:phone_number],
124
+ params[:phone],
372
125
  params[:order_id],
373
- invoice_url
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 get_invoice_url(order_id)
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
- # Try tenant-specific config first
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 resolution
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 = "Framework for building Menu based conversations (e.g. USSD) in Rails."
10
- spec.description = "Framework for building Menu based conversations (e.g. USSD) in Rails."
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.call(@context)
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(Rails.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
@@ -68,4 +68,9 @@ module FlowChat
68
68
  end
69
69
  end
70
70
  end
71
+
72
+ # Shorthand for accessing the logger throughout the application
73
+ def self.logger
74
+ Config.logger
75
+ end
71
76
  end
@@ -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