flow_chat 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ebeb5cdcbacce73ec580e381554dbd2377f7896d7d06c488970051f6a8bf599
4
- data.tar.gz: bc33f9f54c78264f416e00163e2ba0cf78da2e8e0961c942bb3aab1e86e783e5
3
+ metadata.gz: abe12ed32427797b8d7c5dc79766bae76113ec62df415e495f900e4216af5c3f
4
+ data.tar.gz: d344431fbde29789a0013a78dd099b253b65dc9e8af90ef2d403a5592b5bb7aa
5
5
  SHA512:
6
- metadata.gz: 34006bb23ddaf1aefd8ec4876cb70d8f06d2bbb23da48663ff7a40dd630d9b01af58d9980c343c9d933fa637512375b5c7ba3b0b336ba91a314fdd9ae5fb2ba1
7
- data.tar.gz: 78a242f2b175f57ce29326f342c9d4bf4c7c4402c0e2fcd634c21322280dcd0ac273d86eb19750702472218e9d08fd97267ad711b3673f108f89617e5319b9f7
6
+ metadata.gz: b05a61d0e7a41db5ecd2137aaa193c7431f1e16277e093af4eacbcf657696aa42a22d177407718618959b021298a3583101004534d3666a51b2607b4f621db88
7
+ data.tar.gz: a9aab01a2754996205c2b9c1fa8143cdf503f4033c243ed537120c975249eebc0b0cc42f310864a8923b8fd819cc625205cd32463f15e5db93ccd88c2c4ecfe1
@@ -0,0 +1,74 @@
1
+ module FlowChat
2
+ class BaseApp
3
+ attr_reader :session, :input, :context, :navigation_stack
4
+
5
+ def initialize(context)
6
+ @context = context
7
+ @session = context.session
8
+ @input = context.input
9
+ @navigation_stack = []
10
+ end
11
+
12
+ def screen(key)
13
+ raise ArgumentError, "a block is expected" unless block_given?
14
+ raise ArgumentError, "screen has already been presented" if navigation_stack.include?(key)
15
+
16
+ navigation_stack << key
17
+ return session.get(key) if session.get(key).present?
18
+
19
+ user_input = prepare_user_input
20
+ prompt = FlowChat::Prompt.new user_input
21
+ @input = nil # input is being submitted to prompt so we clear it
22
+
23
+ value = yield prompt
24
+ session.set(key, value)
25
+ value
26
+ end
27
+
28
+ def go_back
29
+ return false if navigation_stack.empty?
30
+
31
+ @context.input = nil
32
+ current_screen = navigation_stack.last
33
+ session.delete(current_screen)
34
+
35
+ # Restart the flow from the beginning
36
+ raise FlowChat::Interrupt::RestartFlow.new
37
+ end
38
+
39
+ def say(msg, media: nil)
40
+ raise FlowChat::Interrupt::Terminate.new(msg, media: media)
41
+ end
42
+
43
+ def phone_number
44
+ context["request.msisdn"]
45
+ end
46
+
47
+ def message_id
48
+ context["request.message_id"]
49
+ end
50
+
51
+ def timestamp
52
+ context["request.timestamp"]
53
+ end
54
+
55
+ def contact_name
56
+ nil
57
+ end
58
+
59
+ def location
60
+ nil
61
+ end
62
+
63
+ def media
64
+ nil
65
+ end
66
+
67
+ protected
68
+
69
+ # Platform-specific methods to be overridden
70
+ def prepare_user_input
71
+ input
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,57 @@
1
+ module FlowChat
2
+ class BaseExecutor
3
+ def initialize(app)
4
+ @app = app
5
+ FlowChat.logger.debug { "#{log_prefix}: Initialized #{platform_name} executor middleware" }
6
+ end
7
+
8
+ def call(context)
9
+ flow_class = context.flow
10
+ action = context["flow.action"]
11
+ session_id = context["session.id"]
12
+
13
+ FlowChat.logger.info { "#{log_prefix}: Executing flow #{flow_class.name}##{action} for session #{session_id}" }
14
+
15
+ platform_app = build_platform_app(context)
16
+ FlowChat.logger.debug { "#{log_prefix}: #{platform_name} app built for flow execution" }
17
+
18
+ flow = flow_class.new platform_app
19
+ FlowChat.logger.debug { "#{log_prefix}: Flow instance created, invoking #{action} method" }
20
+
21
+ flow.send action
22
+ FlowChat.logger.warn { "#{log_prefix}: Flow execution failed to interact with user for #{flow_class.name}##{action}" }
23
+ raise FlowChat::Interrupt::Terminate, "Unexpected end of flow."
24
+ rescue FlowChat::Interrupt::RestartFlow => e
25
+ FlowChat.logger.info { "#{log_prefix}: Flow restart requested - Session: #{session_id}, restarting #{action}" }
26
+ retry
27
+ rescue FlowChat::Interrupt::Prompt => e
28
+ FlowChat.logger.info { "#{log_prefix}: Flow prompted user - Session: #{session_id}, Prompt: '#{e.prompt&.truncate(100)}'" }
29
+ FlowChat.logger.debug { "#{log_prefix}: Prompt details - Choices: #{e.choices&.size || 0}, Has media: #{!e.media.nil?}" }
30
+ [:prompt, e.prompt, e.choices, e.media]
31
+ rescue FlowChat::Interrupt::Terminate => e
32
+ FlowChat.logger.info { "#{log_prefix}: Flow terminated - Session: #{session_id}, Message: '#{e.prompt&.truncate(100)}'" }
33
+ FlowChat.logger.debug { "#{log_prefix}: Destroying session #{session_id}" }
34
+ context.session.destroy
35
+ [:terminate, e.prompt, nil, e.media]
36
+ rescue => error
37
+ FlowChat.logger.error { "#{log_prefix}: Flow execution failed - #{flow_class.name}##{action}, Session: #{session_id}, Error: #{error.class.name}: #{error.message}" }
38
+ FlowChat.logger.debug { "#{log_prefix}: Stack trace: #{error.backtrace.join("\n")}" }
39
+ raise
40
+ end
41
+
42
+ protected
43
+
44
+ # Subclasses must implement these methods
45
+ def platform_name
46
+ raise NotImplementedError, "Subclasses must implement platform_name"
47
+ end
48
+
49
+ def log_prefix
50
+ raise NotImplementedError, "Subclasses must implement log_prefix"
51
+ end
52
+
53
+ def build_platform_app(context)
54
+ raise NotImplementedError, "Subclasses must implement build_platform_app"
55
+ end
56
+ end
57
+ end
@@ -22,7 +22,6 @@ module FlowChat
22
22
  def setup_logging!(options = {})
