chat_sdk 1.2.0 → 1.3.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/lib/chat_sdk/adapter/base.rb +7 -1
- data/lib/chat_sdk/ai/stream_handler.rb +3 -3
- data/lib/chat_sdk/cards/actions_context.rb +24 -4
- data/lib/chat_sdk/cards/builder.rb +13 -4
- data/lib/chat_sdk/cards/node.rb +2 -0
- data/lib/chat_sdk/cards/renderer.rb +2 -0
- data/lib/chat_sdk/chat.rb +65 -4
- data/lib/chat_sdk/config.rb +34 -2
- data/lib/chat_sdk/dispatcher.rb +53 -28
- data/lib/chat_sdk/error_reporter.rb +36 -0
- data/lib/chat_sdk/errors.rb +1 -0
- data/lib/chat_sdk/event_serializer.rb +33 -3
- data/lib/chat_sdk/events/app_home_opened.rb +16 -0
- data/lib/chat_sdk/events/assistant_context.rb +25 -0
- data/lib/chat_sdk/events/assistant_context_changed.rb +18 -0
- data/lib/chat_sdk/events/assistant_thread_started.rb +18 -0
- data/lib/chat_sdk/events/member_joined_channel.rb +16 -0
- data/lib/chat_sdk/modals/builder.rb +39 -4
- data/lib/chat_sdk/streaming/chunk.rb +35 -0
- data/lib/chat_sdk/streaming/markdown_text_chunk.rb +15 -0
- data/lib/chat_sdk/streaming/plan_update_chunk.rb +15 -0
- data/lib/chat_sdk/streaming/stream.rb +43 -7
- data/lib/chat_sdk/streaming/task_update_chunk.rb +26 -0
- data/lib/chat_sdk/streaming/writer.rb +16 -0
- data/lib/chat_sdk/streaming_plan.rb +32 -0
- data/lib/chat_sdk/thread.rb +76 -11
- data/lib/chat_sdk/version.rb +1 -1
- data/lib/chat_sdk/webhook/endpoint.rb +43 -3
- data/lib/chat_sdk.rb +2 -2
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 729c1baa552d9bad38924b1f026c75c769bb26b6dc3b4784e5e900ca66c93d21
|
|
4
|
+
data.tar.gz: 677e7b87d6177385e6a0b3fbe52da890a3238c31dd36c754c62d09e224ba385c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 50cb4ab19ee8b817e4395030a9ca15ddd8980d785d0bff8f0dabb13462b636259fd6ff42afca5c2cfece542e74dd50224188dd2090e3bbf3378e558045cb4bf3
|
|
7
|
+
data.tar.gz: 13183055e0ce37f542350374d6210f8ec6dd2b63c34a0acef2629515cf0009bbcc7e93d03c64b39b96d16982dbce8f0904c1a7a51f1661f267f9880bf59d56bc
|
|
@@ -22,7 +22,7 @@ module ChatSDK
|
|
|
22
22
|
nil
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def webhook_response(_events)
|
|
25
|
+
def webhook_response(_events, _request = nil)
|
|
26
26
|
nil
|
|
27
27
|
end
|
|
28
28
|
|
|
@@ -35,6 +35,12 @@ module ChatSDK
|
|
|
35
35
|
raise NotImplementedError
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# Stream using a platform-native API, or return nil before consuming
|
|
39
|
+
# chunks to delegate to the core post+edit fallback.
|
|
40
|
+
def stream(channel_id:, thread_id:, stream:, options: {})
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
38
44
|
def reply_message(channel_id:, message_id:, message:, thread_id: nil)
|
|
39
45
|
require_capability!(:replies)
|
|
40
46
|
raise NotImplementedError
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
module ChatSDK
|
|
4
4
|
module AI
|
|
5
5
|
class StreamHandler
|
|
6
|
-
def self.stream_to_thread(thread, enumerable, placeholder: "Thinking...")
|
|
7
|
-
thread.post_stream(placeholder: placeholder) do |stream|
|
|
8
|
-
enumerable.each { |chunk| stream << chunk
|
|
6
|
+
def self.stream_to_thread(thread, enumerable, placeholder: "Thinking...", **plan_options)
|
|
7
|
+
thread.post_stream(placeholder: placeholder, **plan_options) do |stream|
|
|
8
|
+
enumerable.each { |chunk| stream << chunk }
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
11
|
end
|
|
@@ -9,30 +9,50 @@ module ChatSDK
|
|
|
9
9
|
@nodes = []
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def button(text, id:, style: nil, value: nil, tooltip: nil, visible_to: nil)
|
|
12
|
+
def button(text, id:, style: nil, value: nil, tooltip: nil, visible_to: nil, disabled: nil)
|
|
13
13
|
attrs = {text: text, id: id}
|
|
14
14
|
attrs[:style] = style if style
|
|
15
15
|
attrs[:value] = value if value
|
|
16
16
|
attrs[:tooltip] = tooltip if tooltip
|
|
17
17
|
attrs[:visible_to] = visible_to if visible_to
|
|
18
|
+
attrs[:disabled] = disabled unless disabled.nil?
|
|
18
19
|
@nodes << Node.new(:button, attributes: attrs)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
def link_button(text, url:, id: nil, tooltip: nil, visible_to: nil)
|
|
22
|
+
def link_button(text, url:, id: nil, style: nil, tooltip: nil, visible_to: nil)
|
|
22
23
|
attrs = {text: text, url: url}
|
|
23
24
|
attrs[:id] = id if id
|
|
25
|
+
attrs[:style] = style if style
|
|
24
26
|
attrs[:tooltip] = tooltip if tooltip
|
|
25
27
|
attrs[:visible_to] = visible_to if visible_to
|
|
26
28
|
@nodes << Node.new(:link_button, attributes: attrs)
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
def select(id:, placeholder: nil, &block)
|
|
31
|
+
def select(id:, placeholder: nil, label: nil, initial_option: nil, optional: nil, dispatch_action: nil, &block)
|
|
30
32
|
ctx = SelectContext.new
|
|
31
|
-
ctx.instance_eval(&block)
|
|
33
|
+
ctx.instance_eval(&block) if block
|
|
34
|
+
raise ArgumentError, "Select requires at least one option" if ctx.nodes.empty?
|
|
35
|
+
|
|
32
36
|
attrs = {id: id}
|
|
33
37
|
attrs[:placeholder] = placeholder if placeholder
|
|
38
|
+
attrs[:label] = label unless label.nil?
|
|
39
|
+
attrs[:initial_option] = initial_option unless initial_option.nil?
|
|
40
|
+
attrs[:optional] = optional unless optional.nil?
|
|
41
|
+
attrs[:dispatch_action] = dispatch_action unless dispatch_action.nil?
|
|
34
42
|
@nodes << Node.new(:select, attributes: attrs, children: ctx.nodes)
|
|
35
43
|
end
|
|
44
|
+
|
|
45
|
+
def radio_select(id:, label:, initial_option: nil, optional: nil, dispatch_action: nil, &block)
|
|
46
|
+
ctx = SelectContext.new
|
|
47
|
+
ctx.instance_eval(&block) if block
|
|
48
|
+
raise ArgumentError, "RadioSelect requires at least one option" if ctx.nodes.empty?
|
|
49
|
+
|
|
50
|
+
attrs = {id: id, label: label}
|
|
51
|
+
attrs[:initial_option] = initial_option unless initial_option.nil?
|
|
52
|
+
attrs[:optional] = optional unless optional.nil?
|
|
53
|
+
attrs[:dispatch_action] = dispatch_action unless dispatch_action.nil?
|
|
54
|
+
@nodes << Node.new(:radio_select, attributes: attrs, children: ctx.nodes)
|
|
55
|
+
end
|
|
36
56
|
end
|
|
37
57
|
end
|
|
38
58
|
end
|
|
@@ -5,10 +5,11 @@ module ChatSDK
|
|
|
5
5
|
class Builder
|
|
6
6
|
RESERVED_TEXT_INPUT_IDS = %w[actionId value msteams].freeze
|
|
7
7
|
|
|
8
|
-
def initialize(title: nil, subtitle: nil, width: nil, &block)
|
|
8
|
+
def initialize(title: nil, subtitle: nil, width: nil, image_url: nil, &block)
|
|
9
9
|
@title = title
|
|
10
10
|
@subtitle = subtitle
|
|
11
11
|
@width = width
|
|
12
|
+
@image_url = image_url
|
|
12
13
|
@children = []
|
|
13
14
|
instance_eval(&block) if block
|
|
14
15
|
end
|
|
@@ -18,11 +19,18 @@ module ChatSDK
|
|
|
18
19
|
attrs[:title] = @title if @title
|
|
19
20
|
attrs[:subtitle] = @subtitle if @subtitle
|
|
20
21
|
attrs[:width] = @width if @width
|
|
22
|
+
attrs[:image_url] = @image_url if @image_url
|
|
21
23
|
Node.new(:card, attributes: attrs, children: @children)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
def text(content)
|
|
25
|
-
|
|
26
|
+
def text(content, style: nil)
|
|
27
|
+
attrs = {content: content}
|
|
28
|
+
attrs[:style] = style unless style.nil?
|
|
29
|
+
@children << Node.new(:text, attributes: attrs)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def link(url:, label:)
|
|
33
|
+
@children << Node.new(:link, attributes: {url: url, label: label})
|
|
26
34
|
end
|
|
27
35
|
|
|
28
36
|
def divider
|
|
@@ -33,7 +41,7 @@ module ChatSDK
|
|
|
33
41
|
@children << Node.new(:image, attributes: {url: url, alt: alt})
|
|
34
42
|
end
|
|
35
43
|
|
|
36
|
-
def text_input(id:, label:, placeholder: nil, multiline: false, optional: false, value: nil)
|
|
44
|
+
def text_input(id:, label:, placeholder: nil, multiline: false, optional: false, value: nil, max_length: nil)
|
|
37
45
|
if RESERVED_TEXT_INPUT_IDS.include?(id.to_s)
|
|
38
46
|
raise ArgumentError, "Text input id #{id.inspect} is reserved"
|
|
39
47
|
end
|
|
@@ -41,6 +49,7 @@ module ChatSDK
|
|
|
41
49
|
attrs = {id: id, label: label, multiline: multiline, optional: optional}
|
|
42
50
|
attrs[:placeholder] = placeholder if placeholder
|
|
43
51
|
attrs[:value] = value unless value.nil?
|
|
52
|
+
attrs[:max_length] = max_length unless max_length.nil?
|
|
44
53
|
@children << Node.new(:text_input, attributes: attrs)
|
|
45
54
|
end
|
|
46
55
|
|
data/lib/chat_sdk/cards/node.rb
CHANGED
|
@@ -9,6 +9,7 @@ module ChatSDK
|
|
|
9
9
|
when :text then node.attributes[:content]
|
|
10
10
|
when :divider then "---"
|
|
11
11
|
when :image then "![#{node.attributes[:alt]}](#{node.attributes[:url]})"
|
|
12
|
+
when :link then "[#{node.attributes[:label]}](#{node.attributes[:url]})"
|
|
12
13
|
when :fields then render_fields(node)
|
|
13
14
|
when :field then "**#{node.attributes[:label]}**: #{node.attributes[:value]}"
|
|
14
15
|
when :section then render_section(node)
|
|
@@ -17,6 +18,7 @@ module ChatSDK
|
|
|
17
18
|
when :link_button then "[#{node.attributes[:text]}](#{node.attributes[:url]})"
|
|
18
19
|
when :text_input then node.attributes[:label]
|
|
19
20
|
when :select then "_#{node.attributes[:placeholder] || "Select"}_"
|
|
21
|
+
when :radio_select then "_#{node.attributes[:label] || "Select"}_"
|
|
20
22
|
when :table then render_table(node)
|
|
21
23
|
when :chart then render_chart(node)
|
|
22
24
|
else ""
|
data/lib/chat_sdk/chat.rb
CHANGED
|
@@ -3,18 +3,27 @@
|
|
|
3
3
|
module ChatSDK
|
|
4
4
|
class Chat
|
|
5
5
|
attr_reader :config, :state, :history
|
|
6
|
+
# @api private
|
|
7
|
+
attr_reader :error_reporter
|
|
6
8
|
|
|
7
9
|
def initialize(user_name:, adapters:, state:, **options)
|
|
8
10
|
@config = Config.new(user_name: user_name, adapters: adapters, state: state, **options)
|
|
9
11
|
@state = state
|
|
12
|
+
ChatSDK::Log.logger = @config.logger if @config.logger
|
|
13
|
+
ChatSDK::Log.level = @config.log_level if @config.log_level
|
|
10
14
|
@adapters = adapters
|
|
11
15
|
@adapters.each_value { |adapter| adapter.set_state(state) if adapter.respond_to?(:set_state) }
|
|
12
16
|
@registry = EventRegistry.new
|
|
13
17
|
@history = History.new(self)
|
|
14
18
|
@webhooks = {}
|
|
15
|
-
@
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
@error_reporter = ErrorReporter.new(@config.on_error)
|
|
20
|
+
@dispatcher = Dispatcher.new(
|
|
21
|
+
chat: self,
|
|
22
|
+
config: @config,
|
|
23
|
+
state: @state,
|
|
24
|
+
registry: @registry,
|
|
25
|
+
error_reporter: @error_reporter
|
|
26
|
+
)
|
|
18
27
|
end
|
|
19
28
|
|
|
20
29
|
# Event registration
|
|
@@ -67,6 +76,22 @@ module ChatSDK
|
|
|
67
76
|
@registry.register(:uninstalled, &block)
|
|
68
77
|
end
|
|
69
78
|
|
|
79
|
+
def on_assistant_thread_started(&block)
|
|
80
|
+
@registry.register(:assistant_thread_started, &block)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def on_assistant_context_changed(&block)
|
|
84
|
+
@registry.register(:assistant_context_changed, &block)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def on_app_home_opened(&block)
|
|
88
|
+
@registry.register(:app_home_opened, &block)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def on_member_joined_channel(&block)
|
|
92
|
+
@registry.register(:member_joined_channel, &block)
|
|
93
|
+
end
|
|
94
|
+
|
|
70
95
|
# Adapter access
|
|
71
96
|
def adapter(name)
|
|
72
97
|
@adapters.fetch(name) { raise ChatSDK::ConfigurationError, "Unknown adapter: #{name}" }
|
|
@@ -97,7 +122,28 @@ module ChatSDK
|
|
|
97
122
|
# Event dispatch (called by webhook endpoints)
|
|
98
123
|
def dispatch(event, adapter_name:)
|
|
99
124
|
adp = adapter(adapter_name)
|
|
100
|
-
@
|
|
125
|
+
executor = @config.handler_executor
|
|
126
|
+
return @dispatcher.dispatch(event, adapter: adp, adapter_name: adapter_name) if executor == :inline
|
|
127
|
+
|
|
128
|
+
if executor == :thread
|
|
129
|
+
::Thread.new { run_background_dispatch(event, adp, adapter_name) }
|
|
130
|
+
else
|
|
131
|
+
payload = {"adapter_name" => adapter_name.to_s, "event" => EventSerializer.dump(event)}
|
|
132
|
+
begin
|
|
133
|
+
executor.call(payload)
|
|
134
|
+
rescue => e
|
|
135
|
+
raise HandlerExecutorError, "handler_executor failed for #{event.type}: #{e.class}: #{e.message}"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Runs a payload produced for a callable handler_executor, such as from a background job.
|
|
142
|
+
def dispatch_serialized(payload)
|
|
143
|
+
payload = payload.transform_keys(&:to_s)
|
|
144
|
+
adapter_name = payload.fetch("adapter_name").to_sym
|
|
145
|
+
event = EventSerializer.load(payload.fetch("event"))
|
|
146
|
+
@dispatcher.dispatch(event, adapter: adapter(adapter_name), adapter_name: adapter_name)
|
|
101
147
|
end
|
|
102
148
|
|
|
103
149
|
# Deprecated compatibility alias for Vercel Chat SDK's former API.
|
|
@@ -106,6 +152,21 @@ module ChatSDK
|
|
|
106
152
|
end
|
|
107
153
|
|
|
108
154
|
private
|
|
155
|
+
|
|
156
|
+
def run_background_dispatch(event, adp, adapter_name)
|
|
157
|
+
@dispatcher.dispatch(event, adapter: adp, adapter_name: adapter_name)
|
|
158
|
+
rescue => e
|
|
159
|
+
# Nothing above this background thread can rescue errors raised during dispatch.
|
|
160
|
+
@error_reporter.report(
|
|
161
|
+
e,
|
|
162
|
+
"Background dispatch error (#{event.type})",
|
|
163
|
+
source: :dispatch,
|
|
164
|
+
adapter: adapter_name,
|
|
165
|
+
event_type: event.type,
|
|
166
|
+
thread_id: event.respond_to?(:thread_id) ? event.thread_id : nil,
|
|
167
|
+
channel_id: event.respond_to?(:channel_id) ? event.channel_id : nil
|
|
168
|
+
)
|
|
169
|
+
end
|
|
109
170
|
end
|
|
110
171
|
|
|
111
172
|
class WebhookAccessor
|
data/lib/chat_sdk/config.rb
CHANGED
|
@@ -15,7 +15,7 @@ module ChatSDK
|
|
|
15
15
|
|
|
16
16
|
attr_reader :user_name, :adapters, :state, :on_lock_conflict,
|
|
17
17
|
:dedupe_ttl, :streaming_update_interval, :handler_executor, :log_level,
|
|
18
|
-
:history_user, :concurrency
|
|
18
|
+
:history_user, :concurrency, :logger, :on_error
|
|
19
19
|
|
|
20
20
|
def initialize(user_name:, adapters:, state:, **options)
|
|
21
21
|
raise ConfigurationError, "user_name is required" if user_name.nil? || user_name.empty?
|
|
@@ -26,6 +26,8 @@ module ChatSDK
|
|
|
26
26
|
@adapters = adapters
|
|
27
27
|
@state = state
|
|
28
28
|
merged = DEFAULTS.merge(options)
|
|
29
|
+
@logger = merged[:logger]
|
|
30
|
+
@on_error = merged[:on_error]
|
|
29
31
|
@on_lock_conflict = merged[:on_lock_conflict]
|
|
30
32
|
@concurrency = normalize_concurrency(merged[:concurrency], lock_scope: merged[:lock_scope])
|
|
31
33
|
@dedupe_ttl = merged[:dedupe_ttl]
|
|
@@ -38,13 +40,43 @@ module ChatSDK
|
|
|
38
40
|
retention: user_history.fetch(:retention, user_history.fetch("retention", 30 * 24 * 3600)),
|
|
39
41
|
max_per_user: user_history.fetch(:max_per_user, user_history.fetch("max_per_user", 200))
|
|
40
42
|
}
|
|
41
|
-
@log_level =
|
|
43
|
+
@log_level = if !@logger.nil? && !options.key?(:log_level)
|
|
44
|
+
nil
|
|
45
|
+
else
|
|
46
|
+
merged[:log_level]
|
|
47
|
+
end
|
|
42
48
|
|
|
49
|
+
validate_handler_executor!
|
|
50
|
+
validate_logger!
|
|
51
|
+
validate_on_error!
|
|
43
52
|
validate_lock_conflict!
|
|
44
53
|
end
|
|
45
54
|
|
|
46
55
|
private
|
|
47
56
|
|
|
57
|
+
def validate_handler_executor!
|
|
58
|
+
return if %i[inline thread].include?(@handler_executor) || @handler_executor.respond_to?(:call)
|
|
59
|
+
|
|
60
|
+
raise ConfigurationError, "handler_executor must be :inline, :thread, or a callable"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def validate_logger!
|
|
64
|
+
return if @logger.nil?
|
|
65
|
+
unless %i[debug info warn error].all? { |level| @logger.respond_to?(level) }
|
|
66
|
+
raise ConfigurationError, "logger must respond to debug, info, warn, and error"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
if !@log_level.nil? && !@logger.respond_to?(:level=)
|
|
70
|
+
raise ConfigurationError, "logger must respond to level= when log_level is set"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def validate_on_error!
|
|
75
|
+
return if @on_error.nil? || @on_error.respond_to?(:call)
|
|
76
|
+
|
|
77
|
+
raise ConfigurationError, "on_error must be a callable"
|
|
78
|
+
end
|
|
79
|
+
|
|
48
80
|
def validate_lock_conflict!
|
|
49
81
|
return if %i[drop force].include?(@on_lock_conflict) || @on_lock_conflict.respond_to?(:call)
|
|
50
82
|
raise ConfigurationError, "on_lock_conflict must be :drop, :force, or a callable"
|
data/lib/chat_sdk/dispatcher.rb
CHANGED
|
@@ -5,17 +5,25 @@ require "time"
|
|
|
5
5
|
|
|
6
6
|
module ChatSDK
|
|
7
7
|
class Dispatcher
|
|
8
|
+
LIFECYCLE_EVENT_TYPES = %i[
|
|
9
|
+
assistant_thread_started
|
|
10
|
+
assistant_context_changed
|
|
11
|
+
app_home_opened
|
|
12
|
+
member_joined_channel
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
8
15
|
LockHeartbeat = Struct.new(:thread, :mutex, :condition, :stop, :ownership_lost) do
|
|
9
16
|
def ownership_lost?
|
|
10
17
|
mutex.synchronize { ownership_lost }
|
|
11
18
|
end
|
|
12
19
|
end
|
|
13
20
|
|
|
14
|
-
def initialize(chat:, config:, state:, registry:)
|
|
21
|
+
def initialize(chat:, config:, state:, registry:, error_reporter:)
|
|
15
22
|
@chat = chat
|
|
16
23
|
@config = config
|
|
17
24
|
@state = state
|
|
18
25
|
@registry = registry
|
|
26
|
+
@error_reporter = error_reporter
|
|
19
27
|
@slot_mutex = Mutex.new
|
|
20
28
|
@slot_condition = ConditionVariable.new
|
|
21
29
|
@active_slots = Hash.new(0)
|
|
@@ -24,7 +32,13 @@ module ChatSDK
|
|
|
24
32
|
def dispatch(event, adapter:, adapter_name:)
|
|
25
33
|
ChatSDK::Instrumentation.instrument("dispatch.chat_sdk", adapter: adapter_name, event_type: event.type) do
|
|
26
34
|
if %i[message_updated message_deleted].include?(event.type)
|
|
27
|
-
return execute_event(event, build_thread(event, adapter))
|
|
35
|
+
return execute_event(event, build_thread(event, adapter), adapter_name: adapter_name)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if LIFECYCLE_EVENT_TYPES.include?(event.type)
|
|
39
|
+
return unless dedupe(event, adapter_name, adapter.concurrency_scope(event))
|
|
40
|
+
|
|
41
|
+
return execute_event(event, build_thread(event, adapter), adapter_name: adapter_name)
|
|
28
42
|
end
|
|
29
43
|
|
|
30
44
|
concurrency_scope = adapter.concurrency_scope(event)
|
|
@@ -35,32 +49,32 @@ module ChatSDK
|
|
|
35
49
|
strategy = @config.concurrency[:strategy]
|
|
36
50
|
|
|
37
51
|
if strategy == :concurrent
|
|
38
|
-
return with_concurrent_slot(thread_key) { execute_event(event, thread) }
|
|
52
|
+
return with_concurrent_slot(thread_key) { execute_event(event, thread, adapter_name: adapter_name) }
|
|
39
53
|
end
|
|
40
54
|
|
|
41
55
|
owner = "#{Process.pid}:#{::Thread.current.object_id}:#{SecureRandom.uuid}"
|
|
42
56
|
if %i[queue burst debounce].include?(strategy)
|
|
43
|
-
dispatch_queued(event, adapter, thread_key, owner, strategy)
|
|
57
|
+
dispatch_queued(event, adapter, adapter_name, thread_key, owner, strategy)
|
|
44
58
|
else
|
|
45
|
-
dispatch_locked(event, thread, thread_key, owner, strategy)
|
|
59
|
+
dispatch_locked(event, thread, adapter_name, thread_key, owner, strategy)
|
|
46
60
|
end
|
|
47
61
|
end
|
|
48
62
|
end
|
|
49
63
|
|
|
50
64
|
private
|
|
51
65
|
|
|
52
|
-
def dispatch_locked(event, thread, thread_key, owner, strategy)
|
|
66
|
+
def dispatch_locked(event, thread, adapter_name, thread_key, owner, strategy)
|
|
53
67
|
acquired = acquire_lock(thread_key, event, owner, force: strategy == :force)
|
|
54
68
|
return unless acquired
|
|
55
69
|
|
|
56
70
|
heartbeat = start_lock_heartbeat(thread_key, owner)
|
|
57
|
-
execute_event(event, thread)
|
|
71
|
+
execute_event(event, thread, adapter_name: adapter_name)
|
|
58
72
|
ensure
|
|
59
73
|
stop_lock_heartbeat(heartbeat)
|
|
60
74
|
release_lock(thread_key, owner) if acquired
|
|
61
75
|
end
|
|
62
76
|
|
|
63
|
-
def dispatch_queued(event, adapter, thread_key, owner, strategy)
|
|
77
|
+
def dispatch_queued(event, adapter, adapter_name, thread_key, owner, strategy)
|
|
64
78
|
queue_key = "chat_sdk:queue:#{thread_key}"
|
|
65
79
|
acquired = acquire_lock(thread_key, event, owner, apply_conflict_policy: false)
|
|
66
80
|
pre_enqueued = false
|
|
@@ -74,23 +88,23 @@ module ChatSDK
|
|
|
74
88
|
|
|
75
89
|
heartbeat = start_lock_heartbeat(thread_key, owner)
|
|
76
90
|
if strategy == :queue
|
|
77
|
-
execute_event(event, build_thread(event, adapter)) unless pre_enqueued
|
|
91
|
+
execute_event(event, build_thread(event, adapter), adapter_name: adapter_name) unless pre_enqueued
|
|
78
92
|
else
|
|
79
93
|
enqueue(queue_key, event) unless pre_enqueued
|
|
80
94
|
sleep(@config.concurrency[:debounce].to_f) if strategy == :burst
|
|
81
95
|
end
|
|
82
96
|
|
|
83
97
|
if strategy == :debounce
|
|
84
|
-
debounce_loop(queue_key, adapter, heartbeat)
|
|
98
|
+
debounce_loop(queue_key, adapter, adapter_name, heartbeat)
|
|
85
99
|
else
|
|
86
|
-
drain_loop(queue_key, adapter, strategy, heartbeat)
|
|
100
|
+
drain_loop(queue_key, adapter, adapter_name, strategy, heartbeat)
|
|
87
101
|
end
|
|
88
102
|
ensure
|
|
89
103
|
stop_lock_heartbeat(heartbeat)
|
|
90
104
|
release_lock(thread_key, owner) if acquired
|
|
91
105
|
end
|
|
92
106
|
|
|
93
|
-
def drain_loop(queue_key, adapter, strategy, heartbeat)
|
|
107
|
+
def drain_loop(queue_key, adapter, adapter_name, strategy, heartbeat)
|
|
94
108
|
loop do
|
|
95
109
|
break if heartbeat&.ownership_lost?
|
|
96
110
|
|
|
@@ -105,12 +119,12 @@ module ChatSDK
|
|
|
105
119
|
events[0...-1].filter_map { |queued| queued.message if queued.respond_to?(:message) }
|
|
106
120
|
end
|
|
107
121
|
context = {skipped: skipped, total_since_last_handler: skipped.length + 1}
|
|
108
|
-
execute_event(current, build_thread(current, adapter), context: context)
|
|
122
|
+
execute_event(current, build_thread(current, adapter), adapter_name: adapter_name, context: context)
|
|
109
123
|
::Thread.pass
|
|
110
124
|
end
|
|
111
125
|
end
|
|
112
126
|
|
|
113
|
-
def debounce_loop(queue_key, adapter, heartbeat)
|
|
127
|
+
def debounce_loop(queue_key, adapter, adapter_name, heartbeat)
|
|
114
128
|
skipped = []
|
|
115
129
|
loop do
|
|
116
130
|
sleep(@config.concurrency[:debounce].to_f)
|
|
@@ -132,7 +146,7 @@ module ChatSDK
|
|
|
132
146
|
message.thread_id == latest.thread_id
|
|
133
147
|
end
|
|
134
148
|
context = {skipped: relevant, total_since_last_handler: relevant.length + 1}
|
|
135
|
-
execute_event(latest, build_thread(latest, adapter), context: context)
|
|
149
|
+
execute_event(latest, build_thread(latest, adapter), adapter_name: adapter_name, context: context)
|
|
136
150
|
skipped.clear
|
|
137
151
|
end
|
|
138
152
|
end
|
|
@@ -262,29 +276,29 @@ module ChatSDK
|
|
|
262
276
|
ChatSDK::Thread.new(id: thread_id, channel_id: channel_id, adapter: adapter, chat: @chat, current_message: current_message)
|
|
263
277
|
end
|
|
264
278
|
|
|
265
|
-
def execute_event(event, thread, context: nil)
|
|
279
|
+
def execute_event(event, thread, adapter_name:, context: nil)
|
|
266
280
|
if %i[mention subscribed_message direct_message].include?(event.type)
|
|
267
|
-
route_message_event(event, thread, context)
|
|
281
|
+
route_message_event(event, thread, adapter_name, context)
|
|
268
282
|
return
|
|
269
283
|
end
|
|
270
284
|
|
|
271
285
|
@registry.handlers_for(event).each do |handler|
|
|
272
|
-
execute_handler(handler, event, thread, context: context)
|
|
286
|
+
execute_handler(handler, event, thread, adapter_name: adapter_name, context: context)
|
|
273
287
|
end
|
|
274
288
|
end
|
|
275
289
|
|
|
276
|
-
def route_message_event(event, thread, context)
|
|
290
|
+
def route_message_event(event, thread, adapter_name, context)
|
|
277
291
|
message = event.message
|
|
278
292
|
dm_handlers = @registry.handlers_for_type(:direct_message, event: event)
|
|
279
293
|
if event.type == :direct_message && dm_handlers.any?
|
|
280
|
-
run_handlers(dm_handlers, event, thread, context)
|
|
294
|
+
run_handlers(dm_handlers, event, thread, adapter_name, context)
|
|
281
295
|
return
|
|
282
296
|
end
|
|
283
297
|
|
|
284
298
|
message.is_mention = true if event.type == :direct_message
|
|
285
299
|
|
|
286
300
|
if thread&.subscribed?
|
|
287
|
-
run_handlers(@registry.handlers_for_type(:subscribed_message, event: event), event, thread, context)
|
|
301
|
+
run_handlers(@registry.handlers_for_type(:subscribed_message, event: event), event, thread, adapter_name, context)
|
|
288
302
|
return
|
|
289
303
|
end
|
|
290
304
|
|
|
@@ -297,17 +311,19 @@ module ChatSDK
|
|
|
297
311
|
end
|
|
298
312
|
|
|
299
313
|
if message.is_mention
|
|
300
|
-
run_handlers(@registry.handlers_for_type(:mention, event: event), event, thread, context)
|
|
314
|
+
run_handlers(@registry.handlers_for_type(:mention, event: event), event, thread, adapter_name, context)
|
|
301
315
|
return
|
|
302
316
|
end
|
|
303
317
|
|
|
304
318
|
@registry.handlers_for_type(:new_message, event: event).each do |handler|
|
|
305
|
-
execute_handler(handler, event, thread, context: context)
|
|
319
|
+
execute_handler(handler, event, thread, adapter_name: adapter_name, context: context)
|
|
306
320
|
end
|
|
307
321
|
end
|
|
308
322
|
|
|
309
|
-
def run_handlers(handlers, event, thread, context)
|
|
310
|
-
handlers.each
|
|
323
|
+
def run_handlers(handlers, event, thread, adapter_name, context)
|
|
324
|
+
handlers.each do |handler|
|
|
325
|
+
execute_handler(handler, event, thread, adapter_name: adapter_name, context: context)
|
|
326
|
+
end
|
|
311
327
|
end
|
|
312
328
|
|
|
313
329
|
def detect_mention(text)
|
|
@@ -315,7 +331,7 @@ module ChatSDK
|
|
|
315
331
|
/(?<![[:word:]])@#{Regexp.escape(@config.user_name)}\b/i.match?(masked_text)
|
|
316
332
|
end
|
|
317
333
|
|
|
318
|
-
def execute_handler(handler, event, thread, context: nil)
|
|
334
|
+
def execute_handler(handler, event, thread, adapter_name:, context: nil)
|
|
319
335
|
ChatSDK::Instrumentation.instrument("handler.chat_sdk", handler_type: event.type) do
|
|
320
336
|
ChatSDK::AI::ConversationScope.with(thread) do
|
|
321
337
|
case event.type
|
|
@@ -326,12 +342,21 @@ module ChatSDK
|
|
|
326
342
|
when :reaction, :action, :slash_command, :message_deleted, :installed, :uninstalled
|
|
327
343
|
add_thread_to_event(event, thread)
|
|
328
344
|
handler.block.call(event)
|
|
345
|
+
when *LIFECYCLE_EVENT_TYPES
|
|
346
|
+
handler.block.call(event)
|
|
329
347
|
end
|
|
330
348
|
end
|
|
331
349
|
end
|
|
332
350
|
rescue => e
|
|
333
|
-
|
|
334
|
-
|
|
351
|
+
@error_reporter.report(
|
|
352
|
+
e,
|
|
353
|
+
"Handler error (#{event.type})",
|
|
354
|
+
source: :handler,
|
|
355
|
+
adapter: adapter_name,
|
|
356
|
+
event_type: event.type,
|
|
357
|
+
thread_id: event.respond_to?(:thread_id) ? event.thread_id : nil,
|
|
358
|
+
channel_id: event.respond_to?(:channel_id) ? event.channel_id : nil
|
|
359
|
+
)
|
|
335
360
|
end
|
|
336
361
|
|
|
337
362
|
def add_thread_to_event(event, thread)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
class ErrorReporter
|
|
5
|
+
def initialize(callback = nil)
|
|
6
|
+
@callback = callback
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def report(error, message, **context)
|
|
10
|
+
context = context.compact
|
|
11
|
+
ChatSDK::Log.error(format_entry(error, message, context))
|
|
12
|
+
reporter = @callback || self.class.default_callback
|
|
13
|
+
reporter&.call(error, context)
|
|
14
|
+
rescue => e
|
|
15
|
+
ChatSDK::Log.error("on_error callback failed: #{e.class}: #{e.message}")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.default_callback
|
|
19
|
+
return unless defined?(::Rails) && ::Rails.respond_to?(:error)
|
|
20
|
+
|
|
21
|
+
rails_reporter = ::Rails.error
|
|
22
|
+
return unless rails_reporter.respond_to?(:report)
|
|
23
|
+
|
|
24
|
+
->(error, context) { rails_reporter.report(error, handled: true, severity: :error, context: context) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def format_entry(error, message, context)
|
|
30
|
+
details = context.empty? ? "" : " (#{context.map { |key, value| "#{key}=#{value}" }.join(" ")})"
|
|
31
|
+
entry = "#{message}#{details}: #{error.class}: #{error.message}"
|
|
32
|
+
backtrace = error.backtrace
|
|
33
|
+
backtrace ? "#{entry}\n#{backtrace.join("\n")}" : entry
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/chat_sdk/errors.rb
CHANGED