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
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module ChatSDK
|
|
7
|
+
class EventSerializer
|
|
8
|
+
class << self
|
|
9
|
+
def dump(event)
|
|
10
|
+
data = common_event(event)
|
|
11
|
+
case event.type
|
|
12
|
+
when :mention, :subscribed_message, :direct_message, :message_updated
|
|
13
|
+
data["message"] = dump_message(event.message)
|
|
14
|
+
data["previous_message"] = dump_message(event.previous_message) if event.respond_to?(:previous_message) && event.previous_message
|
|
15
|
+
when :message_deleted
|
|
16
|
+
data.merge!(
|
|
17
|
+
"message_id" => event.message_id,
|
|
18
|
+
"previous_message" => dump_message(event.previous_message),
|
|
19
|
+
"deleted_at" => event.deleted_at&.iso8601
|
|
20
|
+
)
|
|
21
|
+
when :reaction
|
|
22
|
+
data.merge!("emoji" => event.emoji, "user_id" => event.user_id, "message_id" => event.message_id, "added" => event.added?)
|
|
23
|
+
when :action
|
|
24
|
+
data.merge!("action_id" => event.action_id, "value" => event.value, "values" => json_safe(event.values), "user" => dump_author(event.user), "trigger_id" => event.trigger_id)
|
|
25
|
+
when :slash_command
|
|
26
|
+
data.merge!("command" => event.command, "text" => event.text, "user_id" => event.user_id, "trigger_id" => event.trigger_id)
|
|
27
|
+
when :installed, :uninstalled
|
|
28
|
+
data.merge!(
|
|
29
|
+
"installation_id" => event.installation_id,
|
|
30
|
+
"team_id" => event.team_id,
|
|
31
|
+
"tenant_id" => event.tenant_id,
|
|
32
|
+
"conversation_id" => event.conversation_id,
|
|
33
|
+
"action" => event.action,
|
|
34
|
+
"user" => dump_author(event.user)
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
data
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def load(data)
|
|
41
|
+
data = data.transform_keys(&:to_s)
|
|
42
|
+
type = data.fetch("type").to_sym
|
|
43
|
+
common = {
|
|
44
|
+
thread_id: data["thread_id"],
|
|
45
|
+
channel_id: data["channel_id"],
|
|
46
|
+
platform: data["platform"].to_sym,
|
|
47
|
+
adapter_name: data["adapter_name"].to_sym,
|
|
48
|
+
raw: data["raw"],
|
|
49
|
+
timestamp: parse_time(data["timestamp"])
|
|
50
|
+
}
|
|
51
|
+
case type
|
|
52
|
+
when :mention, :subscribed_message, :direct_message
|
|
53
|
+
class_name = {mention: Events::Mention, subscribed_message: Events::SubscribedMessage, direct_message: Events::DirectMessage}.fetch(type)
|
|
54
|
+
class_name.new(message: load_message(data["message"]), **common)
|
|
55
|
+
when :message_updated
|
|
56
|
+
Events::MessageUpdated.new(message: load_message(data["message"]), previous_message: load_message(data["previous_message"]), **common)
|
|
57
|
+
when :message_deleted
|
|
58
|
+
Events::MessageDeleted.new(
|
|
59
|
+
message_id: data["message_id"],
|
|
60
|
+
previous_message: load_message(data["previous_message"]),
|
|
61
|
+
deleted_at: parse_time(data["deleted_at"]),
|
|
62
|
+
**common
|
|
63
|
+
)
|
|
64
|
+
when :reaction
|
|
65
|
+
Events::Reaction.new(emoji: data["emoji"], user_id: data["user_id"], message_id: data["message_id"], added: data["added"], **common)
|
|
66
|
+
when :action
|
|
67
|
+
Events::Action.new(action_id: data["action_id"], value: data["value"], values: data["values"], user: load_author(data["user"]), trigger_id: data["trigger_id"], **common)
|
|
68
|
+
when :slash_command
|
|
69
|
+
common.delete(:thread_id)
|
|
70
|
+
Events::SlashCommand.new(command: data["command"], text: data["text"], user_id: data["user_id"], trigger_id: data["trigger_id"], **common)
|
|
71
|
+
when :installed, :uninstalled
|
|
72
|
+
common.delete(:thread_id)
|
|
73
|
+
common.delete(:channel_id)
|
|
74
|
+
event_class = if type == :installed
|
|
75
|
+
Events::Installed
|
|
76
|
+
else
|
|
77
|
+
Events::Uninstalled
|
|
78
|
+
end
|
|
79
|
+
event_class.new(
|
|
80
|
+
installation_id: data["installation_id"],
|
|
81
|
+
team_id: data["team_id"],
|
|
82
|
+
tenant_id: data["tenant_id"],
|
|
83
|
+
conversation_id: data["conversation_id"],
|
|
84
|
+
action: data["action"],
|
|
85
|
+
user: load_author(data["user"]),
|
|
86
|
+
**common
|
|
87
|
+
)
|
|
88
|
+
else
|
|
89
|
+
raise ArgumentError, "unsupported queued event type: #{type}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def common_event(event)
|
|
96
|
+
{
|
|
97
|
+
"type" => event.type.to_s,
|
|
98
|
+
"platform" => event.platform.to_s,
|
|
99
|
+
"adapter_name" => event.adapter_name.to_s,
|
|
100
|
+
"thread_id" => event.respond_to?(:thread_id) ? event.thread_id : nil,
|
|
101
|
+
"channel_id" => event.respond_to?(:channel_id) ? event.channel_id : nil,
|
|
102
|
+
"raw" => json_safe(event.raw),
|
|
103
|
+
"timestamp" => event.timestamp&.iso8601
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def dump_message(message)
|
|
108
|
+
return unless message
|
|
109
|
+
|
|
110
|
+
{
|
|
111
|
+
"id" => message.id,
|
|
112
|
+
"text" => message.text,
|
|
113
|
+
"author" => dump_author(message.author),
|
|
114
|
+
"thread_id" => message.thread_id,
|
|
115
|
+
"channel_id" => message.channel_id,
|
|
116
|
+
"platform" => message.platform.to_s,
|
|
117
|
+
"attachments" => json_safe(message.attachments),
|
|
118
|
+
"links" => json_safe(message.links),
|
|
119
|
+
"reply_to" => dump_message(message.reply_to),
|
|
120
|
+
"subject" => json_safe(message.subject),
|
|
121
|
+
"raw" => json_safe(message.raw),
|
|
122
|
+
"timestamp" => message.timestamp&.iso8601,
|
|
123
|
+
"is_mention" => message.is_mention
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def load_message(data)
|
|
128
|
+
return unless data
|
|
129
|
+
|
|
130
|
+
data = data.transform_keys(&:to_s)
|
|
131
|
+
Message.new(
|
|
132
|
+
id: data["id"],
|
|
133
|
+
text: data["text"],
|
|
134
|
+
author: load_author(data["author"]),
|
|
135
|
+
thread_id: data["thread_id"],
|
|
136
|
+
channel_id: data["channel_id"],
|
|
137
|
+
platform: data["platform"].to_sym,
|
|
138
|
+
attachments: data["attachments"] || [],
|
|
139
|
+
links: data["links"] || [],
|
|
140
|
+
reply_to: load_message(data["reply_to"]),
|
|
141
|
+
subject: data["subject"],
|
|
142
|
+
raw: data["raw"],
|
|
143
|
+
timestamp: parse_time(data["timestamp"]),
|
|
144
|
+
is_mention: data["is_mention"]
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def dump_author(author)
|
|
149
|
+
return unless author
|
|
150
|
+
|
|
151
|
+
{
|
|
152
|
+
"id" => author.id,
|
|
153
|
+
"name" => author.name,
|
|
154
|
+
"platform" => author.platform.to_s,
|
|
155
|
+
"bot" => author.bot?,
|
|
156
|
+
"me" => author.me?,
|
|
157
|
+
"system" => author.system?,
|
|
158
|
+
"locale" => author.locale,
|
|
159
|
+
"email" => author.email,
|
|
160
|
+
"raw" => json_safe(author.raw)
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def load_author(data)
|
|
165
|
+
return unless data
|
|
166
|
+
|
|
167
|
+
data = data.transform_keys(&:to_s)
|
|
168
|
+
Author.new(
|
|
169
|
+
id: data["id"],
|
|
170
|
+
name: data["name"],
|
|
171
|
+
platform: data["platform"].to_sym,
|
|
172
|
+
bot: data["bot"],
|
|
173
|
+
me: data["me"],
|
|
174
|
+
system: data["system"],
|
|
175
|
+
locale: data["locale"],
|
|
176
|
+
email: data["email"],
|
|
177
|
+
raw: data["raw"]
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def parse_time(value)
|
|
182
|
+
Time.iso8601(value) if value
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def json_safe(value)
|
|
186
|
+
JSON.parse(JSON.generate(value))
|
|
187
|
+
rescue JSON::GeneratorError, TypeError
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
module ChatSDK
|
|
4
4
|
module Events
|
|
5
5
|
class Action < Base
|
|
6
|
-
attr_reader :action_id, :value, :user, :thread_id, :channel_id, :trigger_id
|
|
6
|
+
attr_reader :action_id, :value, :values, :user, :thread_id, :channel_id, :trigger_id
|
|
7
7
|
attr_accessor :thread
|
|
8
8
|
|
|
9
|
-
def initialize(action_id:, thread_id:, channel_id:, value: nil, user: nil, trigger_id: nil, **kwargs)
|
|
9
|
+
def initialize(action_id:, thread_id:, channel_id:, value: nil, values: nil, user: nil, trigger_id: nil, **kwargs)
|
|
10
10
|
super(type: :action, **kwargs)
|
|
11
11
|
@action_id = action_id
|
|
12
12
|
@value = value
|
|
13
|
+
@values = values
|
|
13
14
|
@user = user
|
|
14
15
|
@thread_id = thread_id
|
|
15
16
|
@channel_id = channel_id
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Events
|
|
5
|
+
class Installed < Base
|
|
6
|
+
attr_reader :installation_id, :team_id, :tenant_id, :conversation_id, :action, :user
|
|
7
|
+
|
|
8
|
+
def initialize(conversation_id:, action:, installation_id: nil, team_id: nil, tenant_id: nil, user: nil, **kwargs)
|
|
9
|
+
super(type: :installed, **kwargs)
|
|
10
|
+
@installation_id = installation_id
|
|
11
|
+
@team_id = team_id
|
|
12
|
+
@tenant_id = tenant_id
|
|
13
|
+
@conversation_id = conversation_id
|
|
14
|
+
@action = action
|
|
15
|
+
@user = user
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Events
|
|
5
|
+
class MessageDeleted < Base
|
|
6
|
+
attr_reader :message_id, :previous_message, :thread_id, :channel_id, :deleted_at
|
|
7
|
+
attr_accessor :thread
|
|
8
|
+
|
|
9
|
+
def initialize(message_id:, thread_id:, channel_id:, previous_message: nil, deleted_at: nil, **kwargs)
|
|
10
|
+
super(type: :message_deleted, **kwargs)
|
|
11
|
+
@message_id = message_id
|
|
12
|
+
@previous_message = previous_message
|
|
13
|
+
@thread_id = thread_id
|
|
14
|
+
@channel_id = channel_id
|
|
15
|
+
@deleted_at = deleted_at
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Events
|
|
5
|
+
class MessageUpdated < Base
|
|
6
|
+
attr_reader :message, :previous_message, :thread_id, :channel_id
|
|
7
|
+
|
|
8
|
+
def initialize(message:, thread_id:, channel_id:, previous_message: nil, **kwargs)
|
|
9
|
+
super(type: :message_updated, **kwargs)
|
|
10
|
+
@message = message
|
|
11
|
+
@previous_message = previous_message
|
|
12
|
+
@thread_id = thread_id
|
|
13
|
+
@channel_id = channel_id
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Events
|
|
5
|
+
class Uninstalled < Base
|
|
6
|
+
attr_reader :installation_id, :team_id, :tenant_id, :conversation_id, :action, :user
|
|
7
|
+
|
|
8
|
+
def initialize(conversation_id:, action:, installation_id: nil, team_id: nil, tenant_id: nil, user: nil, **kwargs)
|
|
9
|
+
super(type: :uninstalled, **kwargs)
|
|
10
|
+
@installation_id = installation_id
|
|
11
|
+
@team_id = team_id
|
|
12
|
+
@tenant_id = tenant_id
|
|
13
|
+
@conversation_id = conversation_id
|
|
14
|
+
@action = action
|
|
15
|
+
@user = user
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Format
|
|
5
|
+
module Mentions
|
|
6
|
+
class << self
|
|
7
|
+
def mask_code_spans(text)
|
|
8
|
+
characters = text.to_s.chars
|
|
9
|
+
masked = characters.dup
|
|
10
|
+
opening = nil
|
|
11
|
+
opening_length = nil
|
|
12
|
+
fence = false
|
|
13
|
+
index = 0
|
|
14
|
+
|
|
15
|
+
while index < characters.length
|
|
16
|
+
if characters[index] == "`"
|
|
17
|
+
finish = index + 1
|
|
18
|
+
finish += 1 while finish < characters.length && characters[finish] == "`"
|
|
19
|
+
length = finish - index
|
|
20
|
+
|
|
21
|
+
if opening
|
|
22
|
+
closes = fence ? length >= 3 : length == opening_length
|
|
23
|
+
if closes
|
|
24
|
+
blank_span(masked, opening, finish)
|
|
25
|
+
opening = nil
|
|
26
|
+
opening_length = nil
|
|
27
|
+
fence = false
|
|
28
|
+
else
|
|
29
|
+
blank_span(masked, index, finish)
|
|
30
|
+
end
|
|
31
|
+
else
|
|
32
|
+
opening = index
|
|
33
|
+
opening_length = length
|
|
34
|
+
fence = length >= 3
|
|
35
|
+
blank_span(masked, index, finish)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
index = finish
|
|
39
|
+
else
|
|
40
|
+
if opening
|
|
41
|
+
masked[index] = " " unless characters[index] == "\n" || characters[index] == "\r"
|
|
42
|
+
end
|
|
43
|
+
index += 1
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
masked[opening..] = characters[opening..] if opening
|
|
48
|
+
masked.join
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def blank_span(characters, first, finish)
|
|
54
|
+
(first...finish).each do |index|
|
|
55
|
+
characters[index] = " " unless characters[index] == "\n" || characters[index] == "\r"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module ChatSDK
|
|
7
|
+
class History
|
|
8
|
+
attr_reader :user, :thread, :channel
|
|
9
|
+
|
|
10
|
+
def initialize(chat)
|
|
11
|
+
@user = UserHistory.new(chat)
|
|
12
|
+
@thread = ThreadHistory.new(chat)
|
|
13
|
+
@channel = ChannelHistory.new(chat)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class UserHistory
|
|
17
|
+
def initialize(chat)
|
|
18
|
+
@chat = chat
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def append(thread, message, user_key: nil)
|
|
22
|
+
user_key ||= resolve_user_key(thread, message)
|
|
23
|
+
return nil unless user_key
|
|
24
|
+
|
|
25
|
+
entry = normalize_entry(thread, message, user_key)
|
|
26
|
+
entries = Array(@chat.state.get(storage_key(user_key)))
|
|
27
|
+
entries << entry
|
|
28
|
+
max = @chat.config.history_user[:max_per_user]
|
|
29
|
+
entries = entries.last(max) if max && max != false
|
|
30
|
+
@chat.state.set(storage_key(user_key), entries, ttl: @chat.config.history_user[:retention])
|
|
31
|
+
entry
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def list(user_key:, limit: 50, platforms: nil, thread_id: nil, roles: nil)
|
|
35
|
+
entries = Array(@chat.state.get(storage_key(user_key)))
|
|
36
|
+
entries = entries.select { |entry| Array(platforms).map(&:to_s).include?(entry["platform"].to_s) } if platforms
|
|
37
|
+
entries = entries.select { |entry| entry["thread_id"] == thread_id } if thread_id
|
|
38
|
+
entries = entries.select { |entry| Array(roles).map(&:to_s).include?(entry["role"].to_s) } if roles
|
|
39
|
+
limit ? entries.last(limit) : entries
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def count(user_key:)
|
|
43
|
+
Array(@chat.state.get(storage_key(user_key))).length
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def delete(user_key:)
|
|
47
|
+
count = Array(@chat.state.get(storage_key(user_key))).length
|
|
48
|
+
@chat.state.delete(storage_key(user_key))
|
|
49
|
+
{deleted: count}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_prompt_entries(entries)
|
|
53
|
+
entries.map { |entry| {role: entry["role"], content: entry["text"]} }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def resolve_user_key(thread, message)
|
|
59
|
+
resolver = @chat.config.history_user[:identity]
|
|
60
|
+
return resolver.call(thread, message) if resolver
|
|
61
|
+
message.author.email if message.is_a?(Message) && message.author&.email
|
|
62
|
+
rescue => e
|
|
63
|
+
ChatSDK::Log.warn("History identity resolver failed: #{e.message}")
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def normalize_entry(thread, message, user_key)
|
|
68
|
+
if message.is_a?(Message)
|
|
69
|
+
{
|
|
70
|
+
"id" => SecureRandom.uuid,
|
|
71
|
+
"platform_message_id" => message.id,
|
|
72
|
+
"user_key" => user_key,
|
|
73
|
+
"role" => message.author&.bot? ? "assistant" : "user",
|
|
74
|
+
"text" => message.text.to_s,
|
|
75
|
+
"platform" => message.platform.to_s,
|
|
76
|
+
"thread_id" => thread.id,
|
|
77
|
+
"timestamp" => (message.timestamp || Time.now).iso8601
|
|
78
|
+
}
|
|
79
|
+
else
|
|
80
|
+
data = message.transform_keys(&:to_sym)
|
|
81
|
+
{
|
|
82
|
+
"id" => SecureRandom.uuid,
|
|
83
|
+
"platform_message_id" => data[:platform_message_id] || data[:id],
|
|
84
|
+
"user_key" => user_key,
|
|
85
|
+
"role" => (data[:role] || "assistant").to_s,
|
|
86
|
+
"text" => data[:text].to_s,
|
|
87
|
+
"platform" => (data[:platform] || thread.adapter.name).to_s,
|
|
88
|
+
"thread_id" => thread.id,
|
|
89
|
+
"timestamp" => (data[:timestamp] || Time.now).iso8601
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def storage_key(user_key)
|
|
95
|
+
"chat_sdk:history:user:#{user_key}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class ThreadHistory
|
|
100
|
+
def initialize(chat)
|
|
101
|
+
@chat = chat
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def list(thread, cursor: nil, limit: 50)
|
|
105
|
+
unless thread.is_a?(ChatSDK::Thread)
|
|
106
|
+
raise ArgumentError, "thread history requires a ChatSDK::Thread"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
messages, next_cursor = thread.adapter.fetch_messages(
|
|
110
|
+
channel_id: thread.channel_id,
|
|
111
|
+
thread_id: thread.id,
|
|
112
|
+
cursor: cursor,
|
|
113
|
+
limit: limit
|
|
114
|
+
)
|
|
115
|
+
{messages: messages, next_cursor: next_cursor}
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
class ChannelHistory
|
|
120
|
+
def initialize(chat)
|
|
121
|
+
@chat = chat
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def list_messages(channel, cursor: nil, limit: 50)
|
|
125
|
+
unless channel.is_a?(ChatSDK::Channel)
|
|
126
|
+
raise ArgumentError, "channel history requires a ChatSDK::Channel"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
messages, next_cursor = channel.adapter.fetch_channel_messages(
|
|
130
|
+
channel_id: channel.id,
|
|
131
|
+
cursor: cursor,
|
|
132
|
+
limit: limit
|
|
133
|
+
)
|
|
134
|
+
{messages: messages, next_cursor: next_cursor}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def list_threads(channel, cursor: nil, limit: 50)
|
|
138
|
+
unless channel.is_a?(ChatSDK::Channel)
|
|
139
|
+
raise ArgumentError, "channel history requires a ChatSDK::Channel"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
threads, next_cursor = channel.adapter.list_threads(
|
|
143
|
+
channel_id: channel.id,
|
|
144
|
+
cursor: cursor,
|
|
145
|
+
limit: limit
|
|
146
|
+
)
|
|
147
|
+
{threads: threads, next_cursor: next_cursor}
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
data/lib/chat_sdk/message.rb
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
module ChatSDK
|
|
4
4
|
class Message
|
|
5
5
|
attr_reader :id, :text, :author, :thread_id, :channel_id,
|
|
6
|
-
:platform, :attachments, :raw, :timestamp
|
|
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: [],
|
|
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
|
|
@@ -14,8 +16,12 @@ module ChatSDK
|
|
|
14
16
|
@channel_id = channel_id
|
|
15
17
|
@platform = platform
|
|
16
18
|
@attachments = attachments
|
|
19
|
+
@links = links
|
|
20
|
+
@reply_to = reply_to
|
|
21
|
+
@subject = subject
|
|
17
22
|
@raw = raw
|
|
18
23
|
@timestamp = timestamp
|
|
24
|
+
@is_mention = is_mention
|
|
19
25
|
end
|
|
20
26
|
|
|
21
27
|
def ==(other)
|
|
@@ -32,6 +32,22 @@ module ChatSDK
|
|
|
32
32
|
@children << ChatSDK::Cards::Node.new(:input, attributes: attrs, children: ctx.nodes)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
def date_input(id:, label:, initial_value: nil, placeholder: nil, optional: false)
|
|
36
|
+
attrs = {id: id, label: label, input_type: :date, optional: optional}
|
|
37
|
+
attrs[:initial_value] = initial_value if initial_value
|
|
38
|
+
attrs[:placeholder] = placeholder if placeholder
|
|
39
|
+
@children << ChatSDK::Cards::Node.new(:input, attributes: attrs)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def number_input(id:, label:, min: nil, max: nil, initial_value: nil, placeholder: nil, decimal: false, optional: false)
|
|
43
|
+
attrs = {id: id, label: label, input_type: :number, decimal: decimal, optional: optional}
|
|
44
|
+
attrs[:min] = min unless min.nil?
|
|
45
|
+
attrs[:max] = max unless max.nil?
|
|
46
|
+
attrs[:initial_value] = initial_value unless initial_value.nil?
|
|
47
|
+
attrs[:placeholder] = placeholder if placeholder
|
|
48
|
+
@children << ChatSDK::Cards::Node.new(:input, attributes: attrs)
|
|
49
|
+
end
|
|
50
|
+
|
|
35
51
|
def static_text(content)
|
|
36
52
|
@children << ChatSDK::Cards::Node.new(:text, attributes: {content: content})
|
|
37
53
|
end
|
|
@@ -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?
|
data/lib/chat_sdk/state/base.rb
CHANGED
|
@@ -42,6 +42,22 @@ module ChatSDK
|
|
|
42
42
|
def set_if_absent(key, value, ttl: nil)
|
|
43
43
|
raise NotImplementedError
|
|
44
44
|
end
|
|
45
|
+
|
|
46
|
+
def enqueue(key, value, max_size:, drop: :drop_oldest)
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def drain_queue(key)
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def queue_depth(key)
|
|
55
|
+
raise NotImplementedError
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def extend_lock(key, owner:, ttl:)
|
|
59
|
+
raise NotImplementedError
|
|
60
|
+
end
|
|
45
61
|
end
|
|
46
62
|
end
|
|
47
63
|
end
|
|
@@ -9,6 +9,7 @@ module ChatSDK
|
|
|
9
9
|
@expirations = {}
|
|
10
10
|
@subscriptions = Set.new
|
|
11
11
|
@locks = {}
|
|
12
|
+
@queues = {}
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def subscribe(thread_id)
|
|
@@ -47,6 +48,16 @@ module ChatSDK
|
|
|
47
48
|
end
|
|
48
49
|
end
|
|
49
50
|
|
|
51
|
+
def extend_lock(key, owner:, ttl:)
|
|
52
|
+
@mutex.synchronize do
|
|
53
|
+
expire_if_needed(key)
|
|
54
|
+
return false unless @locks[key] && @locks[key][:owner] == owner
|
|
55
|
+
|
|
56
|
+
@locks[key][:expires_at] = Time.now.to_f + ttl
|
|
57
|
+
true
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
50
61
|
def get(key)
|
|
51
62
|
@mutex.synchronize do
|
|
52
63
|
expire_if_needed(key)
|
|
@@ -80,12 +91,32 @@ module ChatSDK
|
|
|
80
91
|
end
|
|
81
92
|
end
|
|
82
93
|
|
|
94
|
+
def enqueue(key, value, max_size:, drop: :drop_oldest)
|
|
95
|
+
@mutex.synchronize do
|
|
96
|
+
queue = (@queues[key] ||= [])
|
|
97
|
+
return queue.length if queue.length >= max_size && drop.to_sym == :drop_newest
|
|
98
|
+
|
|
99
|
+
queue << value
|
|
100
|
+
queue.shift while queue.length > max_size
|
|
101
|
+
queue.length
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def drain_queue(key)
|
|
106
|
+
@mutex.synchronize { @queues.delete(key) || [] }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def queue_depth(key)
|
|
110
|
+
@mutex.synchronize { Array(@queues[key]).length }
|
|
111
|
+
end
|
|
112
|
+
|
|
83
113
|
def clear
|
|
84
114
|
@mutex.synchronize do
|
|
85
115
|
@store.clear
|
|
86
116
|
@expirations.clear
|
|
87
117
|
@subscriptions.clear
|
|
88
118
|
@locks.clear
|
|
119
|
+
@queues.clear
|
|
89
120
|
end
|
|
90
121
|
end
|
|
91
122
|
|
|
@@ -3,6 +3,8 @@
|
|
|
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
10
|
def initialize(adapter:, channel_id:, thread_id:, placeholder: nil, update_interval: 0.5)
|
|
@@ -50,10 +52,12 @@ module ChatSDK
|
|
|
50
52
|
return if @buffer.empty?
|
|
51
53
|
|
|
52
54
|
if @message_id && adapter.supports?(:edit_messages)
|
|
53
|
-
|
|
55
|
+
call_adapter(
|
|
56
|
+
:edit_message,
|
|
54
57
|
channel_id: channel_id,
|
|
55
58
|
message_id: @message_id,
|
|
56
|
-
message: PostableMessage.new(text: @buffer.dup)
|
|
59
|
+
message: PostableMessage.new(text: @buffer.dup),
|
|
60
|
+
thread_id: thread_id
|
|
57
61
|
)
|
|
58
62
|
elsif !@message_id
|
|
59
63
|
result = adapter.post_message(
|