23
23
  return if @log_subscriber_setup
24
24
 
25
- require_relative "log_subscriber"
26
25
  setup_log_subscriber(options)
27
26
  @log_subscriber_setup = true
28
27
  end
@@ -31,7 +30,6 @@ module FlowChat
31
30
  def setup_metrics!(options = {})
32
31
  return if @metrics_collector_setup
33
32
 
34
- require_relative "metrics_collector"
35
33
  setup_metrics_collector(options)
36
34
  @metrics_collector_setup = true
37
35
  end
@@ -22,5 +22,11 @@ module FlowChat
22
22
  end
23
23
 
24
24
  class Terminate < Base; end
25
+
26
+ class RestartFlow < Base
27
+ def initialize
28
+ super("restart_flow")
29
+ end
30
+ end
25
31
  end
26
32
  end
@@ -1,58 +1,6 @@
1
1
  module FlowChat
2
2
  module Ussd
3
- class App
4
- attr_reader :session, :input, :context, :navigation_stack
5
-
6
- def initialize(context)
7
- @context = context
8
- @session = context.session
9
- @input = context.input
10
- @navigation_stack = []
11
- end
12
-
13
- def screen(key)
14
- raise ArgumentError, "a block is expected" unless block_given?
15
- raise ArgumentError, "screen has already been presented" if navigation_stack.include?(key)
16
-
17
- navigation_stack << key
18
- return session.get(key) if session.get(key).present?
19
-
20
- prompt = FlowChat::Prompt.new input
21
- @input = nil # input is being submitted to prompt so we clear it
22
-
23
- value = yield prompt
24
- session.set(key, value)
25
- value
26
- end
27
-
28
- def say(msg, media: nil)
29
- raise FlowChat::Interrupt::Terminate.new(msg)
30
- end
31
-
32
- # WhatsApp-specific data accessors (not supported in USSD)
33
- def contact_name
34
- nil
35
- end
36
-
37
- def message_id
38
- context["request.message_id"]
39
- end
40
-
41
- def timestamp
42
- context["request.timestamp"]
43
- end
44
-
45
- def location
46
- nil
47
- end
48
-
49
- def media
50
- nil
51
- end
52
-
53
- def phone_number
54
- context["request.msisdn"]
55
- end
3
+ class App < FlowChat::BaseApp
56
4
  end
57
5
  end
58
6
  end
@@ -1,47 +1,21 @@
1
+ require_relative "../../base_executor"
2
+
1
3
  module FlowChat
2
4
  module Ussd
3
5
  module Middleware
4
- class Executor
5
- def initialize(app)
6
- @app = app
7
- FlowChat.logger.debug { "Ussd::Executor: Initialized USSD executor middleware" }
8
- end
9
-
10
- def call(context)
11
- flow_class = context.flow
12
- action = context["flow.action"]
13
- session_id = context["session.id"]
14
-
15
- FlowChat.logger.info { "Ussd::Executor: Executing flow #{flow_class.name}##{action} for session #{session_id}" }
6
+ class Executor < FlowChat::BaseExecutor
7
+ protected
16
8
 
