chat_sdk 1.1.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 +19 -5
- data/lib/chat_sdk/adapter/invocation.rb +18 -0
- data/lib/chat_sdk/ai/conversation_scope.rb +11 -1
- data/lib/chat_sdk/ai/stream_handler.rb +3 -3
- data/lib/chat_sdk/ai/tool_builder.rb +2 -1
- data/lib/chat_sdk/ai/tool_executor.rb +32 -15
- data/lib/chat_sdk/author.rb +6 -1
- data/lib/chat_sdk/cards/actions_context.rb +26 -4
- data/lib/chat_sdk/cards/builder.rb +30 -4
- data/lib/chat_sdk/cards/node.rb +4 -0
- data/lib/chat_sdk/cards/renderer.rb +3 -0
- data/lib/chat_sdk/chat.rb +80 -5
- data/lib/chat_sdk/config.rb +34 -2
- data/lib/chat_sdk/dispatcher.rb +107 -28
- data/lib/chat_sdk/error_reporter.rb +36 -0
- data/lib/chat_sdk/errors.rb +1 -0
- data/lib/chat_sdk/event_registry.rb +7 -2
- data/lib/chat_sdk/event_serializer.rb +66 -6
- data/lib/chat_sdk/events/action.rb +3 -2
- 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/installed.rb +19 -0
- data/lib/chat_sdk/events/member_joined_channel.rb +16 -0
- data/lib/chat_sdk/events/uninstalled.rb +19 -0
- data/lib/chat_sdk/format/mentions.rb +61 -0
- data/lib/chat_sdk/message.rb +4 -1
- data/lib/chat_sdk/modals/builder.rb +39 -4
- data/lib/chat_sdk/postable_message.rb +3 -2
- 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 +48 -8
- 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/testing/fake_adapter.rb +6 -6
- data/lib/chat_sdk/thread.rb +86 -15
- data/lib/chat_sdk/version.rb +1 -1
- data/lib/chat_sdk/webhook/endpoint.rb +45 -2
- data/lib/chat_sdk.rb +2 -2
- metadata +17 -1
data/lib/chat_sdk/message.rb
CHANGED
|
@@ -4,9 +4,11 @@ module ChatSDK
|
|
|
4
4
|
class Message
|
|
5
5
|
attr_reader :id, :text, :author, :thread_id, :channel_id,
|
|
6
6
|
:platform, :attachments, :links, :reply_to, :subject, :raw, :timestamp
|
|
7
|
+
attr_accessor :is_mention
|
|
7
8
|
|
|
8
9
|
def initialize(id:, text:, author:, thread_id:, channel_id:, platform:,
|
|
9
|
-
attachments: [], links: [], reply_to: nil, subject: nil, raw: nil,
|
|
10
|
+
attachments: [], links: [], reply_to: nil, subject: nil, raw: nil,
|
|
11
|
+
timestamp: nil, is_mention: nil)
|
|
10
12
|
@id = id
|
|
11
13
|
@text = text
|
|
12
14
|
@author = author
|
|
@@ -19,6 +21,7 @@ module ChatSDK
|
|
|
19
21
|
@subject = subject
|
|
20
22
|
@raw = raw
|
|
21
23
|
@timestamp = timestamp
|
|
24
|
+
@is_mention = is_mention
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
def ==(other)
|
|
@@ -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
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module ChatSDK
|
|
4
4
|
class PostableMessage
|
|
5
|
-
attr_reader :text, :card, :attachments, :metadata
|
|
5
|
+
attr_reader :text, :card, :attachments, :metadata, :ai_generated
|
|
6
6
|
|
|
7
|
-
def initialize(text: nil, card: nil, attachments: [], metadata: {})
|
|
7
|
+
def initialize(text: nil, card: nil, attachments: [], metadata: {}, ai_generated: false)
|
|
8
8
|
raise ArgumentError, "text or card required" if text.nil? && card.nil?
|
|
9
9
|
@text = text
|
|
10
10
|
@card = card
|
|
11
11
|
@attachments = attachments
|
|
12
12
|
@metadata = metadata
|
|
13
|
+
@ai_generated = ai_generated
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def card?
|
|
@@ -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
|
|
@@ -3,39 +3,74 @@
|
|
|
3
3
|
module ChatSDK
|
|
4
4
|
module Streaming
|
|
5
5
|
class Stream
|
|
6
|
+
include ChatSDK::Adapter::Invocation
|
|
7
|
+
|
|
6
8
|
attr_reader :adapter, :channel_id, :thread_id, :update_interval
|
|
7
9
|
|
|
8
|
-
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)
|
|
9
11
|
@adapter = adapter
|
|
10
12
|
@channel_id = channel_id
|
|
11
13
|
@thread_id = thread_id
|
|
12
14
|
@placeholder = placeholder
|
|
13
15
|
@update_interval = update_interval
|
|
16
|
+
@ai_generated = ai_generated
|
|
14
17
|
@buffer = +""
|
|
18
|
+
@delivered_text = +""
|
|
15
19
|
@message_id = nil
|
|
20
|
+
@message = nil
|
|
16
21
|
@last_flush_at = nil
|
|
17
22
|
end
|
|
18
23
|
|
|
19
24
|
def <<(chunk)
|
|
20
|
-
|
|
25
|
+
text = Chunk.text_of(Chunk.normalize(chunk))
|
|
26
|
+
return self unless text
|
|
27
|
+
|
|
28
|
+
@buffer << text
|
|
21
29
|
maybe_flush
|
|
22
30
|
self
|
|
23
31
|
end
|
|
24
32
|
|
|
25
|
-
def run(&block)
|
|
33
|
+
def run(source = nil, &block)
|
|
26
34
|
post_placeholder if @placeholder
|
|
27
|
-
|
|
35
|
+
if source
|
|
36
|
+
source.each { |chunk| self << chunk }
|
|
37
|
+
elsif block
|
|
38
|
+
block.call(self)
|
|
39
|
+
end
|
|
28
40
|
flush
|
|
41
|
+
message_with_text(@message, @delivered_text)
|
|
29
42
|
end
|
|
30
43
|
|
|
31
44
|
private
|
|
32
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
|
+
|
|
33
66
|
def post_placeholder
|
|
34
67
|
result = adapter.post_message(
|
|
35
68
|
channel_id: channel_id,
|
|
36
|
-
message: PostableMessage.new(text: @placeholder),
|
|
69
|
+
message: PostableMessage.new(text: @placeholder, ai_generated: @ai_generated),
|
|
37
70
|
thread_id: thread_id
|
|
38
71
|
)
|
|
72
|
+
@delivered_text = @placeholder.dup
|
|
73
|
+
@message = result if result.is_a?(ChatSDK::Message)
|
|
39
74
|
@message_id = result.id if result.respond_to?(:id)
|
|
40
75
|
end
|
|
41
76
|
|
|
@@ -50,18 +85,23 @@ module ChatSDK
|
|
|
50
85
|
return if @buffer.empty?
|
|
51
86
|
|
|
52
87
|
if @message_id && adapter.supports?(:edit_messages)
|
|
53
|
-
|
|
88
|
+
call_adapter(
|
|
89
|
+
:edit_message,
|
|
54
90
|
channel_id: channel_id,
|
|
55
91
|
message_id: @message_id,
|
|
56
|
-
message: PostableMessage.new(text: @buffer.dup)
|
|
92
|
+
message: PostableMessage.new(text: @buffer.dup, ai_generated: @ai_generated),
|
|
93
|
+
thread_id: thread_id
|
|
57
94
|
)
|
|
95
|
+
@delivered_text = @buffer.dup
|
|
58
96
|
elsif !@message_id
|
|
59
97
|
result = adapter.post_message(
|
|
60
98
|
channel_id: channel_id,
|
|
61
|
-
message: PostableMessage.new(text: @buffer.dup),
|
|
99
|
+
message: PostableMessage.new(text: @buffer.dup, ai_generated: @ai_generated),
|
|
62
100
|
thread_id: thread_id
|
|
63
101
|
)
|
|
102
|
+
@message = result if result.is_a?(ChatSDK::Message)
|
|
64
103
|
@message_id = result.id if result.respond_to?(:id)
|
|
104
|
+
@delivered_text = @buffer.dup
|
|
65
105
|
end
|
|
66
106
|
|
|
67
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
|
|
@@ -50,7 +50,7 @@ module ChatSDK
|
|
|
50
50
|
)
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
def edit_message(channel_id:, message_id:, message
|
|
53
|
+
def edit_message(channel_id:, message_id:, message:, **_options)
|
|
54
54
|
record = RecordedCall.new(:edit_message, channel_id: channel_id, message_id: message_id, message: message)
|
|
55
55
|
@edited_messages << record
|
|
56
56
|
record
|
|
@@ -68,7 +68,7 @@ module ChatSDK
|
|
|
68
68
|
record
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
def delete_message(channel_id:, message_id
|
|
71
|
+
def delete_message(channel_id:, message_id:, **_options)
|
|
72
72
|
record = RecordedCall.new(:delete_message, channel_id: channel_id, message_id: message_id)
|
|
73
73
|
@deleted_messages << record
|
|
74
74
|
record
|
|
@@ -86,13 +86,13 @@ module ChatSDK
|
|
|
86
86
|
record
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
-
def add_reaction(channel_id:, message_id:, emoji
|
|
89
|
+
def add_reaction(channel_id:, message_id:, emoji:, **_options)
|
|
90
90
|
record = RecordedCall.new(:add_reaction, channel_id: channel_id, message_id: message_id, emoji: emoji)
|
|
91
91
|
@reactions_added << record
|
|
92
92
|
record
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
-
def remove_reaction(channel_id:, message_id:, emoji
|
|
95
|
+
def remove_reaction(channel_id:, message_id:, emoji:, **_options)
|
|
96
96
|
record = RecordedCall.new(:remove_reaction, channel_id: channel_id, message_id: message_id, emoji: emoji)
|
|
97
97
|
@reactions_removed << record
|
|
98
98
|
record
|
|
@@ -122,8 +122,8 @@ module ChatSDK
|
|
|
122
122
|
record
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
-
def start_typing(channel_id:, thread_id: nil)
|
|
126
|
-
record = RecordedCall.new(:start_typing, channel_id: channel_id, thread_id: thread_id)
|
|
125
|
+
def start_typing(channel_id:, thread_id: nil, message_id: nil)
|
|
126
|
+
record = RecordedCall.new(:start_typing, channel_id: channel_id, thread_id: thread_id, message_id: message_id)
|
|
127
127
|
@typing_started << record
|
|
128
128
|
record
|
|
129
129
|
end
|
data/lib/chat_sdk/thread.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module ChatSDK
|
|
4
4
|
class Thread
|
|
5
|
+
include Adapter::Invocation
|
|
6
|
+
|
|
5
7
|
attr_reader :id, :channel_id, :adapter, :chat, :current_message
|
|
6
8
|
|
|
7
9
|
def initialize(id:, channel_id:, adapter:, chat:, current_message: nil)
|
|
@@ -25,6 +27,8 @@ module ChatSDK
|
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def post(content)
|
|
30
|
+
return handle_stream(content.stream, content.to_stream_options) if content.is_a?(StreamingPlan)
|
|
31
|
+
|
|
28
32
|
message = PostableMessage.from(content)
|
|
29
33
|
adapter.post_message(channel_id: channel_id, message: message, thread_id: id)
|
|
30
34
|
end
|
|
@@ -51,41 +55,54 @@ module ChatSDK
|
|
|
51
55
|
adapter.mark_as_read(channel_id: channel_id, thread_id: id, message_id: message_id, message: target.is_a?(ChatSDK::Message) ? target : nil)
|
|
52
56
|
end
|
|
53
57
|
|
|
58
|
+
def start_typing
|
|
59
|
+
adapter.start_typing(channel_id: channel_id, thread_id: id, message_id: current_message&.id)
|
|
60
|
+
end
|
|
61
|
+
|
|
54
62
|
def post_ephemeral(content, user_id:)
|
|
55
63
|
message = PostableMessage.from(content)
|
|
56
64
|
adapter.post_ephemeral(channel_id: channel_id, user_id: user_id, message: message, thread_id: id)
|
|
57
65
|
end
|
|
58
66
|
|
|
59
|
-
def post_stream(placeholder: nil, &block)
|
|
60
|
-
stream
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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))
|
|
68
85
|
end
|
|
69
86
|
|
|
70
87
|
def edit(message_id, content)
|
|
71
88
|
message = PostableMessage.from(content)
|
|
72
|
-
|
|
89
|
+
call_adapter(:edit_message, channel_id: channel_id, message_id: message_id, message: message, thread_id: id)
|
|
73
90
|
end
|
|
74
91
|
|
|
75
92
|
def delete(message_id)
|
|
76
|
-
|
|
93
|
+
call_adapter(:delete_message, channel_id: channel_id, message_id: message_id, thread_id: id)
|
|
77
94
|
end
|
|
78
95
|
|
|
79
96
|
def react(message_id, emoji)
|
|
80
|
-
|
|
97
|
+
call_adapter(:add_reaction, channel_id: channel_id, message_id: message_id, emoji: emoji, thread_id: id)
|
|
81
98
|
end
|
|
82
99
|
|
|
83
100
|
def unreact(message_id, emoji)
|
|
84
|
-
|
|
101
|
+
call_adapter(:remove_reaction, channel_id: channel_id, message_id: message_id, emoji: emoji, thread_id: id)
|
|
85
102
|
end
|
|
86
103
|
|
|
87
|
-
def post_ai_stream(enumerable, placeholder: "Thinking...")
|
|
88
|
-
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)
|
|
89
106
|
end
|
|
90
107
|
|
|
91
108
|
def upload(io:, filename:, comment: nil)
|
|
@@ -123,6 +140,60 @@ module ChatSDK
|
|
|
123
140
|
|
|
124
141
|
private
|
|
125
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
|
+
|
|
126
197
|
def state_key(suffix = nil)
|
|
127
198
|
base = "#{adapter.name}:#{channel_id}:#{id}"
|
|
128
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,16 +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
|
|
28
|
+
|
|
29
|
+
response = if webhook_response_accepts_request?
|
|
30
|
+
adapter.webhook_response(events, request || env)
|
|
31
|
+
else
|
|
32
|
+
adapter.webhook_response(events)
|
|
33
|
+
end
|
|
34
|
+
return response if response
|
|
24
35
|
|
|
25
36
|
[200, {"content-type" => "text/plain"}, [""]]
|
|
26
37
|
rescue ChatSDK::SignatureVerificationError => e
|
|
27
38
|
ChatSDK::Log.warn("Signature verification failed: #{e.message}")
|
|
28
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"}, [""]]
|
|
29
48
|
rescue => e
|
|
30
|
-
|
|
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.
|
|
31
56
|
[200, {"content-type" => "text/plain"}, [""]]
|
|
32
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
|
|
33
76
|
end
|
|
34
77
|
end
|
|
35
78
|
end
|
data/lib/chat_sdk.rb
CHANGED
|
@@ -17,8 +17,8 @@ module ChatSDK
|
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def card(title: nil, subtitle: nil, width: nil, &block)
|
|
21
|
-
Cards::Builder.new(title: title, subtitle: subtitle, width: width, &block).build
|
|
20
|
+
def card(title: nil, subtitle: nil, width: nil, image_url: nil, &block)
|
|
21
|
+
Cards::Builder.new(title: title, subtitle: subtitle, width: width, image_url: image_url, &block).build
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
end
|