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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dc45b8149b772db87ae0793f070a3f068353ef09a177d59c3526eb0ec37d1fb9
|
|
4
|
+
data.tar.gz: 16eb68d587eb53a43337f22dd2cd7d5b564383d892ecda9b64b77b5ae45a05a0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a6cb0121846bf8a75b849f0d6bedfc48601c738dba99c3ad838ecdcb3b31fd7b4a27cd16bfa75cee66a4f70e62b62ae1c4730546b22f430e72d1af1bf6219c2
|
|
7
|
+
data.tar.gz: 602d33c5bba332dac427d61e8566c396f10c057e6b17a3422d1ab4846e4d7e8857ece123b50d2d357e310072f119bee00436db593e6138e46aabc34ba65f777c
|
|
@@ -22,6 +22,10 @@ module ChatSDK
|
|
|
22
22
|
nil
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
def webhook_response(_events)
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
25
29
|
def parse_events(rack_request)
|
|
26
30
|
raise NotImplementedError
|
|
27
31
|
end
|
|
@@ -31,12 +35,22 @@ module ChatSDK
|
|
|
31
35
|
raise NotImplementedError
|
|
32
36
|
end
|
|
33
37
|
|
|
34
|
-
def
|
|
38
|
+
def reply_message(channel_id:, message_id:, message:, thread_id: nil)
|
|
39
|
+
require_capability!(:replies)
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def mark_as_read(channel_id:, message_id:, thread_id: nil, message: nil)
|
|
44
|
+
require_capability!(:read_receipts)
|
|
45
|
+
raise NotImplementedError
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def edit_message(channel_id:, message_id:, message:, thread_id: nil)
|
|
35
49
|
require_capability!(:edit_messages)
|
|
36
50
|
raise NotImplementedError
|
|
37
51
|
end
|
|
38
52
|
|
|
39
|
-
def delete_message(channel_id:, message_id:)
|
|
53
|
+
def delete_message(channel_id:, message_id:, thread_id: nil)
|
|
40
54
|
require_capability!(:delete_messages)
|
|
41
55
|
raise NotImplementedError
|
|
42
56
|
end
|
|
@@ -51,12 +65,12 @@ module ChatSDK
|
|
|
51
65
|
raise NotImplementedError
|
|
52
66
|
end
|
|
53
67
|
|
|
54
|
-
def add_reaction(channel_id:, message_id:, emoji:)
|
|
68
|
+
def add_reaction(channel_id:, message_id:, emoji:, thread_id: nil)
|
|
55
69
|
require_capability!(:reactions)
|
|
56
70
|
raise NotImplementedError
|
|
57
71
|
end
|
|
58
72
|
|
|
59
|
-
def remove_reaction(channel_id:, message_id:, emoji:)
|
|
73
|
+
def remove_reaction(channel_id:, message_id:, emoji:, thread_id: nil)
|
|
60
74
|
require_capability!(:reactions)
|
|
61
75
|
raise NotImplementedError
|
|
62
76
|
end
|
|
@@ -71,16 +85,30 @@ module ChatSDK
|
|
|
71
85
|
raise NotImplementedError
|
|
72
86
|
end
|
|
73
87
|
|
|
88
|
+
def fetch_channel_messages(channel_id:, cursor: nil, limit: 50)
|
|
89
|
+
require_capability!(:message_history)
|
|
90
|
+
fetch_messages(channel_id: channel_id, cursor: cursor, limit: limit)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def list_threads(channel_id:, cursor: nil, limit: 50)
|
|
94
|
+
require_capability!(:threads)
|
|
95
|
+
raise NotImplementedError
|
|
96
|
+
end
|
|
97
|
+
|
|
74
98
|
def open_modal(trigger_id:, modal:)
|
|
75
99
|
require_capability!(:modals)
|
|
76
100
|
raise NotImplementedError
|
|
77
101
|
end
|
|
78
102
|
|
|
79
|
-
def start_typing(channel_id:, thread_id: nil)
|
|
103
|
+
def start_typing(channel_id:, thread_id: nil, message_id: nil)
|
|
80
104
|
require_capability!(:typing_indicator)
|
|
81
105
|
raise NotImplementedError
|
|
82
106
|
end
|
|
83
107
|
|
|
108
|
+
def concurrency_scope(_event)
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
84
112
|
def mention(user_id)
|
|
85
113
|
raise NotImplementedError
|
|
86
114
|
end
|
|
@@ -7,7 +7,7 @@ module ChatSDK
|
|
|
7
7
|
edit_messages delete_messages ephemeral_messages
|
|
8
8
|
file_uploads reactions modals typing_indicator
|
|
9
9
|
streaming_edit threads direct_messages
|
|
10
|
-
scheduled_messages message_history
|
|
10
|
+
scheduled_messages message_history replies read_receipts
|
|
11
11
|
].freeze
|
|
12
12
|
|
|
13
13
|
def self.included(base)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Adapter
|
|
5
|
+
module Invocation
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def call_adapter(method_name, **kwargs)
|
|
9
|
+
params = adapter.method(method_name).parameters
|
|
10
|
+
accepts_thread_id = params.any? do |type, name|
|
|
11
|
+
type == :keyrest || (%i[key keyreq].include?(type) && name == :thread_id)
|
|
12
|
+
end
|
|
13
|
+
kwargs.delete(:thread_id) unless accepts_thread_id
|
|
14
|
+
adapter.public_send(method_name, **kwargs)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module AI
|
|
5
|
+
module ConversationScope
|
|
6
|
+
THREAD_KEY = :chat_sdk_ai_conversation_scope
|
|
7
|
+
ACTIVE_KEY = :chat_sdk_ai_conversation_scope_active
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def current
|
|
11
|
+
::Thread.current[THREAD_KEY]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def active?
|
|
15
|
+
!!::Thread.current[ACTIVE_KEY]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def with(thread)
|
|
19
|
+
previous = current
|
|
20
|
+
previous_active = active?
|
|
21
|
+
::Thread.current[THREAD_KEY] = scope_for(thread)
|
|
22
|
+
::Thread.current[ACTIVE_KEY] = true
|
|
23
|
+
yield
|
|
24
|
+
ensure
|
|
25
|
+
::Thread.current[THREAD_KEY] = previous
|
|
26
|
+
::Thread.current[ACTIVE_KEY] = previous_active
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def scope_for(value)
|
|
30
|
+
return if value.nil?
|
|
31
|
+
return value.transform_keys(&:to_sym) if value.is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
scope = {
|
|
34
|
+
adapter_name: value.adapter.name.to_sym,
|
|
35
|
+
channel_id: value.respond_to?(:channel_id) ? value.channel_id.to_s : value.id.to_s,
|
|
36
|
+
thread_id: value.is_a?(ChatSDK::Thread) ? value.id.to_s : nil
|
|
37
|
+
}
|
|
38
|
+
scope[:message_id] = value.current_message.id if value.respond_to?(:current_message) && value.current_message
|
|
39
|
+
scope
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -10,7 +10,7 @@ module ChatSDK
|
|
|
10
10
|
def to_ai_messages(messages, include_names: false, &transform)
|
|
11
11
|
messages
|
|
12
12
|
.sort_by { |m| m.timestamp || m.id }
|
|
13
|
-
.reject { |m|
|
|
13
|
+
.reject { |m| blank_message?(m) }
|
|
14
14
|
.filter_map { |m| convert_message(m, include_names: include_names, &transform) }
|
|
15
15
|
end
|
|
16
16
|
|
|
@@ -19,16 +19,22 @@ module ChatSDK
|
|
|
19
19
|
def convert_message(message, include_names: false)
|
|
20
20
|
role = message.author&.bot? ? ROLE_ASSISTANT : ROLE_USER
|
|
21
21
|
|
|
22
|
-
content = message.text
|
|
22
|
+
content = message.text.to_s
|
|
23
23
|
if include_names && role == ROLE_USER && message.author
|
|
24
24
|
content = "[#{message.author.name}]: #{content}"
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
result = {role: role, content: content}
|
|
28
28
|
|
|
29
|
-
if message.attachments&.any?
|
|
30
|
-
parts = [
|
|
31
|
-
|
|
29
|
+
if message.attachments&.any? || message.links&.any?
|
|
30
|
+
parts = []
|
|
31
|
+
parts << {type: "text", text: content} unless content.strip.empty?
|
|
32
|
+
Array(message.links).each do |link|
|
|
33
|
+
url = link.is_a?(Hash) ? (link[:url] || link["url"]) : link.to_s
|
|
34
|
+
title = link.is_a?(Hash) ? (link[:title] || link["title"]) : nil
|
|
35
|
+
parts << {type: "text", text: title ? "[#{title}](#{url})" : url}
|
|
36
|
+
end
|
|
37
|
+
Array(message.attachments).each do |att|
|
|
32
38
|
parts << attachment_to_part(att)
|
|
33
39
|
end
|
|
34
40
|
result[:content] = parts
|
|
@@ -38,6 +44,11 @@ module ChatSDK
|
|
|
38
44
|
result
|
|
39
45
|
end
|
|
40
46
|
|
|
47
|
+
def blank_message?(message)
|
|
48
|
+
text_blank = message.text.nil? || message.text.strip.empty?
|
|
49
|
+
text_blank && !message.attachments&.any? && !message.links&.any?
|
|
50
|
+
end
|
|
51
|
+
|
|
41
52
|
def attachment_to_part(attachment)
|
|
42
53
|
if attachment.is_a?(Hash)
|
|
43
54
|
mime = attachment[:mime_type] || attachment[:content_type] || "application/octet-stream"
|
|
@@ -72,6 +72,7 @@ module ChatSDK
|
|
|
72
72
|
properties: {
|
|
73
73
|
adapter_name: {type: "string", description: "Adapter name"},
|
|
74
74
|
channel_id: {type: "string", description: "Channel ID"},
|
|
75
|
+
thread_id: {type: "string", description: "Thread ID (optional)"},
|
|
75
76
|
message_id: {type: "string", description: "Message ID to edit"},
|
|
76
77
|
text: {type: "string", description: "New message text"}
|
|
77
78
|
},
|
|
@@ -86,6 +87,7 @@ module ChatSDK
|
|
|
86
87
|
properties: {
|
|
87
88
|
adapter_name: {type: "string", description: "Adapter name"},
|
|
88
89
|
channel_id: {type: "string", description: "Channel ID"},
|
|
90
|
+
thread_id: {type: "string", description: "Thread ID (optional)"},
|
|
89
91
|
message_id: {type: "string", description: "Message ID to delete"}
|
|
90
92
|
},
|
|
91
93
|
required: %w[adapter_name channel_id message_id]
|
|
@@ -99,6 +101,7 @@ module ChatSDK
|
|
|
99
101
|
properties: {
|
|
100
102
|
adapter_name: {type: "string", description: "Adapter name"},
|
|
101
103
|
channel_id: {type: "string", description: "Channel ID"},
|
|
104
|
+
thread_id: {type: "string", description: "Thread ID (optional)"},
|
|
102
105
|
message_id: {type: "string", description: "Message ID"},
|
|
103
106
|
emoji: {type: "string", description: "Emoji name (e.g., 'thumbsup')"}
|
|
104
107
|
},
|
|
@@ -113,6 +116,7 @@ module ChatSDK
|
|
|
113
116
|
properties: {
|
|
114
117
|
adapter_name: {type: "string", description: "Adapter name"},
|
|
115
118
|
channel_id: {type: "string", description: "Channel ID"},
|
|
119
|
+
thread_id: {type: "string", description: "Thread ID (optional)"},
|
|
116
120
|
message_id: {type: "string", description: "Message ID"},
|
|
117
121
|
emoji: {type: "string", description: "Emoji name"}
|
|
118
122
|
},
|
|
@@ -127,7 +131,8 @@ module ChatSDK
|
|
|
127
131
|
properties: {
|
|
128
132
|
adapter_name: {type: "string", description: "Adapter name"},
|
|
129
133
|
channel_id: {type: "string", description: "Channel ID"},
|
|
130
|
-
thread_id: {type: "string", description: "Thread ID (optional)"}
|
|
134
|
+
thread_id: {type: "string", description: "Thread ID (optional)"},
|
|
135
|
+
message_id: {type: "string", description: "Current message ID (optional)"}
|
|
131
136
|
},
|
|
132
137
|
required: %w[adapter_name channel_id]
|
|
133
138
|
},
|
|
@@ -145,10 +150,21 @@ module ChatSDK
|
|
|
145
150
|
tool_names = PRESETS[@preset]
|
|
146
151
|
tool_names.each_with_object({}) do |name, tools|
|
|
147
152
|
defn = TOOL_DEFINITIONS[name].dup
|
|
148
|
-
defn[:requires_approval] =
|
|
153
|
+
defn[:requires_approval] = approval_required?(name, defn)
|
|
149
154
|
tools[name] = defn
|
|
150
155
|
end
|
|
151
156
|
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
def approval_required?(name, definition)
|
|
161
|
+
return false if definition[:read_only] || @require_approval == false
|
|
162
|
+
return true unless @require_approval.is_a?(Hash)
|
|
163
|
+
|
|
164
|
+
@require_approval.fetch(name) do
|
|
165
|
+
@require_approval.fetch(name.to_s, true)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
152
168
|
end
|
|
153
169
|
end
|
|
154
170
|
end
|
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
module ChatSDK
|
|
4
4
|
module AI
|
|
5
5
|
class ToolExecutor
|
|
6
|
-
def initialize(chat:)
|
|
6
|
+
def initialize(chat:, scope: nil, strict_scope: false)
|
|
7
7
|
@chat = chat
|
|
8
|
+
@scope = (scope == false) ? false : (ConversationScope.scope_for(scope) || ConversationScope.current&.dup)
|
|
9
|
+
@created_in_handler = ConversationScope.active?
|
|
10
|
+
validate_scope!(@scope) if @scope
|
|
11
|
+
@strict_scope = strict_scope
|
|
12
|
+
@warned_unscoped = {}
|
|
8
13
|
end
|
|
9
14
|
|
|
10
15
|
def execute(tool_name, arguments)
|
|
@@ -13,6 +18,7 @@ module ChatSDK
|
|
|
13
18
|
|
|
14
19
|
args = arguments.transform_keys(&:to_sym)
|
|
15
20
|
adapter_name = args[:adapter_name].to_sym
|
|
21
|
+
guard_scope!(tool_name, adapter_name, args)
|
|
16
22
|
|
|
17
23
|
send(:"execute_#{tool_name}", adapter_name, args)
|
|
18
24
|
end
|
|
@@ -40,54 +46,102 @@ module ChatSDK
|
|
|
40
46
|
|
|
41
47
|
def execute_post_message(adapter_name, args)
|
|
42
48
|
channel = @chat.channel(args[:channel_id], adapter_name: adapter_name)
|
|
49
|
+
message = ChatSDK::PostableMessage.new(text: args[:text], ai_generated: true)
|
|
43
50
|
if args[:thread_id]
|
|
44
51
|
thread = channel.thread(args[:thread_id])
|
|
45
|
-
result = thread.post(
|
|
52
|
+
result = thread.post(message)
|
|
46
53
|
else
|
|
47
|
-
result = channel.post(
|
|
54
|
+
result = channel.post(message)
|
|
48
55
|
end
|
|
49
56
|
{id: result.id, text: args[:text]}
|
|
50
57
|
end
|
|
51
58
|
|
|
52
59
|
def execute_send_direct_message(adapter_name, args)
|
|
53
60
|
dm_channel = @chat.open_dm(args[:user_id], adapter_name: adapter_name)
|
|
54
|
-
result = dm_channel.post(args[:text])
|
|
61
|
+
result = dm_channel.post(ChatSDK::PostableMessage.new(text: args[:text], ai_generated: true))
|
|
55
62
|
{id: result.id, channel_id: dm_channel.id}
|
|
56
63
|
end
|
|
57
64
|
|
|
58
65
|
def execute_edit_message(adapter_name, args)
|
|
59
|
-
thread =
|
|
60
|
-
thread.edit(args[:message_id], args[:text])
|
|
66
|
+
thread = target_thread(adapter_name, args)
|
|
67
|
+
thread.edit(args[:message_id], ChatSDK::PostableMessage.new(text: args[:text], ai_generated: true))
|
|
61
68
|
{success: true}
|
|
62
69
|
end
|
|
63
70
|
|
|
64
71
|
def execute_delete_message(adapter_name, args)
|
|
65
|
-
thread =
|
|
72
|
+
thread = target_thread(adapter_name, args)
|
|
66
73
|
thread.delete(args[:message_id])
|
|
67
74
|
{success: true}
|
|
68
75
|
end
|
|
69
76
|
|
|
70
77
|
def execute_add_reaction(adapter_name, args)
|
|
71
|
-
thread =
|
|
78
|
+
thread = target_thread(adapter_name, args)
|
|
72
79
|
thread.react(args[:message_id], args[:emoji])
|
|
73
80
|
{success: true}
|
|
74
81
|
end
|
|
75
82
|
|
|
76
83
|
def execute_remove_reaction(adapter_name, args)
|
|
77
|
-
thread =
|
|
84
|
+
thread = target_thread(adapter_name, args)
|
|
78
85
|
thread.unreact(args[:message_id], args[:emoji])
|
|
79
86
|
{success: true}
|
|
80
87
|
end
|
|
81
88
|
|
|
82
89
|
def execute_start_typing(adapter_name, args)
|
|
83
90
|
adapter = @chat.adapter(adapter_name)
|
|
84
|
-
|
|
91
|
+
scoped_message_id = @scope.is_a?(Hash) ? @scope[:message_id] : nil
|
|
92
|
+
message_id = args[:message_id] || ConversationScope.current&.[](:message_id) || scoped_message_id
|
|
93
|
+
adapter.start_typing(channel_id: args[:channel_id], thread_id: args[:thread_id], message_id: message_id)
|
|
85
94
|
{success: true}
|
|
86
95
|
end
|
|
87
96
|
|
|
88
97
|
def serialize_messages(messages)
|
|
89
98
|
messages.map { |m| {id: m.id, text: m.text, author: m.author&.name, timestamp: m.timestamp} }
|
|
90
99
|
end
|
|
100
|
+
|
|
101
|
+
def target_thread(adapter_name, args)
|
|
102
|
+
channel = @chat.channel(args[:channel_id], adapter_name: adapter_name)
|
|
103
|
+
channel.thread(args[:thread_id] || args[:channel_id])
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def guard_scope!(tool_name, adapter_name, args)
|
|
107
|
+
return if @scope == false
|
|
108
|
+
|
|
109
|
+
scope = @scope || ConversationScope.current
|
|
110
|
+
unless scope
|
|
111
|
+
if ConversationScope.active? || @created_in_handler
|
|
112
|
+
raise ChatSDK::Error, "AI tool call requires an explicit scope when run inside a handler without a conversation"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
unless @warned_unscoped[tool_name]
|
|
116
|
+
ChatSDK::Log.warn("AI tool ran without a conversation scope; pass scope: to create_executor to confine access")
|
|
117
|
+
@warned_unscoped[tool_name] = true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
return
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
return if tool_name == :send_direct_message
|
|
124
|
+
|
|
125
|
+
target_channel = args[:channel_id].to_s
|
|
126
|
+
target_thread = args[:thread_id]&.to_s
|
|
127
|
+
same_channel = adapter_name == scope.fetch(:adapter_name).to_sym && target_channel == scope.fetch(:channel_id).to_s
|
|
128
|
+
scope_is_channel = scope[:thread_id].nil?
|
|
129
|
+
in_scope = same_channel && (!@strict_scope || scope_is_channel || target_thread == scope[:thread_id].to_s)
|
|
130
|
+
return if in_scope
|
|
131
|
+
|
|
132
|
+
target = [adapter_name, target_channel, target_thread].compact.join(":")
|
|
133
|
+
active = [scope[:adapter_name], scope[:channel_id], scope[:thread_id]].compact.join(":")
|
|
134
|
+
raise ChatSDK::Error, "AI tool call blocked: executor is scoped to #{active.inspect}, but targeted #{target.inspect}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def validate_scope!(scope)
|
|
138
|
+
%i[channel_id thread_id].each do |key|
|
|
139
|
+
next if key == :thread_id && scope[key].nil?
|
|
140
|
+
next unless scope.key?(key) && scope[key].to_s.empty?
|
|
141
|
+
|
|
142
|
+
raise ChatSDK::ConfigurationError, "AI tool scope #{key} must not be empty"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
91
145
|
end
|
|
92
146
|
end
|
|
93
147
|
end
|
data/lib/chat_sdk/ai.rb
CHANGED
|
@@ -11,8 +11,8 @@ module ChatSDK
|
|
|
11
11
|
ToolBuilder.new(preset: preset, require_approval: require_approval).build
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def create_executor(chat:)
|
|
15
|
-
ToolExecutor.new(chat: chat)
|
|
14
|
+
def create_executor(chat:, scope: nil, strict_scope: false)
|
|
15
|
+
ToolExecutor.new(chat: chat, scope: scope, strict_scope: strict_scope)
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
end
|
data/lib/chat_sdk/author.rb
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module ChatSDK
|
|
4
4
|
class Author
|
|
5
|
-
attr_reader :id, :name, :platform, :locale, :raw
|
|
5
|
+
attr_reader :id, :name, :platform, :locale, :email, :raw
|
|
6
6
|
|
|
7
|
-
def initialize(id:, name:, platform:, bot: false, locale: nil, raw: nil)
|
|
7
|
+
def initialize(id:, name:, platform:, bot: false, me: false, system: false, locale: nil, email: nil, raw: nil)
|
|
8
8
|
@id = id
|
|
9
9
|
@name = name
|
|
10
10
|
@platform = platform
|
|
11
11
|
@bot = bot
|
|
12
|
+
@me = !!me
|
|
13
|
+
@system = system
|
|
12
14
|
@locale = locale
|
|
15
|
+
@email = email
|
|
13
16
|
@raw = raw
|
|
14
17
|
end
|
|
15
18
|
|
|
@@ -17,6 +20,14 @@ module ChatSDK
|
|
|
17
20
|
@bot
|
|
18
21
|
end
|
|
19
22
|
|
|
23
|
+
def me?
|
|
24
|
+
@me
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def system?
|
|
28
|
+
@system
|
|
29
|
+
end
|
|
30
|
+
|
|
20
31
|
def ==(other)
|
|
21
32
|
other.is_a?(Author) && id == other.id && platform == other.platform
|
|
22
33
|
end
|
|
@@ -9,15 +9,21 @@ module ChatSDK
|
|
|
9
9
|
@nodes = []
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def button(text, id:, style: nil, value: nil)
|
|
12
|
+
def button(text, id:, style: nil, value: nil, tooltip: nil, visible_to: nil)
|
|
13
13
|
attrs = {text: text, id: id}
|
|
14
14
|
attrs[:style] = style if style
|
|
15
15
|
attrs[:value] = value if value
|
|
16
|
+
attrs[:tooltip] = tooltip if tooltip
|
|
17
|
+
attrs[:visible_to] = visible_to if visible_to
|
|
16
18
|
@nodes << Node.new(:button, attributes: attrs)
|
|
17
19
|
end
|
|
18
20
|
|
|
19
|
-
def link_button(text, url:)
|
|
20
|
-
|
|
21
|
+
def link_button(text, url:, id: nil, tooltip: nil, visible_to: nil)
|
|
22
|
+
attrs = {text: text, url: url}
|
|
23
|
+
attrs[:id] = id if id
|
|
24
|
+
attrs[:tooltip] = tooltip if tooltip
|
|
25
|
+
attrs[:visible_to] = visible_to if visible_to
|
|
26
|
+
@nodes << Node.new(:link_button, attributes: attrs)
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
def select(id:, placeholder: nil, &block)
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
module ChatSDK
|
|
4
4
|
module Cards
|
|
5
5
|
class Builder
|
|
6
|
-
|
|
6
|
+
RESERVED_TEXT_INPUT_IDS = %w[actionId value msteams].freeze
|
|
7
|
+
|
|
8
|
+
def initialize(title: nil, subtitle: nil, width: nil, &block)
|
|
7
9
|
@title = title
|
|
8
10
|
@subtitle = subtitle
|
|
11
|
+
@width = width
|
|
9
12
|
@children = []
|
|
10
13
|
instance_eval(&block) if block
|
|
11
14
|
end
|
|
@@ -14,6 +17,7 @@ module ChatSDK
|
|
|
14
17
|
attrs = {}
|
|
15
18
|
attrs[:title] = @title if @title
|
|
16
19
|
attrs[:subtitle] = @subtitle if @subtitle
|
|
20
|
+
attrs[:width] = @width if @width
|
|
17
21
|
Node.new(:card, attributes: attrs, children: @children)
|
|
18
22
|
end
|
|
19
23
|
|
|
@@ -29,6 +33,17 @@ module ChatSDK
|
|
|
29
33
|
@children << Node.new(:image, attributes: {url: url, alt: alt})
|
|
30
34
|
end
|
|
31
35
|
|
|
36
|
+
def text_input(id:, label:, placeholder: nil, multiline: false, optional: false, value: nil)
|
|
37
|
+
if RESERVED_TEXT_INPUT_IDS.include?(id.to_s)
|
|
38
|
+
raise ArgumentError, "Text input id #{id.inspect} is reserved"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
attrs = {id: id, label: label, multiline: multiline, optional: optional}
|
|
42
|
+
attrs[:placeholder] = placeholder if placeholder
|
|
43
|
+
attrs[:value] = value unless value.nil?
|
|
44
|
+
@children << Node.new(:text_input, attributes: attrs)
|
|
45
|
+
end
|
|
46
|
+
|
|
32
47
|
def fields(&block)
|
|
33
48
|
ctx = FieldsContext.new
|
|
34
49
|
ctx.instance_eval(&block)
|
|
@@ -47,6 +62,28 @@ module ChatSDK
|
|
|
47
62
|
ctx.instance_eval(&block)
|
|
48
63
|
@children << Node.new(:actions, children: ctx.nodes)
|
|
49
64
|
end
|
|
65
|
+
|
|
66
|
+
def table(headers:, rows:, align: nil, caption: nil, page_size: nil, widths: nil, grid_lines: nil, grid_style: nil, vertical_align: nil)
|
|
67
|
+
attrs = {headers: headers, rows: rows}
|
|
68
|
+
attrs[:align] = align if align
|
|
69
|
+
attrs[:caption] = caption if caption
|
|
70
|
+
attrs[:page_size] = page_size if page_size
|
|
71
|
+
attrs[:widths] = widths if widths
|
|
72
|
+
attrs[:grid_lines] = grid_lines unless grid_lines.nil?
|
|
73
|
+
attrs[:grid_style] = grid_style if grid_style
|
|
74
|
+
attrs[:vertical_align] = vertical_align if vertical_align
|
|
75
|
+
@children << Node.new(:table, attributes: attrs)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def chart(title:, type:, segments: nil, categories: nil, series: nil, x_label: nil, y_label: nil)
|
|
79
|
+
attrs = {title: title, chart_type: type}
|
|
80
|
+
attrs[:segments] = segments if segments
|
|
81
|
+
attrs[:categories] = categories if categories
|
|
82
|
+
attrs[:series] = series if series
|
|
83
|
+
attrs[:x_label] = x_label if x_label
|
|
84
|
+
attrs[:y_label] = y_label if y_label
|
|
85
|
+
@children << Node.new(:chart, attributes: attrs)
|
|
86
|
+
end
|
|
50
87
|
end
|
|
51
88
|
end
|
|
52
89
|
end
|
data/lib/chat_sdk/cards/node.rb
CHANGED
|
@@ -31,11 +31,47 @@ module ChatSDK
|
|
|
31
31
|
["#{node.attributes[:label]}: #{node.attributes[:value]}"]
|
|
32
32
|
when :button, :link_button
|
|
33
33
|
[node.attributes[:text]]
|
|
34
|
+
when :text_input
|
|
35
|
+
[node.attributes[:label]]
|
|
36
|
+
when :table
|
|
37
|
+
table_text(node)
|
|
38
|
+
when :chart
|
|
39
|
+
chart_text(node)
|
|
34
40
|
else
|
|
35
41
|
collect_text(node.children)
|
|
36
42
|
end
|
|
37
43
|
end
|
|
38
44
|
end
|
|
45
|
+
|
|
46
|
+
def table_text(node)
|
|
47
|
+
headers = Array(node.attributes[:headers]).map(&:to_s)
|
|
48
|
+
rows = Array(node.attributes[:rows]).map { |row| Array(row).map(&:to_s) }
|
|
49
|
+
lines = []
|
|
50
|
+
lines << node.attributes[:caption].to_s if node.attributes[:caption]
|
|
51
|
+
lines << headers.join(" | ") unless headers.empty?
|
|
52
|
+
lines.concat(rows.map { |row| row.join(" | ") })
|
|
53
|
+
lines
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def chart_text(node)
|
|
57
|
+
lines = [node.attributes[:title].to_s]
|
|
58
|
+
if node.attributes[:chart_type].to_sym == :pie
|
|
59
|
+
lines.concat(Array(node.attributes[:segments]).map do |segment|
|
|
60
|
+
"#{value_for(segment, :label)}: #{value_for(segment, :value)}"
|
|
61
|
+
end)
|
|
62
|
+
else
|
|
63
|
+
Array(node.attributes[:series]).each do |series|
|
|
64
|
+
data = value_for(series, :data) || []
|
|
65
|
+
points = data.map { |point| "#{value_for(point, :label)}=#{value_for(point, :value)}" }
|
|
66
|
+
lines << "#{value_for(series, :name)}: #{points.join(", ")}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
lines
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def value_for(hash, key)
|
|
73
|
+
hash[key] || hash[key.to_s]
|
|
74
|
+
end
|
|
39
75
|
end
|
|
40
76
|
end
|
|
41
77
|
end
|
|
@@ -15,7 +15,10 @@ module ChatSDK
|
|
|
15
15
|
when :actions then render_actions(node)
|
|
16
16
|
when :button then "[#{node.attributes[:text]}]"
|
|
17
17
|
when :link_button then "[#{node.attributes[:text]}](#{node.attributes[:url]})"
|
|
18
|
+
when :text_input then node.attributes[:label]
|
|
18
19
|
when :select then "_#{node.attributes[:placeholder] || "Select"}_"
|
|
20
|
+
when :table then render_table(node)
|
|
21
|
+
when :chart then render_chart(node)
|
|
19
22
|
else ""
|
|
20
23
|
end
|
|
21
24
|
end
|
|
@@ -44,6 +47,23 @@ module ChatSDK
|
|
|
44
47
|
def render_actions(node)
|
|
45
48
|
node.children.map { |c| render(c) }.join(" | ")
|
|
46
49
|
end
|
|
50
|
+
|
|
51
|
+
def render_table(node)
|
|
52
|
+
headers = Array(node.attributes[:headers]).map(&:to_s)
|
|
53
|
+
rows = Array(node.attributes[:rows]).map { |row| Array(row).map(&:to_s) }
|
|
54
|
+
parts = []
|
|
55
|
+
parts << node.attributes[:caption].to_s if node.attributes[:caption]
|
|
56
|
+
unless headers.empty?
|
|
57
|
+
parts << "| #{headers.join(" | ")} |"
|
|
58
|
+
parts << "| #{headers.map { "---" }.join(" | ")} |"
|
|
59
|
+
end
|
|
60
|
+
parts.concat(rows.map { |row| "| #{row.join(" | ")} |" })
|
|
61
|
+
parts.join("\n")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def render_chart(node)
|
|
65
|
+
node.fallback_text
|
|
66
|
+
end
|
|
47
67
|
end
|
|
48
68
|
end
|
|
49
69
|
end
|
data/lib/chat_sdk/channel.rb
CHANGED
|
@@ -19,6 +19,14 @@ module ChatSDK
|
|
|
19
19
|
ChatSDK::Thread.new(id: thread_id, channel_id: id, adapter: adapter, chat: chat)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
def messages(cursor: nil, limit: 50)
|
|
23
|
+
adapter.fetch_channel_messages(channel_id: id, cursor: cursor, limit: limit)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def threads(cursor: nil, limit: 50)
|
|
27
|
+
adapter.list_threads(channel_id: id, cursor: cursor, limit: limit)
|
|
28
|
+
end
|
|
29
|
+
|
|
22
30
|
def ==(other)
|
|
23
31
|
other.is_a?(Channel) && id == other.id
|
|
24
32
|
end
|