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 +4 -4
- data/lib/flow_chat/base_app.rb +74 -0
- data/lib/flow_chat/base_executor.rb +57 -0
- data/lib/flow_chat/instrumentation/setup.rb +0 -2
- data/lib/flow_chat/interrupt.rb +6 -0
- data/lib/flow_chat/ussd/app.rb +1 -53
- data/lib/flow_chat/ussd/middleware/executor.rb +11 -37
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/app.rb +11 -46
- data/lib/flow_chat/whatsapp/middleware/executor.rb +11 -39
- data/lib/flow_chat.rb +1 -11
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: abe12ed32427797b8d7c5dc79766bae76113ec62df415e495f900e4216af5c3f
|
4
|
+
data.tar.gz: d344431fbde29789a0013a78dd099b253b65dc9e8af90ef2d403a5592b5bb7aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/flow_chat/interrupt.rb
CHANGED
data/lib/flow_chat/ussd/app.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
18
|
-
|
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
|
-
|
13
|
+
def log_prefix
|
14
|
+
"Ussd::Executor"
|
15
|
+
end
|
42
16
|
|
43
|
-
def
|
44
|
-
FlowChat.logger.debug { "
|
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
|
data/lib/flow_chat/version.rb
CHANGED
@@ -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
|
-
|
60
|
-
|
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
|
-
|
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
|
-
|
18
|
-
|
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
|
-
|
13
|
+
def log_prefix
|
14
|
+
"Whatsapp::Executor"
|
15
|
+
end
|
44
16
|
|
45
|
-
def
|
46
|
-
FlowChat.logger.debug { "
|
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.
|
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-
|
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
|