17
- ussd_app = build_ussd_app context
18
- FlowChat.logger.debug { "Ussd::Executor: USSD app built for flow execution" }
19
-
20
- flow = flow_class.new ussd_app
21
- FlowChat.logger.debug { "Ussd::Executor: Flow instance created, invoking #{action} method" }
22
-
23
- flow.send action
24
- FlowChat.logger.warn { "Ussd::Executor: Flow execution failed to interact with user for #{flow_class.name}##{action}" }
25
- raise FlowChat::Interrupt::Terminate, "Unexpected end of flow."
26
- rescue FlowChat::Interrupt::Prompt => e
27
- FlowChat.logger.info { "Ussd::Executor: Flow prompted user - Session: #{session_id}, Prompt: '#{e.prompt.truncate(100)}'" }
28
- FlowChat.logger.debug { "Ussd::Executor: Prompt details - Choices: #{e.choices&.size || 0}, Has media: #{!e.media.nil?}" }
29
- [:prompt, e.prompt, e.choices, e.media]
30
- rescue FlowChat::Interrupt::Terminate => e
31
- FlowChat.logger.info { "Ussd::Executor: Flow terminated - Session: #{session_id}, Message: '#{e.prompt.truncate(100)}'" }
32
- FlowChat.logger.debug { "Ussd::Executor: Destroying session #{session_id}" }
33
- context.session.destroy
34
- [:terminate, e.prompt, nil, e.media]
35
- rescue => error
36
- FlowChat.logger.error { "Ussd::Executor: Flow execution failed - #{flow_class.name}##{action}, Session: #{session_id}, Error: #{error.class.name}: #{error.message}" }
37
- FlowChat.logger.debug { "Ussd::Executor: Stack trace: #{error.backtrace.join("\n")}" }
38
- raise
9
+ def platform_name
10
+ "USSD"
39
11
  end
40
12
 
41
- private
13
+ def log_prefix
14
+ "Ussd::Executor"
15
+ end
42
16
 
43
- def build_ussd_app(context)
44
- FlowChat.logger.debug { "Ussd::Executor: Building USSD app instance" }
17
+ def build_platform_app(context)
18
+ FlowChat.logger.debug { "#{log_prefix}: Building USSD app instance" }
45
19
  FlowChat::Ussd::App.new(context)
46
20
  end
47
21
  end
@@ -1,3 +1,3 @@
1
1
  module FlowChat
2
- VERSION = "0.8.0"
2
+ VERSION = "0.8.1"
3
3
  end
@@ -1,53 +1,10 @@
1
1
  module FlowChat
2
2
  module Whatsapp
3
- class App
4
- attr_reader :session, :input, :context, :navigation_stack
5
-
6
- def initialize(context)
7
- @context = context
8
- @session = context.session
9
- @input = context.input
10
- @navigation_stack = []
11
- end
12
-
13
- def screen(key)
14
- raise ArgumentError, "a block is expected" unless block_given?
15
- raise ArgumentError, "screen has already been presented" if navigation_stack.include?(key)
16
-
17
- navigation_stack << key
18
- return session.get(key) if session.get(key).present?
19
-
20
- user_input = input
21
- if session.get("$started_at$").nil?
22
- session.set("$started_at$", Time.current.iso8601)
23
- user_input = nil
24
- end
25
-
26
- prompt = FlowChat::Prompt.new user_input
27
- @input = nil # input is being submitted to prompt so we clear it
28
-
29
- value = yield prompt
30
- session.set(key, value)
31
- value
32
- end
33
-
34
- def say(msg, media: nil)
35
- raise FlowChat::Interrupt::Terminate.new(msg, media: media)
36
- end
37
-
38
- # WhatsApp-specific data accessors (read-only)
3
+ class App < FlowChat::BaseApp
39
4
  def contact_name
40
5
  context["request.contact_name"]
41
6
  end
42
7
 
43
- def message_id
44
- context["request.message_id"]
45
- end
46
-
47
- def timestamp
48
- context["request.timestamp"]
49
- end
50
-
51
8
  def location
52
9
  context["request.location"]
53
10
  end
@@ -56,8 +13,16 @@ module FlowChat
56
13
  context["request.media"]
57
14
  end
58
15
 
59
- def phone_number
60
- context["request.msisdn"]
16
+ protected
17
+
18
+ # WhatsApp has special startup logic and supports media
19
+ def prepare_user_input
20
+ user_input = input
21
+ if session.get("$started_at$").nil?
22
+ session.set("$started_at$", Time.current.iso8601)
23
+ user_input = nil
24
+ end
25
+ user_input
61
26
  end
