chat_sdk 1.2.0 → 1.4.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
|
@@ -16,7 +16,7 @@ module ChatSDK
|
|
|
16
16
|
data.merge!(
|
|
17
17
|
"message_id" => event.message_id,
|
|
18
18
|
"previous_message" => dump_message(event.previous_message),
|
|
19
|
-
"deleted_at" => event.deleted_at&.iso8601
|
|
19
|
+
"deleted_at" => event.deleted_at&.iso8601(6)
|
|
20
20
|
)
|
|
21
21
|
when :reaction
|
|
22
22
|
data.merge!("emoji" => event.emoji, "user_id" => event.user_id, "message_id" => event.message_id, "added" => event.added?)
|
|
@@ -33,6 +33,18 @@ module ChatSDK
|
|
|
33
33
|
"action" => event.action,
|
|
34
34
|
"user" => dump_author(event.user)
|
|
35
35
|
)
|
|
36
|
+
when :assistant_thread_started, :assistant_context_changed
|
|
37
|
+
data.merge!(
|
|
38
|
+
"thread_ts" => event.thread_ts,
|
|
39
|
+
"user_id" => event.user_id,
|
|
40
|
+
"context" => event.context.to_h.transform_keys(&:to_s)
|
|
41
|
+
)
|
|
42
|
+
when :app_home_opened
|
|
43
|
+
data["user_id"] = event.user_id
|
|
44
|
+
data["tab"] = event.tab
|
|
45
|
+
when :member_joined_channel
|
|
46
|
+
data["user_id"] = event.user_id
|
|
47
|
+
data["inviter_id"] = event.inviter_id
|
|
36
48
|
end
|
|
37
49
|
data
|
|
38
50
|
end
|
|
@@ -85,6 +97,24 @@ module ChatSDK
|
|
|
85
97
|
user: load_author(data["user"]),
|
|
86
98
|
**common
|
|
87
99
|
)
|
|
100
|
+
when :assistant_thread_started, :assistant_context_changed
|
|
101
|
+
event_class = if type == :assistant_thread_started
|
|
102
|
+
Events::AssistantThreadStarted
|
|
103
|
+
else
|
|
104
|
+
Events::AssistantContextChanged
|
|
105
|
+
end
|
|
106
|
+
event_class.new(
|
|
107
|
+
thread_ts: data["thread_ts"],
|
|
108
|
+
user_id: data["user_id"],
|
|
109
|
+
context: data["context"] || {},
|
|
110
|
+
**common
|
|
111
|
+
)
|
|
112
|
+
when :app_home_opened
|
|
113
|
+
common.delete(:thread_id)
|
|
114
|
+
Events::AppHomeOpened.new(user_id: data["user_id"], tab: data["tab"], **common)
|
|
115
|
+
when :member_joined_channel
|
|
116
|
+
common.delete(:thread_id)
|
|
117
|
+
Events::MemberJoinedChannel.new(user_id: data["user_id"], inviter_id: data["inviter_id"], **common)
|
|
88
118
|
else
|
|
89
119
|
raise ArgumentError, "unsupported queued event type: #{type}"
|
|
90
120
|
end
|
|
@@ -100,7 +130,7 @@ module ChatSDK
|
|
|
100
130
|
"thread_id" => event.respond_to?(:thread_id) ? event.thread_id : nil,
|
|
101
131
|
"channel_id" => event.respond_to?(:channel_id) ? event.channel_id : nil,
|
|
102
132
|
"raw" => json_safe(event.raw),
|
|
103
|
-
"timestamp" => event.timestamp&.iso8601
|
|
133
|
+
"timestamp" => event.timestamp&.iso8601(6)
|
|
104
134
|
}
|
|
105
135
|
end
|
|
106
136
|
|
|
@@ -119,7 +149,7 @@ module ChatSDK
|
|
|
119
149
|
"reply_to" => dump_message(message.reply_to),
|
|
120
150
|
"subject" => json_safe(message.subject),
|
|
121
151
|
"raw" => json_safe(message.raw),
|
|
122
|
-
"timestamp" => message.timestamp&.iso8601,
|
|
152
|
+
"timestamp" => message.timestamp&.iso8601(6),
|
|
123
153
|
"is_mention" => message.is_mention
|
|
124
154
|
}
|
|
125
155
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Events
|
|
5
|
+
class AppHomeOpened < Base
|
|
6
|
+
attr_reader :user_id, :channel_id, :tab
|
|
7
|
+
|
|
8
|
+
def initialize(user_id:, channel_id:, tab: nil, **kwargs)
|
|
9
|
+
super(type: :app_home_opened, **kwargs)
|
|
10
|
+
@user_id = user_id
|
|
11
|
+
@channel_id = channel_id
|
|
12
|
+
@tab = tab
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Events
|
|
5
|
+
# rubocop:disable Style/RedundantStructKeywordInit
|
|
6
|
+
AssistantContext = Struct.new(
|
|
7
|
+
:channel_id,
|
|
8
|
+
:team_id,
|
|
9
|
+
:enterprise_id,
|
|
10
|
+
:thread_entry_point,
|
|
11
|
+
:force_search,
|
|
12
|
+
keyword_init: true
|
|
13
|
+
)
|
|
14
|
+
# rubocop:enable Style/RedundantStructKeywordInit
|
|
15
|
+
|
|
16
|
+
def AssistantContext.coerce(value)
|
|
17
|
+
return value if value.is_a?(self)
|
|
18
|
+
|
|
19
|
+
value = {} if value.nil?
|
|
20
|
+
return value unless value.is_a?(Hash)
|
|
21
|
+
|
|
22
|
+
new(**value.transform_keys(&:to_sym).slice(*members))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Events
|
|
5
|
+
class AssistantContextChanged < Base
|
|
6
|
+
attr_reader :thread_id, :channel_id, :thread_ts, :user_id, :context
|
|
7
|
+
|
|
8
|
+
def initialize(thread_id:, channel_id:, thread_ts:, user_id:, context: {}, **kwargs)
|
|
9
|
+
super(type: :assistant_context_changed, **kwargs)
|
|
10
|
+
@thread_id = thread_id
|
|
11
|
+
@channel_id = channel_id
|
|
12
|
+
@thread_ts = thread_ts
|
|
13
|
+
@user_id = user_id
|
|
14
|
+
@context = AssistantContext.coerce(context)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Events
|
|
5
|
+
class AssistantThreadStarted < Base
|
|
6
|
+
attr_reader :thread_id, :channel_id, :thread_ts, :user_id, :context
|
|
7
|
+
|
|
8
|
+
def initialize(thread_id:, channel_id:, thread_ts:, user_id:, context: {}, **kwargs)
|
|
9
|
+
super(type: :assistant_thread_started, **kwargs)
|
|
10
|
+
@thread_id = thread_id
|
|
11
|
+
@channel_id = channel_id
|
|
12
|
+
@thread_ts = thread_ts
|
|
13
|
+
@user_id = user_id
|
|
14
|
+
@context = AssistantContext.coerce(context)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Events
|
|
5
|
+
class MemberJoinedChannel < Base
|
|
6
|
+
attr_reader :user_id, :channel_id, :inviter_id
|
|
7
|
+
|
|
8
|
+
def initialize(user_id:, channel_id:, inviter_id: nil, **kwargs)
|
|
9
|
+
super(type: :member_joined_channel, **kwargs)
|
|
10
|
+
@user_id = user_id
|
|
11
|
+
@channel_id = channel_id
|
|
12
|
+
@inviter_id = inviter_id
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
module ChatSDK
|
|
4
4
|
module Modals
|
|
5
5
|
class Builder
|
|
6
|
-
def initialize(title:, submit_label: nil, callback_id: nil, &block)
|
|
6
|
+
def initialize(title:, submit_label: nil, callback_id: nil, close_label: nil, &block)
|
|
7
7
|
@title = title
|
|
8
8
|
@submit_label = submit_label
|
|
9
9
|
@callback_id = callback_id
|
|
10
|
+
@close_label = close_label
|
|
10
11
|
@children = []
|
|
11
12
|
instance_eval(&block) if block
|
|
12
13
|
end
|
|
@@ -15,23 +16,57 @@ module ChatSDK
|
|
|
15
16
|
attrs = {title: @title}
|
|
16
17
|
attrs[:submit_label] = @submit_label if @submit_label
|
|
17
18
|
attrs[:callback_id] = @callback_id if @callback_id
|
|
19
|
+
attrs[:close_label] = @close_label if @close_label
|
|
18
20
|
ChatSDK::Cards::Node.new(:modal, attributes: attrs, children: @children)
|
|
19
21
|
end
|
|
20
22
|
|
|
21
|
-
def text_input(id:, label:, placeholder: nil, multiline: false, optional: false)
|
|
23
|
+
def text_input(id:, label:, placeholder: nil, multiline: false, optional: false, initial_value: nil, max_length: nil)
|
|
22
24
|
attrs = {id: id, label: label, input_type: :text, multiline: multiline, optional: optional}
|
|
23
25
|
attrs[:placeholder] = placeholder if placeholder
|
|
26
|
+
attrs[:initial_value] = initial_value unless initial_value.nil?
|
|
27
|
+
attrs[:max_length] = max_length unless max_length.nil?
|
|
24
28
|
@children << ChatSDK::Cards::Node.new(:input, attributes: attrs)
|
|
25
29
|
end
|
|
26
30
|
|
|
27
|
-
def select_input(id:, label:, placeholder: nil, optional: false, &block)
|
|
31
|
+
def select_input(id:, label:, placeholder: nil, optional: false, initial_option: nil, dispatch_action: nil, &block)
|
|
28
32
|
ctx = ChatSDK::Cards::SelectContext.new
|
|
29
|
-
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
|
+
|
|
30
36
|
attrs = {id: id, label: label, input_type: :select, optional: optional}
|
|
31
37
|
attrs[:placeholder] = placeholder if placeholder
|
|
38
|
+
attrs[:initial_option] = initial_option unless initial_option.nil?
|
|
39
|
+
attrs[:dispatch_action] = dispatch_action unless dispatch_action.nil?
|
|
40
|
+
@children << ChatSDK::Cards::Node.new(:input, attributes: attrs, children: ctx.nodes)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def radio_select_input(id:, label:, initial_option: nil, optional: nil, dispatch_action: nil, &block)
|
|
44
|
+
ctx = ChatSDK::Cards::SelectContext.new
|
|
45
|
+
ctx.instance_eval(&block) if block
|
|
46
|
+
raise ArgumentError, "RadioSelect requires at least one option" if ctx.nodes.empty?
|
|
47
|
+
|
|
48
|
+
attrs = {id: id, label: label, input_type: :radio_select}
|
|
49
|
+
attrs[:initial_option] = initial_option unless initial_option.nil?
|
|
50
|
+
attrs[:optional] = optional unless optional.nil?
|
|
51
|
+
attrs[:dispatch_action] = dispatch_action unless dispatch_action.nil?
|
|
32
52
|
@children << ChatSDK::Cards::Node.new(:input, attributes: attrs, children: ctx.nodes)
|
|
33
53
|
end
|
|
34
54
|
|
|
55
|
+
def external_select_input(id:, label:, placeholder: nil, min_query_length: nil, initial_option: nil, optional: nil)
|
|
56
|
+
attrs = {id: id, label: label, input_type: :external_select}
|
|
57
|
+
attrs[:placeholder] = placeholder if placeholder
|
|
58
|
+
attrs[:min_query_length] = min_query_length unless min_query_length.nil?
|
|
59
|
+
attrs[:initial_option] = initial_option unless initial_option.nil?
|
|
60
|
+
attrs[:optional] = optional unless optional.nil?
|
|
61
|
+
@children << ChatSDK::Cards::Node.new(:input, attributes: attrs)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def fields(&block)
|
|
65
|
+
ctx = ChatSDK::Cards::FieldsContext.new
|
|
66
|
+
ctx.instance_eval(&block)
|
|
67
|
+
@children << ChatSDK::Cards::Node.new(:fields, children: ctx.nodes)
|
|
68
|
+
end
|
|
69
|
+
|
|
35
70
|
def date_input(id:, label:, initial_value: nil, placeholder: nil, optional: false)
|
|
36
71
|
attrs = {id: id, label: label, input_type: :date, optional: optional}
|
|
37
72
|
attrs[:initial_value] = initial_value if initial_value
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Streaming
|
|
5
|
+
module Chunk
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def normalize(value)
|
|
9
|
+
return value if value.is_a?(MarkdownTextChunk) || value.is_a?(TaskUpdateChunk) || value.is_a?(PlanUpdateChunk)
|
|
10
|
+
|
|
11
|
+
if value.is_a?(Hash)
|
|
12
|
+
type = value[:type] || value["type"]
|
|
13
|
+
attributes = value.each_with_object({}) do |(key, entry), result|
|
|
14
|
+
result[key.to_sym] = entry
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
return MarkdownTextChunk.new(text: attributes[:text]) if type.to_s == "markdown_text"
|
|
18
|
+
return TaskUpdateChunk.new(**attributes.except(:type)) if type.to_s == "task_update"
|
|
19
|
+
return PlanUpdateChunk.new(**attributes.except(:type)) if type.to_s == "plan_update"
|
|
20
|
+
|
|
21
|
+
return nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
value.to_s
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def text_of(value)
|
|
28
|
+
case value
|
|
29
|
+
when String then value
|
|
30
|
+
when MarkdownTextChunk then value.text
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -7,37 +7,70 @@ module ChatSDK
|
|
|
7
7
|
|
|
8
8
|
attr_reader :adapter, :channel_id, :thread_id, :update_interval
|
|
9
9
|
|
|
10
|
-
def initialize(adapter:, channel_id:, thread_id:, placeholder: nil, update_interval: 0.5)
|
|
10
|
+
def initialize(adapter:, channel_id:, thread_id:, placeholder: nil, update_interval: 0.5, ai_generated: false)
|
|
11
11
|
@adapter = adapter
|
|
12
12
|
@channel_id = channel_id
|
|
13
13
|
@thread_id = thread_id
|
|
14
14
|
@placeholder = placeholder
|
|
15
15
|
@update_interval = update_interval
|
|
16
|
+
@ai_generated = ai_generated
|
|
16
17
|
@buffer = +""
|
|
18
|
+
@delivered_text = +""
|
|
17
19
|
@message_id = nil
|
|
20
|
+
@message = nil
|
|
18
21
|
@last_flush_at = nil
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
def <<(chunk)
|
|
22
|
-
|
|
25
|
+
text = Chunk.text_of(Chunk.normalize(chunk))
|
|
26
|
+
return self unless text
|
|
27
|
+
|
|
28
|
+
@buffer << text
|
|
23
29
|
maybe_flush
|
|
24
30
|
self
|
|
25
31
|
end
|
|
26
32
|
|
|
27
|
-
def run(&block)
|
|
33
|
+
def run(source = nil, &block)
|
|
28
34
|
post_placeholder if @placeholder
|
|
29
|
-
|
|
35
|
+
if source
|
|
36
|
+
source.each { |chunk| self << chunk }
|
|
37
|
+
elsif block
|
|
38
|
+
block.call(self)
|
|
39
|
+
end
|
|
30
40
|
flush
|
|
41
|
+
message_with_text(@message, @delivered_text)
|
|
31
42
|
end
|
|
32
43
|
|
|
33
44
|
private
|
|
34
45
|
|
|
46
|
+
def message_with_text(message, text)
|
|
47
|
+
return unless message
|
|
48
|
+
|
|
49
|
+
ChatSDK::Message.new(
|
|
50
|
+
id: message.id,
|
|
51
|
+
text: text.dup,
|
|
52
|
+
author: message.author,
|
|
53
|
+
thread_id: message.thread_id,
|
|
54
|
+
channel_id: message.channel_id,
|
|
55
|
+
platform: message.platform,
|
|
56
|
+
attachments: message.attachments,
|
|
57
|
+
links: message.links,
|
|
58
|
+
reply_to: message.reply_to,
|
|
59
|
+
subject: message.subject,
|
|
60
|
+
raw: message.raw,
|
|
61
|
+
timestamp: message.timestamp,
|
|
62
|
+
is_mention: message.is_mention
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
35
66
|
def post_placeholder
|
|
36
67
|
result = adapter.post_message(
|
|
37
68
|
channel_id: channel_id,
|
|
38
|
-
message: PostableMessage.new(text: @placeholder),
|
|
69
|
+
message: PostableMessage.new(text: @placeholder, ai_generated: @ai_generated),
|
|
39
70
|
thread_id: thread_id
|
|
40
71
|
)
|
|
72
|
+
@delivered_text = @placeholder.dup
|
|
73
|
+
@message = result if result.is_a?(ChatSDK::Message)
|
|
41
74
|
@message_id = result.id if result.respond_to?(:id)
|
|
42
75
|
end
|
|
43
76
|
|
|
@@ -56,16 +89,19 @@ module ChatSDK
|
|
|
56
89
|
:edit_message,
|
|
57
90
|
channel_id: channel_id,
|
|
58
91
|
message_id: @message_id,
|
|
59
|
-
message: PostableMessage.new(text: @buffer.dup),
|
|
92
|
+
message: PostableMessage.new(text: @buffer.dup, ai_generated: @ai_generated),
|
|
60
93
|
thread_id: thread_id
|
|
61
94
|
)
|
|
95
|
+
@delivered_text = @buffer.dup
|
|
62
96
|
elsif !@message_id
|
|
63
97
|
result = adapter.post_message(
|
|
64
98
|
channel_id: channel_id,
|
|
65
|
-
message: PostableMessage.new(text: @buffer.dup),
|
|
99
|
+
message: PostableMessage.new(text: @buffer.dup, ai_generated: @ai_generated),
|
|
66
100
|
thread_id: thread_id
|
|
67
101
|
)
|
|
102
|
+
@message = result if result.is_a?(ChatSDK::Message)
|
|
68
103
|
@message_id = result.id if result.respond_to?(:id)
|
|
104
|
+
@delivered_text = @buffer.dup
|
|
69
105
|
end
|
|
70
106
|
|
|
71
107
|
@last_flush_at = Time.now.to_f
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Streaming
|
|
5
|
+
TASK_UPDATE_STATUSES = %w[pending in_progress complete error].freeze
|
|
6
|
+
private_constant :TASK_UPDATE_STATUSES
|
|
7
|
+
|
|
8
|
+
TaskUpdateChunk = Data.define(:id, :title, :status, :details, :output) do
|
|
9
|
+
def initialize(id:, title:, status:, details: nil, output: nil)
|
|
10
|
+
unless TASK_UPDATE_STATUSES.include?(status.to_s)
|
|
11
|
+
raise ArgumentError, "status must be one of #{TASK_UPDATE_STATUSES.join(", ")}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
super(id: id, title: title, status: status.to_s, details: details, output: output)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def type
|
|
18
|
+
"task_update"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_h
|
|
22
|
+
{type: type, id: id, title: title, status: status, details: details, output: output}.compact
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
class StreamingPlan
|
|
5
|
+
GROUP_TASKS = %w[plan timeline].freeze
|
|
6
|
+
|
|
7
|
+
attr_reader :stream, :options
|
|
8
|
+
|
|
9
|
+
def initialize(stream, group_tasks: nil, end_with: nil, update_interval: nil, ai_generated: false)
|
|
10
|
+
if group_tasks && !GROUP_TASKS.include?(group_tasks.to_s)
|
|
11
|
+
raise ArgumentError, "group_tasks must be :plan or :timeline"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
@stream = stream
|
|
15
|
+
@options = {
|
|
16
|
+
group_tasks: group_tasks&.to_s,
|
|
17
|
+
end_with: end_with,
|
|
18
|
+
update_interval: update_interval,
|
|
19
|
+
ai_generated: ai_generated
|
|
20
|
+
}.compact
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_stream_options
|
|
24
|
+
{
|
|
25
|
+
task_display_mode: options[:group_tasks],
|
|
26
|
+
stop_blocks: options[:end_with],
|
|
27
|
+
update_interval: options[:update_interval],
|
|
28
|
+
ai_generated: (true if options[:ai_generated])
|
|
29
|
+
}.compact
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/chat_sdk/thread.rb
CHANGED
|
@@ -27,6 +27,8 @@ module ChatSDK
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def post(content)
|
|
30
|
+
return handle_stream(content.stream, content.to_stream_options) if content.is_a?(StreamingPlan)
|
|
31
|
+
|
|
30
32
|
message = PostableMessage.from(content)
|
|
31
33
|
adapter.post_message(channel_id: channel_id, message: message, thread_id: id)
|
|
32
34
|
end
|
|
@@ -62,15 +64,24 @@ module ChatSDK
|
|
|
62
64
|
adapter.post_ephemeral(channel_id: channel_id, user_id: user_id, message: message, thread_id: id)
|
|
63
65
|
end
|
|
64
66
|
|
|
65
|
-
def post_stream(placeholder: nil, &block)
|
|
66
|
-
stream
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
def post_stream(source = nil, placeholder: nil, **plan_options, &block)
|
|
68
|
+
raise ArgumentError, "provide either a stream source or a block" if !source.nil? && block
|
|
69
|
+
raise ArgumentError, "a stream source or block is required" if source.nil? && !block
|
|
70
|
+
|
|
71
|
+
if block
|
|
72
|
+
source = Enumerator.new do |yielder|
|
|
73
|
+
block.call(Streaming::Writer.new(yielder))
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
if source.is_a?(StreamingPlan)
|
|
78
|
+
options = StreamingPlan.new(source.stream, **source.options.merge(plan_options)).to_stream_options
|
|
79
|
+
source = source.stream
|
|
80
|
+
else
|
|
81
|
+
options = StreamingPlan.new(source, **plan_options).to_stream_options
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
handle_stream(source, options.merge(placeholder: placeholder))
|
|
74
85
|
end
|
|
75
86
|
|
|
76
87
|
def edit(message_id, content)
|
|
@@ -90,8 +101,8 @@ module ChatSDK
|
|
|
90
101
|
call_adapter(:remove_reaction, channel_id: channel_id, message_id: message_id, emoji: emoji, thread_id: id)
|
|
91
102
|
end
|
|
92
103
|
|
|
93
|
-
def post_ai_stream(enumerable, placeholder: "Thinking...")
|
|
94
|
-
ChatSDK::AI::StreamHandler.stream_to_thread(self, enumerable, placeholder: placeholder)
|
|
104
|
+
def post_ai_stream(enumerable, placeholder: "Thinking...", **plan_options)
|
|
105
|
+
ChatSDK::AI::StreamHandler.stream_to_thread(self, enumerable, placeholder: placeholder, **plan_options)
|
|
95
106
|
end
|
|
96
107
|
|
|
97
108
|
def upload(io:, filename:, comment: nil)
|
|
@@ -129,6 +140,60 @@ module ChatSDK
|
|
|
129
140
|
|
|
130
141
|
private
|
|
131
142
|
|
|
143
|
+
def handle_stream(source, caller_options)
|
|
144
|
+
options = caller_options.merge(
|
|
145
|
+
update_interval: caller_options[:update_interval] || chat.config.streaming_update_interval
|
|
146
|
+
)
|
|
147
|
+
if current_message
|
|
148
|
+
options[:recipient_user_id] = current_message.author&.id
|
|
149
|
+
options[:recipient_team_id] = extract_recipient_team_id(current_message.raw)
|
|
150
|
+
end
|
|
151
|
+
options = options.reject { |_, value| value.nil? }
|
|
152
|
+
normalized_source = Enumerator.new do |yielder|
|
|
153
|
+
source.each do |item|
|
|
154
|
+
chunk = Streaming::Chunk.normalize(item)
|
|
155
|
+
yielder << chunk unless chunk.nil?
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
if adapter.respond_to?(:stream)
|
|
160
|
+
native_message = adapter.stream(
|
|
161
|
+
channel_id: channel_id,
|
|
162
|
+
thread_id: id,
|
|
163
|
+
stream: normalized_source,
|
|
164
|
+
options: options
|
|
165
|
+
)
|
|
166
|
+
return native_message if native_message
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
text_source = Enumerator.new do |yielder|
|
|
170
|
+
normalized_source.each do |item|
|
|
171
|
+
text = Streaming::Chunk.text_of(item)
|
|
172
|
+
yielder << text if text
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
Streaming::Stream.new(
|
|
176
|
+
adapter: adapter,
|
|
177
|
+
channel_id: channel_id,
|
|
178
|
+
thread_id: id,
|
|
179
|
+
placeholder: options[:placeholder],
|
|
180
|
+
update_interval: options[:update_interval],
|
|
181
|
+
ai_generated: options[:ai_generated] || false
|
|
182
|
+
).run(text_source)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def extract_recipient_team_id(raw)
|
|
186
|
+
return unless raw.is_a?(Hash)
|
|
187
|
+
|
|
188
|
+
team_id = raw[:team_id] || raw["team_id"]
|
|
189
|
+
team = raw[:team] || raw["team"]
|
|
190
|
+
user = raw[:user] || raw["user"]
|
|
191
|
+
team_id ||= team if team.is_a?(String)
|
|
192
|
+
team_id ||= team[:id] || team["id"] if team.is_a?(Hash)
|
|
193
|
+
team_id ||= user[:team_id] || user["team_id"] if user.is_a?(Hash)
|
|
194
|
+
team_id if team_id.is_a?(String) && !team_id.empty?
|
|
195
|
+
end
|
|
196
|
+
|
|
132
197
|
def state_key(suffix = nil)
|
|
133
198
|
base = "#{adapter.name}:#{channel_id}:#{id}"
|
|
134
199
|
suffix ? "#{base}:#{suffix}" : base
|
data/lib/chat_sdk/version.rb
CHANGED
|
@@ -12,6 +12,7 @@ module ChatSDK
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def call(env)
|
|
15
|
+
current_event = nil
|
|
15
16
|
request = Rack::Request.new(env) if defined?(Rack)
|
|
16
17
|
|
|
17
18
|
adapter.verify_request!(request || env)
|
|
@@ -20,19 +21,58 @@ module ChatSDK
|
|
|
20
21
|
return ack if ack
|
|
21
22
|
|
|
22
23
|
events = adapter.parse_events(request || env)
|
|
23
|
-
events.each
|
|
24
|
+
events.each do |event|
|
|
25
|
+
current_event = event
|
|
26
|
+
chat.dispatch(event, adapter_name: adapter_name)
|
|
27
|
+
end
|
|
24
28
|
|
|
25
|
-
response =
|
|
29
|
+
response = if webhook_response_accepts_request?
|
|
30
|
+
adapter.webhook_response(events, request || env)
|
|
31
|
+
else
|
|
32
|
+
adapter.webhook_response(events)
|
|
33
|
+
end
|
|
26
34
|
return response if response
|
|
27
35
|
|
|
28
36
|
[200, {"content-type" => "text/plain"}, [""]]
|
|
29
37
|
rescue ChatSDK::SignatureVerificationError => e
|
|
30
38
|
ChatSDK::Log.warn("Signature verification failed: #{e.message}")
|
|
31
39
|
[401, {"content-type" => "text/plain"}, ["Unauthorized"]]
|
|
40
|
+
rescue ChatSDK::HandlerExecutorError => e
|
|
41
|
+
chat.error_reporter.report(
|
|
42
|
+
e,
|
|
43
|
+
"Handler executor error",
|
|
44
|
+
**error_context(source: :executor, event: current_event)
|
|
45
|
+
)
|
|
46
|
+
# Platforms redeliver non-2xx responses; handlers did not run in this request, and workers deduplicate already-enqueued events.
|
|
47
|
+
[500, {"content-type" => "text/plain"}, [""]]
|
|
32
48
|
rescue => e
|
|
33
|
-
|
|
49
|
+
chat.error_reporter.report(
|
|
50
|
+
e,
|
|
51
|
+
"Webhook error",
|
|
52
|
+
**error_context(source: :webhook, event: current_event)
|
|
53
|
+
)
|
|
54
|
+
# Platforms retry non-2xx responses, which can repeat handler side effects.
|
|
55
|
+
# Logging and on_error surface failures without causing a redelivery.
|
|
34
56
|
[200, {"content-type" => "text/plain"}, [""]]
|
|
35
57
|
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def webhook_response_accepts_request?
|
|
62
|
+
parameters = adapter.method(:webhook_response).parameters
|
|
63
|
+
parameters.any? { |type, _| type == :rest } ||
|
|
64
|
+
parameters.count { |type, _| %i[req opt].include?(type) } > 1
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def error_context(source:, event:)
|
|
68
|
+
{
|
|
69
|
+
source: source,
|
|
70
|
+
adapter: adapter_name,
|
|
71
|
+
event_type: event&.type,
|
|
72
|
+
thread_id: event&.respond_to?(:thread_id) ? event.thread_id : nil,
|
|
73
|
+
channel_id: event&.respond_to?(:channel_id) ? event.channel_id : nil
|
|
74
|
+
}
|
|
75
|
+
end
|
|
36
76
|
end
|
|
37
77
|
end
|
|
38
78
|
end
|