chat_sdk 1.0.1 → 1.2.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 +33 -5
- data/lib/chat_sdk/adapter/capabilities.rb +1 -1
- data/lib/chat_sdk/adapter/invocation.rb +18 -0
- data/lib/chat_sdk/ai/conversation_scope.rb +44 -0
- data/lib/chat_sdk/ai/converter.rb +16 -5
- data/lib/chat_sdk/ai/tool_builder.rb +18 -2
- data/lib/chat_sdk/ai/tool_executor.rb +64 -10
- data/lib/chat_sdk/ai.rb +2 -2
- data/lib/chat_sdk/author.rb +13 -2
- data/lib/chat_sdk/cards/actions_context.rb +9 -3
- data/lib/chat_sdk/cards/builder.rb +38 -1
- data/lib/chat_sdk/cards/node.rb +36 -0
- data/lib/chat_sdk/cards/renderer.rb +20 -0
- data/lib/chat_sdk/channel.rb +8 -0
- data/lib/chat_sdk/chat.rb +35 -2
- data/lib/chat_sdk/config.rb +48 -1
- data/lib/chat_sdk/dispatcher.rb +280 -33
- data/lib/chat_sdk/event_registry.rb +7 -2
- data/lib/chat_sdk/event_serializer.rb +192 -0
- data/lib/chat_sdk/events/action.rb +3 -2
- data/lib/chat_sdk/events/installed.rb +19 -0
- data/lib/chat_sdk/events/message_deleted.rb +19 -0
- data/lib/chat_sdk/events/message_updated.rb +17 -0
- data/lib/chat_sdk/events/uninstalled.rb +19 -0
- data/lib/chat_sdk/format/mentions.rb +61 -0
- data/lib/chat_sdk/history.rb +151 -0
- data/lib/chat_sdk/message.rb +8 -2
- data/lib/chat_sdk/modals/builder.rb +16 -0
- data/lib/chat_sdk/postable_message.rb +3 -2
- data/lib/chat_sdk/state/base.rb +16 -0
- data/lib/chat_sdk/state/memory.rb +31 -0
- data/lib/chat_sdk/streaming/stream.rb +6 -2
- data/lib/chat_sdk/testing/fake_adapter.rb +32 -8
- data/lib/chat_sdk/thread.rb +35 -6
- data/lib/chat_sdk/version.rb +1 -1
- data/lib/chat_sdk/webhook/endpoint.rb +3 -0
- data/lib/chat_sdk.rb +2 -2
- metadata +10 -1
data/lib/chat_sdk/chat.rb
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module ChatSDK
|
|
4
4
|
class Chat
|
|
5
|
-
attr_reader :config, :state
|
|
5
|
+
attr_reader :config, :state, :history
|
|
6
6
|
|
|
7
7
|
def initialize(user_name:, adapters:, state:, **options)
|
|
8
8
|
@config = Config.new(user_name: user_name, adapters: adapters, state: state, **options)
|
|
9
9
|
@state = state
|
|
10
10
|
@adapters = adapters
|
|
11
|
+
@adapters.each_value { |adapter| adapter.set_state(state) if adapter.respond_to?(:set_state) }
|
|
11
12
|
@registry = EventRegistry.new
|
|
13
|
+
@history = History.new(self)
|
|
12
14
|
@webhooks = {}
|
|
13
15
|
@dispatcher = Dispatcher.new(chat: self, config: @config, state: @state, registry: @registry)
|
|
14
16
|
|
|
@@ -25,13 +27,26 @@ module ChatSDK
|
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def on_new_message(pattern = nil, &block)
|
|
28
|
-
|
|
30
|
+
unless pattern.nil? || pattern.is_a?(Regexp)
|
|
31
|
+
raise ChatSDK::ConfigurationError, "on_new_message pattern must be a Regexp or nil"
|
|
32
|
+
end
|
|
33
|
+
raise ChatSDK::ConfigurationError, "on_new_message requires a block" unless block
|
|
34
|
+
|
|
35
|
+
@registry.register(:new_message, matcher: pattern, &block)
|
|
29
36
|
end
|
|
30
37
|
|
|
31
38
|
def on_direct_message(&block)
|
|
32
39
|
@registry.register(:direct_message, &block)
|
|
33
40
|
end
|
|
34
41
|
|
|
42
|
+
def on_message_updated(&block)
|
|
43
|
+
@registry.register(:message_updated, &block)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def on_message_deleted(&block)
|
|
47
|
+
@registry.register(:message_deleted, &block)
|
|
48
|
+
end
|
|
49
|
+
|
|
35
50
|
def on_reaction(emojis = nil, &block)
|
|
36
51
|
@registry.register(:reaction, matcher: emojis, &block)
|
|
37
52
|
end
|
|
@@ -44,6 +59,14 @@ module ChatSDK
|
|
|
44
59
|
@registry.register(:slash_command, matcher: command, &block)
|
|
45
60
|
end
|
|
46
61
|
|
|
62
|
+
def on_installed(&block)
|
|
63
|
+
@registry.register(:installed, &block)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def on_uninstalled(&block)
|
|
67
|
+
@registry.register(:uninstalled, &block)
|
|
68
|
+
end
|
|
69
|
+
|
|
47
70
|
# Adapter access
|
|
48
71
|
def adapter(name)
|
|
49
72
|
@adapters.fetch(name) { raise ChatSDK::ConfigurationError, "Unknown adapter: #{name}" }
|
|
@@ -61,6 +84,11 @@ module ChatSDK
|
|
|
61
84
|
Channel.new(id: channel_id, adapter: adp, chat: self)
|
|
62
85
|
end
|
|
63
86
|
|
|
87
|
+
def thread(id, channel_id:, adapter_name: nil)
|
|
88
|
+
adp = adapter_name ? adapter(adapter_name) : @adapters.values.first
|
|
89
|
+
Thread.new(id: id, channel_id: channel_id, adapter: adp, chat: self)
|
|
90
|
+
end
|
|
91
|
+
|
|
64
92
|
# Webhook endpoints (Rack apps)
|
|
65
93
|
def webhooks
|
|
66
94
|
@webhook_accessor ||= WebhookAccessor.new(self, @adapters)
|
|
@@ -72,6 +100,11 @@ module ChatSDK
|
|
|
72
100
|
@dispatcher.dispatch(event, adapter: adp, adapter_name: adapter_name)
|
|
73
101
|
end
|
|
74
102
|
|
|
103
|
+
# Deprecated compatibility alias for Vercel Chat SDK's former API.
|
|
104
|
+
def transcripts
|
|
105
|
+
history.user
|
|
106
|
+
end
|
|
107
|
+
|
|
75
108
|
private
|
|
76
109
|
end
|
|
77
110
|
|
data/lib/chat_sdk/config.rb
CHANGED
|
@@ -6,12 +6,16 @@ module ChatSDK
|
|
|
6
6
|
dedupe_ttl: 600,
|
|
7
7
|
streaming_update_interval: 0.5,
|
|
8
8
|
on_lock_conflict: :drop,
|
|
9
|
+
concurrency: nil,
|
|
10
|
+
lock_scope: :thread,
|
|
9
11
|
handler_executor: :inline,
|
|
12
|
+
history: {},
|
|
10
13
|
log_level: :info
|
|
11
14
|
}.freeze
|
|
12
15
|
|
|
13
16
|
attr_reader :user_name, :adapters, :state, :on_lock_conflict,
|
|
14
|
-
:dedupe_ttl, :streaming_update_interval, :handler_executor, :log_level
|
|
17
|
+
:dedupe_ttl, :streaming_update_interval, :handler_executor, :log_level,
|
|
18
|
+
:history_user, :concurrency
|
|
15
19
|
|
|
16
20
|
def initialize(user_name:, adapters:, state:, **options)
|
|
17
21
|
raise ConfigurationError, "user_name is required" if user_name.nil? || user_name.empty?
|
|
@@ -23,9 +27,17 @@ module ChatSDK
|
|
|
23
27
|
@state = state
|
|
24
28
|
merged = DEFAULTS.merge(options)
|
|
25
29
|
@on_lock_conflict = merged[:on_lock_conflict]
|
|
30
|
+
@concurrency = normalize_concurrency(merged[:concurrency], lock_scope: merged[:lock_scope])
|
|
26
31
|
@dedupe_ttl = merged[:dedupe_ttl]
|
|
27
32
|
@streaming_update_interval = merged[:streaming_update_interval]
|
|
28
33
|
@handler_executor = merged[:handler_executor]
|
|
34
|
+
history = merged[:history] || {}
|
|
35
|
+
user_history = history[:user] || history["user"] || {}
|
|
36
|
+
@history_user = {
|
|
37
|
+
identity: user_history[:identity] || user_history["identity"] || merged[:identity],
|
|
38
|
+
retention: user_history.fetch(:retention, user_history.fetch("retention", 30 * 24 * 3600)),
|
|
39
|
+
max_per_user: user_history.fetch(:max_per_user, user_history.fetch("max_per_user", 200))
|
|
40
|
+
}
|
|
29
41
|
@log_level = merged[:log_level]
|
|
30
42
|
|
|
31
43
|
validate_lock_conflict!
|
|
@@ -37,5 +49,40 @@ module ChatSDK
|
|
|
37
49
|
return if %i[drop force].include?(@on_lock_conflict) || @on_lock_conflict.respond_to?(:call)
|
|
38
50
|
raise ConfigurationError, "on_lock_conflict must be :drop, :force, or a callable"
|
|
39
51
|
end
|
|
52
|
+
|
|
53
|
+
def normalize_concurrency(value, lock_scope:)
|
|
54
|
+
supplied = if value.nil?
|
|
55
|
+
{}
|
|
56
|
+
elsif value.is_a?(Hash)
|
|
57
|
+
value
|
|
58
|
+
else
|
|
59
|
+
{strategy: value}
|
|
60
|
+
end
|
|
61
|
+
supplied = supplied.transform_keys(&:to_sym)
|
|
62
|
+
supplied[:debounce] = supplied.delete(:debounce_ms).to_f / 1000 if supplied.key?(:debounce_ms)
|
|
63
|
+
supplied[:queue_entry_ttl] = supplied.delete(:queue_entry_ttl_ms).to_f / 1000 if supplied.key?(:queue_entry_ttl_ms)
|
|
64
|
+
supplied[:max_lock_lifetime] = supplied.delete(:max_lock_lifetime_ms).to_f / 1000 if supplied.key?(:max_lock_lifetime_ms)
|
|
65
|
+
supplied[:lock_ttl] = supplied.delete(:lock_ttl_ms).to_f / 1000 if supplied.key?(:lock_ttl_ms)
|
|
66
|
+
config = {
|
|
67
|
+
strategy: :drop,
|
|
68
|
+
max_queue_size: 10,
|
|
69
|
+
on_queue_full: :drop_oldest,
|
|
70
|
+
queue_entry_ttl: 90,
|
|
71
|
+
debounce: 1.5,
|
|
72
|
+
lock_ttl: 30,
|
|
73
|
+
max_lock_lifetime: 600,
|
|
74
|
+
max_concurrent: nil,
|
|
75
|
+
lock_scope: lock_scope
|
|
76
|
+
}.merge(supplied)
|
|
77
|
+
config[:strategy] = config[:strategy].to_sym
|
|
78
|
+
config[:on_queue_full] = config[:on_queue_full].to_s.tr("-", "_").to_sym
|
|
79
|
+
unless %i[drop force queue burst debounce concurrent].include?(config[:strategy])
|
|
80
|
+
raise ConfigurationError, "concurrency strategy must be :drop, :queue, :burst, :debounce, or :concurrent"
|
|
81
|
+
end
|
|
82
|
+
if config[:max_concurrent] && config[:max_concurrent].to_i < 1
|
|
83
|
+
raise ConfigurationError, "max_concurrent must be at least 1"
|
|
84
|
+
end
|
|
85
|
+
config
|
|
86
|
+
end
|
|
40
87
|
end
|
|
41
88
|
end
|
data/lib/chat_sdk/dispatcher.rb
CHANGED
|
@@ -1,40 +1,167 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
3
6
|
module ChatSDK
|
|
4
7
|
class Dispatcher
|
|
8
|
+
LockHeartbeat = Struct.new(:thread, :mutex, :condition, :stop, :ownership_lost) do
|
|
9
|
+
def ownership_lost?
|
|
10
|
+
mutex.synchronize { ownership_lost }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
5
14
|
def initialize(chat:, config:, state:, registry:)
|
|
6
15
|
@chat = chat
|
|
7
16
|
@config = config
|
|
8
17
|
@state = state
|
|
9
18
|
@registry = registry
|
|
19
|
+
@slot_mutex = Mutex.new
|
|
20
|
+
@slot_condition = ConditionVariable.new
|
|
21
|
+
@active_slots = Hash.new(0)
|
|
10
22
|
end
|
|
11
23
|
|
|
12
24
|
def dispatch(event, adapter:, adapter_name:)
|
|
13
25
|
ChatSDK::Instrumentation.instrument("dispatch.chat_sdk", adapter: adapter_name, event_type: event.type) do
|
|
14
|
-
|
|
26
|
+
if %i[message_updated message_deleted].include?(event.type)
|
|
27
|
+
return execute_event(event, build_thread(event, adapter))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
concurrency_scope = adapter.concurrency_scope(event)
|
|
31
|
+
return unless dedupe(event, adapter_name, concurrency_scope)
|
|
15
32
|
|
|
16
33
|
thread = build_thread(event, adapter)
|
|
17
|
-
thread_key = thread_key_for(event, adapter_name)
|
|
34
|
+
thread_key = thread_key_for(event, adapter_name, concurrency_scope)
|
|
35
|
+
strategy = @config.concurrency[:strategy]
|
|
18
36
|
|
|
19
|
-
|
|
37
|
+
if strategy == :concurrent
|
|
38
|
+
return with_concurrent_slot(thread_key) { execute_event(event, thread) }
|
|
39
|
+
end
|
|
20
40
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
41
|
+
owner = "#{Process.pid}:#{::Thread.current.object_id}:#{SecureRandom.uuid}"
|
|
42
|
+
if %i[queue burst debounce].include?(strategy)
|
|
43
|
+
dispatch_queued(event, adapter, thread_key, owner, strategy)
|
|
44
|
+
else
|
|
45
|
+
dispatch_locked(event, thread, thread_key, owner, strategy)
|
|
26
46
|
end
|
|
27
47
|
end
|
|
28
48
|
end
|
|
29
49
|
|
|
30
50
|
private
|
|
31
51
|
|
|
32
|
-
def
|
|
52
|
+
def dispatch_locked(event, thread, thread_key, owner, strategy)
|
|
53
|
+
acquired = acquire_lock(thread_key, event, owner, force: strategy == :force)
|
|
54
|
+
return unless acquired
|
|
55
|
+
|
|
56
|
+
heartbeat = start_lock_heartbeat(thread_key, owner)
|
|
57
|
+
execute_event(event, thread)
|
|
58
|
+
ensure
|
|
59
|
+
stop_lock_heartbeat(heartbeat)
|
|
60
|
+
release_lock(thread_key, owner) if acquired
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def dispatch_queued(event, adapter, thread_key, owner, strategy)
|
|
64
|
+
queue_key = "chat_sdk:queue:#{thread_key}"
|
|
65
|
+
acquired = acquire_lock(thread_key, event, owner, apply_conflict_policy: false)
|
|
66
|
+
pre_enqueued = false
|
|
67
|
+
|
|
68
|
+
unless acquired
|
|
69
|
+
enqueue(queue_key, event)
|
|
70
|
+
pre_enqueued = true
|
|
71
|
+
acquired = acquire_lock(thread_key, event, owner, apply_conflict_policy: false)
|
|
72
|
+
return unless acquired
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
heartbeat = start_lock_heartbeat(thread_key, owner)
|
|
76
|
+
if strategy == :queue
|
|
77
|
+
execute_event(event, build_thread(event, adapter)) unless pre_enqueued
|
|
78
|
+
else
|
|
79
|
+
enqueue(queue_key, event) unless pre_enqueued
|
|
80
|
+
sleep(@config.concurrency[:debounce].to_f) if strategy == :burst
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if strategy == :debounce
|
|
84
|
+
debounce_loop(queue_key, adapter, heartbeat)
|
|
85
|
+
else
|
|
86
|
+
drain_loop(queue_key, adapter, strategy, heartbeat)
|
|
87
|
+
end
|
|
88
|
+
ensure
|
|
89
|
+
stop_lock_heartbeat(heartbeat)
|
|
90
|
+
release_lock(thread_key, owner) if acquired
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def drain_loop(queue_key, adapter, strategy, heartbeat)
|
|
94
|
+
loop do
|
|
95
|
+
break if heartbeat&.ownership_lost?
|
|
96
|
+
|
|
97
|
+
entries = fresh_entries(@state.drain_queue(queue_key))
|
|
98
|
+
break if entries.empty?
|
|
99
|
+
|
|
100
|
+
events = entries.map { |entry| EventSerializer.load(entry.fetch("event")) }
|
|
101
|
+
current = events.last
|
|
102
|
+
skipped = if strategy == :debounce
|
|
103
|
+
[]
|
|
104
|
+
else
|
|
105
|
+
events[0...-1].filter_map { |queued| queued.message if queued.respond_to?(:message) }
|
|
106
|
+
end
|
|
107
|
+
context = {skipped: skipped, total_since_last_handler: skipped.length + 1}
|
|
108
|
+
execute_event(current, build_thread(current, adapter), context: context)
|
|
109
|
+
::Thread.pass
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def debounce_loop(queue_key, adapter, heartbeat)
|
|
114
|
+
skipped = []
|
|
115
|
+
loop do
|
|
116
|
+
sleep(@config.concurrency[:debounce].to_f)
|
|
117
|
+
break if heartbeat&.ownership_lost?
|
|
118
|
+
|
|
119
|
+
entries = fresh_entries(@state.drain_queue(queue_key))
|
|
120
|
+
break if entries.empty?
|
|
121
|
+
|
|
122
|
+
events = entries.map { |entry| EventSerializer.load(entry.fetch("event")) }
|
|
123
|
+
latest = events.last
|
|
124
|
+
skipped.concat(events[0...-1].filter_map { |queued| queued.message if queued.respond_to?(:message) })
|
|
125
|
+
|
|
126
|
+
if @state.queue_depth(queue_key).positive?
|
|
127
|
+
skipped << latest.message if latest.respond_to?(:message)
|
|
128
|
+
next
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
relevant = skipped.select do |message|
|
|
132
|
+
message.thread_id == latest.thread_id
|
|
133
|
+
end
|
|
134
|
+
context = {skipped: relevant, total_since_last_handler: relevant.length + 1}
|
|
135
|
+
execute_event(latest, build_thread(latest, adapter), context: context)
|
|
136
|
+
skipped.clear
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def enqueue(queue_key, event)
|
|
141
|
+
entry = {"event" => EventSerializer.dump(event), "enqueued_at" => Time.now.iso8601}
|
|
142
|
+
@state.enqueue(
|
|
143
|
+
queue_key,
|
|
144
|
+
entry,
|
|
145
|
+
max_size: @config.concurrency[:max_queue_size],
|
|
146
|
+
drop: @config.concurrency[:on_queue_full]
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def fresh_entries(entries)
|
|
151
|
+
cutoff = Time.now - @config.concurrency[:queue_entry_ttl].to_f
|
|
152
|
+
entries.select do |entry|
|
|
153
|
+
Time.iso8601(entry.fetch("enqueued_at")) >= cutoff
|
|
154
|
+
rescue ArgumentError, KeyError
|
|
155
|
+
false
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def dedupe(event, adapter_name, concurrency_scope)
|
|
33
160
|
event_id = extract_event_id(event)
|
|
34
161
|
return true unless event_id
|
|
35
162
|
|
|
36
163
|
is_new = @state.set_if_absent(
|
|
37
|
-
|
|
164
|
+
dedupe_key(adapter_name, concurrency_scope, event_id),
|
|
38
165
|
true,
|
|
39
166
|
ttl: @config.dedupe_ttl
|
|
40
167
|
)
|
|
@@ -45,41 +172,85 @@ module ChatSDK
|
|
|
45
172
|
def extract_event_id(event)
|
|
46
173
|
if event.respond_to?(:message) && event.message
|
|
47
174
|
event.message.id
|
|
175
|
+
elsif event.respond_to?(:message_id) && event.message_id
|
|
176
|
+
"#{event.type}:#{event.message_id}:#{event.timestamp.to_f}"
|
|
48
177
|
elsif event.respond_to?(:raw) && event.raw.is_a?(Hash)
|
|
49
178
|
event.raw[:event_id] || event.raw["event_id"]
|
|
50
179
|
end
|
|
51
180
|
end
|
|
52
181
|
|
|
53
|
-
def
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"#{
|
|
182
|
+
def dedupe_key(adapter_name, concurrency_scope, event_id)
|
|
183
|
+
prefix = ["chat_sdk:dedupe", adapter_name]
|
|
184
|
+
prefix << concurrency_scope if concurrency_scope
|
|
185
|
+
"#{prefix.join(":")}:#{event_id}"
|
|
57
186
|
end
|
|
58
187
|
|
|
59
|
-
def
|
|
60
|
-
|
|
188
|
+
def thread_key_for(event, adapter_name, concurrency_scope)
|
|
189
|
+
channel_id = event.respond_to?(:channel_id) ? event.channel_id : nil
|
|
190
|
+
thread_id = event.respond_to?(:thread_id) ? event.thread_id : nil
|
|
191
|
+
lock_scope = @config.concurrency[:lock_scope]
|
|
192
|
+
scope_id = (lock_scope&.to_sym == :channel) ? channel_id : thread_id
|
|
193
|
+
[adapter_name, concurrency_scope, channel_id, scope_id].compact.join(":")
|
|
61
194
|
end
|
|
62
195
|
|
|
63
|
-
def acquire_lock(thread_key, event)
|
|
196
|
+
def acquire_lock(thread_key, event, owner, force: false, apply_conflict_policy: true)
|
|
64
197
|
lock_key = "chat_sdk:lock:#{thread_key}"
|
|
65
|
-
acquired = @state.acquire_lock(lock_key, owner:
|
|
198
|
+
acquired = @state.acquire_lock(lock_key, owner: owner, ttl: @config.concurrency[:lock_ttl])
|
|
66
199
|
|
|
67
|
-
|
|
68
|
-
policy = @config.on_lock_conflict
|
|
200
|
+
if !acquired && apply_conflict_policy
|
|
201
|
+
policy = force ? :force : @config.on_lock_conflict
|
|
69
202
|
policy = policy.call(thread_key, event) if policy.respond_to?(:call)
|
|
70
|
-
|
|
71
203
|
if policy == :force
|
|
72
|
-
@state.force_lock(lock_key, owner:
|
|
204
|
+
@state.force_lock(lock_key, owner: owner, ttl: @config.concurrency[:lock_ttl])
|
|
73
205
|
acquired = true
|
|
74
206
|
end
|
|
75
207
|
end
|
|
76
208
|
|
|
77
|
-
ChatSDK::Instrumentation.instrument("lock.chat_sdk", key: thread_key, acquired: acquired
|
|
209
|
+
ChatSDK::Instrumentation.instrument("lock.chat_sdk", key: thread_key, acquired: acquired)
|
|
78
210
|
acquired
|
|
79
211
|
end
|
|
80
212
|
|
|
81
|
-
def release_lock(thread_key)
|
|
82
|
-
@state.release_lock("chat_sdk:lock:#{thread_key}", owner:
|
|
213
|
+
def release_lock(thread_key, owner)
|
|
214
|
+
@state.release_lock("chat_sdk:lock:#{thread_key}", owner: owner)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def start_lock_heartbeat(thread_key, owner)
|
|
218
|
+
lock_ttl = @config.concurrency[:lock_ttl].to_f
|
|
219
|
+
max_lifetime = @config.concurrency[:max_lock_lifetime].to_f
|
|
220
|
+
mutex = Mutex.new
|
|
221
|
+
condition = ConditionVariable.new
|
|
222
|
+
heartbeat = LockHeartbeat.new(mutex: mutex, condition: condition, stop: false, ownership_lost: false)
|
|
223
|
+
heartbeat.thread = ::Thread.new do
|
|
224
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
225
|
+
interval = [lock_ttl / 3.0, 0.01].max
|
|
226
|
+
loop do
|
|
227
|
+
should_stop = mutex.synchronize do
|
|
228
|
+
condition.wait(mutex, interval) unless heartbeat.stop
|
|
229
|
+
heartbeat.stop
|
|
230
|
+
end
|
|
231
|
+
break if should_stop
|
|
232
|
+
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) - started >= max_lifetime
|
|
233
|
+
|
|
234
|
+
extended = @state.extend_lock("chat_sdk:lock:#{thread_key}", owner: owner, ttl: lock_ttl)
|
|
235
|
+
unless extended
|
|
236
|
+
mutex.synchronize { heartbeat.ownership_lost = true }
|
|
237
|
+
break
|
|
238
|
+
end
|
|
239
|
+
rescue NotImplementedError
|
|
240
|
+
break
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
heartbeat
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def stop_lock_heartbeat(heartbeat)
|
|
247
|
+
return unless heartbeat
|
|
248
|
+
|
|
249
|
+
heartbeat.mutex.synchronize do
|
|
250
|
+
heartbeat.stop = true
|
|
251
|
+
heartbeat.condition.signal
|
|
252
|
+
end
|
|
253
|
+
heartbeat.thread.join
|
|
83
254
|
end
|
|
84
255
|
|
|
85
256
|
def build_thread(event, adapter)
|
|
@@ -87,17 +258,75 @@ module ChatSDK
|
|
|
87
258
|
channel_id = event.respond_to?(:channel_id) ? event.channel_id : nil
|
|
88
259
|
return nil unless thread_id && channel_id
|
|
89
260
|
|
|
90
|
-
|
|
261
|
+
current_message = event.respond_to?(:message) ? event.message : nil
|
|
262
|
+
ChatSDK::Thread.new(id: thread_id, channel_id: channel_id, adapter: adapter, chat: @chat, current_message: current_message)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def execute_event(event, thread, context: nil)
|
|
266
|
+
if %i[mention subscribed_message direct_message].include?(event.type)
|
|
267
|
+
route_message_event(event, thread, context)
|
|
268
|
+
return
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
@registry.handlers_for(event).each do |handler|
|
|
272
|
+
execute_handler(handler, event, thread, context: context)
|
|
273
|
+
end
|
|
91
274
|
end
|
|
92
275
|
|
|
93
|
-
def
|
|
276
|
+
def route_message_event(event, thread, context)
|
|
277
|
+
message = event.message
|
|
278
|
+
dm_handlers = @registry.handlers_for_type(:direct_message, event: event)
|
|
279
|
+
if event.type == :direct_message && dm_handlers.any?
|
|
280
|
+
run_handlers(dm_handlers, event, thread, context)
|
|
281
|
+
return
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
message.is_mention = true if event.type == :direct_message
|
|
285
|
+
|
|
286
|
+
if thread&.subscribed?
|
|
287
|
+
run_handlers(@registry.handlers_for_type(:subscribed_message, event: event), event, thread, context)
|
|
288
|
+
return
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
if message.is_mention.nil?
|
|
292
|
+
message.is_mention = if event.type == :mention
|
|
293
|
+
true
|
|
294
|
+
else
|
|
295
|
+
detect_mention(message.text)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
if message.is_mention
|
|
300
|
+
run_handlers(@registry.handlers_for_type(:mention, event: event), event, thread, context)
|
|
301
|
+
return
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
@registry.handlers_for_type(:new_message, event: event).each do |handler|
|
|
305
|
+
execute_handler(handler, event, thread, context: context)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def run_handlers(handlers, event, thread, context)
|
|
310
|
+
handlers.each { |handler| execute_handler(handler, event, thread, context: context) }
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def detect_mention(text)
|
|
314
|
+
masked_text = ChatSDK::Format::Mentions.mask_code_spans(text)
|
|
315
|
+
/(?<![[:word:]])@#{Regexp.escape(@config.user_name)}\b/i.match?(masked_text)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def execute_handler(handler, event, thread, context: nil)
|
|
94
319
|
ChatSDK::Instrumentation.instrument("handler.chat_sdk", handler_type: event.type) do
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
320
|
+
ChatSDK::AI::ConversationScope.with(thread) do
|
|
321
|
+
case event.type
|
|
322
|
+
when :mention, :subscribed_message, :direct_message, :new_message
|
|
323
|
+
handler.block.call(thread, event.message, context)
|
|
324
|
+
when :message_updated
|
|
325
|
+
handler.block.call(thread, event.message, event.previous_message)
|
|
326
|
+
when :reaction, :action, :slash_command, :message_deleted, :installed, :uninstalled
|
|
327
|
+
add_thread_to_event(event, thread)
|
|
328
|
+
handler.block.call(event)
|
|
329
|
+
end
|
|
101
330
|
end
|
|
102
331
|
end
|
|
103
332
|
rescue => e
|
|
@@ -108,5 +337,23 @@ module ChatSDK
|
|
|
108
337
|
def add_thread_to_event(event, thread)
|
|
109
338
|
event.thread = thread if event.respond_to?(:thread=)
|
|
110
339
|
end
|
|
340
|
+
|
|
341
|
+
def with_concurrent_slot(key)
|
|
342
|
+
max = @config.concurrency[:max_concurrent]
|
|
343
|
+
return yield unless max
|
|
344
|
+
|
|
345
|
+
@slot_mutex.synchronize do
|
|
346
|
+
@slot_condition.wait(@slot_mutex) while @active_slots[key] >= max
|
|
347
|
+
@active_slots[key] += 1
|
|
348
|
+
end
|
|
349
|
+
yield
|
|
350
|
+
ensure
|
|
351
|
+
if max
|
|
352
|
+
@slot_mutex.synchronize do
|
|
353
|
+
@active_slots[key] -= 1
|
|
354
|
+
@slot_condition.broadcast
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
111
358
|
end
|
|
112
359
|
end
|
|
@@ -13,13 +13,18 @@ module ChatSDK
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def handlers_for(event)
|
|
16
|
-
|
|
16
|
+
handlers_for_type(event.type, event: event)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def handlers_for_type(type, event: nil)
|
|
20
|
+
@handlers.select do |handler|
|
|
21
|
+
handler.type == type && (!event || matches?(handler, event))
|
|
22
|
+
end
|
|
17
23
|
end
|
|
18
24
|
|
|
19
25
|
private
|
|
20
26
|
|
|
21
27
|
def matches?(handler, event)
|
|
22
|
-
return false unless handler.type == event.type
|
|
23
28
|
return true if handler.matcher.nil?
|
|
24
29
|
|
|
25
30
|
case handler.matcher
|