62
27
  end
63
28
  end
@@ -1,49 +1,21 @@
1
+ require_relative "../../base_executor"
2
+
1
3
  module FlowChat
2
4
  module Whatsapp
3
5
  module Middleware
4
- class Executor
5
- def initialize(app)
6
- @app = app
7
- FlowChat.logger.debug { "Whatsapp::Executor: Initialized WhatsApp executor middleware" }
8
- end
9
-
10
- def call(context)
11
- flow_class = context.flow
12
- action = context["flow.action"]
13
- session_id = context["session.id"]
14
-
15
- FlowChat.logger.info { "Whatsapp::Executor: Executing flow #{flow_class.name}##{action} for session #{session_id}" }
6
+ class Executor < FlowChat::BaseExecutor
7
+ protected
16
8
 
17
- whatsapp_app = build_whatsapp_app context
18
- FlowChat.logger.debug { "Whatsapp::Executor: WhatsApp app built for flow execution" }
19
-
20
- flow = flow_class.new whatsapp_app
21
- FlowChat.logger.debug { "Whatsapp::Executor: Flow instance created, invoking #{action} method" }
22
-
23
- flow.send action
24
- FlowChat.logger.warn { "Whatsapp::Executor: Flow execution failed to interact with user for #{flow_class.name}##{action}" }
25
- raise FlowChat::Interrupt::Terminate, "Unexpected end of flow."
26
- rescue FlowChat::Interrupt::Prompt => e
27
- FlowChat.logger.info { "Whatsapp::Executor: Flow prompted user - Session: #{session_id}, Prompt: '#{e.prompt.truncate(100)}'" }
28
- FlowChat.logger.debug { "Whatsapp::Executor: Prompt details - Choices: #{e.choices&.size || 0}, Has media: #{!e.media.nil?}" }
29
- # Return the same triplet format as USSD for consistency
30
- [:prompt, e.prompt, e.choices, e.media]
31
- rescue FlowChat::Interrupt::Terminate => e
32
- FlowChat.logger.info { "Whatsapp::Executor: Flow terminated - Session: #{session_id}, Message: '#{e.prompt.truncate(100)}'" }
33
- FlowChat.logger.debug { "Whatsapp::Executor: Destroying session #{session_id}" }
34
- # Clean up session and return terminal message
35
- context.session.destroy
36
- [:terminate, e.prompt, nil, e.media]
37
- rescue => error
38
- FlowChat.logger.error { "Whatsapp::Executor: Flow execution failed - #{flow_class.name}##{action}, Session: #{session_id}, Error: #{error.class.name}: #{error.message}" }
39
- FlowChat.logger.debug { "Whatsapp::Executor: Stack trace: #{error.backtrace.join("\n")}" }
40
- raise
9
+ def platform_name
10
+ "WhatsApp"
41
11
  end
42
12
 
43
- private
13
+ def log_prefix
14
+ "Whatsapp::Executor"
15
+ end
44
16
 
45
- def build_whatsapp_app(context)
46
- FlowChat.logger.debug { "Whatsapp::Executor: Building WhatsApp app instance" }
17
+ def build_platform_app(context)
18
+ FlowChat.logger.debug { "#{log_prefix}: Building WhatsApp app instance" }
47
19
  FlowChat::Whatsapp::App.new(context)
48
20
  end
49
21
  end
data/lib/flow_chat.rb CHANGED
@@ -15,7 +15,6 @@ module FlowChat
15
15
  end
16
16
 
17
17
  def self.setup_instrumentation!
18
- require_relative "flow_chat/instrumentation/setup"
19
18
  FlowChat::Instrumentation::Setup.setup_instrumentation!
20
19
  end
21
20
 
@@ -27,13 +26,4 @@ module FlowChat
27
26
  def self.metrics
28
27
  FlowChat::Instrumentation::Setup.metrics_collector
29
28
  end
30
- end
31
-
32
- loader.eager_load
33
-
34
- # Auto-setup instrumentation in Rails environments
35
- if defined?(Rails)
36
- Rails.application.config.after_initialize do
37
- FlowChat.setup_instrumentation!
38
- end
39
- end
29
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flow_chat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-06-09 00:00:00.000000000 Z
11
+ date: 2025-06-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -119,6 +119,8 @@ files:
119
119
  - examples/whatsapp_message_job.rb
120
120
  - flow_chat.gemspec
121
121
  - lib/flow_chat.rb
122
+ - lib/flow_chat/base_app.rb
123
+ - lib/flow_chat/base_executor.rb
122
124
  - lib/flow_chat/base_processor.rb
123
125
  - lib/flow_chat/config.rb
124
126
  - lib/flow_chat/context.rb