chat_sdk 0.1.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6bcc60c6b4443d14e0afb167cc7a6b8f5bbe60663fd97e275f87435313cf0705
4
- data.tar.gz: 4f3820e9f8ae54789a15b89f351e035b942d741d7670accd509adf86e693b4c8
3
+ metadata.gz: a8db1850adc2b313ad351d222eed5ed588f6c46b122bcf9f6f8f3eba445c19d5
4
+ data.tar.gz: 5fd507fa0cb224b1e204f5eeaedee74379769f4f9c5a8055af4549ecb1bbc97f
5
5
  SHA512:
6
- metadata.gz: b60a52d3a1c878b74e57e57ee3ed2b40191f60ce2db45c3c80ec762324f54b738f6d4ea448f362f02f3e0a84026656e55090c1feef4abf96cfe6b67ba2c19675
7
- data.tar.gz: f13771e3df25d44835659b04d424b6410eb2c9f8ffd5a53e3aaa24cd111446ca10b53a6f88adccccb6b261a7289597f0ee7c166795b6abb6fe69fccfc2fa7c4d
6
+ metadata.gz: 8af8ee31c13cbc5acccef41282ad5717ad2b2ff149fed4bcec95bfdb9fc6c422560d4d55c965e992cc1df1194232813b64f62653cc4c158ff51b4226768f871c
7
+ data.tar.gz: 0d1132b3d7f19f466aeb29c0d5827af6abc7702a917c90b1d679675239d24fda092b7d273830f881dd0b2e9693a1a975b6f3e6f1ff22f4c4652dbb876489915e
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module AI
5
+ class Converter
6
+ ROLE_USER = "user"
7
+ ROLE_ASSISTANT = "assistant"
8
+
9
+ class << self
10
+ def to_ai_messages(messages, include_names: false, &transform)
11
+ messages
12
+ .sort_by { |m| m.timestamp || m.id }
13
+ .reject { |m| m.text.nil? || m.text.strip.empty? }
14
+ .filter_map { |m| convert_message(m, include_names: include_names, &transform) }
15
+ end
16
+
17
+ private
18
+
19
+ def convert_message(message, include_names: false)
20
+ role = message.author&.bot? ? ROLE_ASSISTANT : ROLE_USER
21
+
22
+ content = message.text
23
+ if include_names && role == ROLE_USER && message.author
24
+ content = "[#{message.author.name}]: #{content}"
25
+ end
26
+
27
+ result = {role: role, content: content}
28
+
29
+ if message.attachments&.any?
30
+ parts = [{type: "text", text: content}]
31
+ message.attachments.each do |att|
32
+ parts << attachment_to_part(att)
33
+ end
34
+ result[:content] = parts
35
+ end
36
+
37
+ result = yield(result, message) if block_given?
38
+ result
39
+ end
40
+
41
+ def attachment_to_part(attachment)
42
+ if attachment.is_a?(Hash)
43
+ mime = attachment[:mime_type] || attachment[:content_type] || "application/octet-stream"
44
+ if mime.start_with?("image/")
45
+ {type: "image", url: attachment[:url], media_type: mime}
46
+ else
47
+ {type: "file", url: attachment[:url], filename: attachment[:filename], media_type: mime}
48
+ end
49
+ else
50
+ {type: "text", text: attachment.to_s}
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module AI
5
+ class StreamHandler
6
+ def self.stream_to_thread(thread, enumerable, placeholder: "Thinking...")
7
+ thread.post_stream(placeholder: placeholder) do |stream|
8
+ enumerable.each { |chunk| stream << chunk.to_s }
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module AI
5
+ class ToolBuilder
6
+ PRESETS = {
7
+ reader: %i[fetch_messages fetch_thread],
8
+ messenger: %i[fetch_messages fetch_thread post_message send_direct_message add_reaction start_typing],
9
+ moderator: %i[fetch_messages fetch_thread post_message send_direct_message add_reaction
10
+ edit_message delete_message remove_reaction start_typing]
11
+ }.freeze
12
+
13
+ TOOL_DEFINITIONS = {
14
+ fetch_messages: {
15
+ description: "Fetch recent messages from a channel or thread",
16
+ parameters: {
17
+ type: "object",
18
+ properties: {
19
+ adapter_name: {type: "string", description: "Adapter name (e.g., 'slack', 'teams')"},
20
+ channel_id: {type: "string", description: "Channel ID"},
21
+ thread_id: {type: "string", description: "Thread ID (optional)"},
22
+ limit: {type: "integer", description: "Max messages to fetch", default: 20}
23
+ },
24
+ required: %w[adapter_name channel_id]
25
+ },
26
+ read_only: true
27
+ },
28
+ fetch_thread: {
29
+ description: "Fetch all messages in a specific thread",
30
+ parameters: {
31
+ type: "object",
32
+ properties: {
33
+ adapter_name: {type: "string", description: "Adapter name"},
34
+ channel_id: {type: "string", description: "Channel ID"},
35
+ thread_id: {type: "string", description: "Thread ID"}
36
+ },
37
+ required: %w[adapter_name channel_id thread_id]
38
+ },
39
+ read_only: true
40
+ },
41
+ post_message: {
42
+ description: "Post a message to a channel or thread",
43
+ parameters: {
44
+ type: "object",
45
+ properties: {
46
+ adapter_name: {type: "string", description: "Adapter name"},
47
+ channel_id: {type: "string", description: "Channel ID"},
48
+ thread_id: {type: "string", description: "Thread ID (optional, for replies)"},
49
+ text: {type: "string", description: "Message text (markdown)"}
50
+ },
51
+ required: %w[adapter_name channel_id text]
52
+ },
53
+ read_only: false
54
+ },
55
+ send_direct_message: {
56
+ description: "Send a direct message to a user",
57
+ parameters: {
58
+ type: "object",
59
+ properties: {
60
+ adapter_name: {type: "string", description: "Adapter name"},
61
+ user_id: {type: "string", description: "User ID"},
62
+ text: {type: "string", description: "Message text"}
63
+ },
64
+ required: %w[adapter_name user_id text]
65
+ },
66
+ read_only: false
67
+ },
68
+ edit_message: {
69
+ description: "Edit an existing message",
70
+ parameters: {
71
+ type: "object",
72
+ properties: {
73
+ adapter_name: {type: "string", description: "Adapter name"},
74
+ channel_id: {type: "string", description: "Channel ID"},
75
+ message_id: {type: "string", description: "Message ID to edit"},
76
+ text: {type: "string", description: "New message text"}
77
+ },
78
+ required: %w[adapter_name channel_id message_id text]
79
+ },
80
+ read_only: false
81
+ },
82
+ delete_message: {
83
+ description: "Delete a message",
84
+ parameters: {
85
+ type: "object",
86
+ properties: {
87
+ adapter_name: {type: "string", description: "Adapter name"},
88
+ channel_id: {type: "string", description: "Channel ID"},
89
+ message_id: {type: "string", description: "Message ID to delete"}
90
+ },
91
+ required: %w[adapter_name channel_id message_id]
92
+ },
93
+ read_only: false
94
+ },
95
+ add_reaction: {
96
+ description: "Add an emoji reaction to a message",
97
+ parameters: {
98
+ type: "object",
99
+ properties: {
100
+ adapter_name: {type: "string", description: "Adapter name"},
101
+ channel_id: {type: "string", description: "Channel ID"},
102
+ message_id: {type: "string", description: "Message ID"},
103
+ emoji: {type: "string", description: "Emoji name (e.g., 'thumbsup')"}
104
+ },
105
+ required: %w[adapter_name channel_id message_id emoji]
106
+ },
107
+ read_only: false
108
+ },
109
+ remove_reaction: {
110
+ description: "Remove an emoji reaction from a message",
111
+ parameters: {
112
+ type: "object",
113
+ properties: {
114
+ adapter_name: {type: "string", description: "Adapter name"},
115
+ channel_id: {type: "string", description: "Channel ID"},
116
+ message_id: {type: "string", description: "Message ID"},
117
+ emoji: {type: "string", description: "Emoji name"}
118
+ },
119
+ required: %w[adapter_name channel_id message_id emoji]
120
+ },
121
+ read_only: false
122
+ },
123
+ start_typing: {
124
+ description: "Show typing indicator in a channel",
125
+ parameters: {
126
+ type: "object",
127
+ properties: {
128
+ adapter_name: {type: "string", description: "Adapter name"},
129
+ channel_id: {type: "string", description: "Channel ID"},
130
+ thread_id: {type: "string", description: "Thread ID (optional)"}
131
+ },
132
+ required: %w[adapter_name channel_id]
133
+ },
134
+ read_only: false
135
+ }
136
+ }.freeze
137
+
138
+ def initialize(preset: :messenger, require_approval: true)
139
+ @preset = preset.to_sym
140
+ @require_approval = require_approval
141
+ raise ChatSDK::ConfigurationError, "Unknown preset: #{@preset}" unless PRESETS.key?(@preset)
142
+ end
143
+
144
+ def build
145
+ tool_names = PRESETS[@preset]
146
+ tool_names.each_with_object({}) do |name, tools|
147
+ defn = TOOL_DEFINITIONS[name].dup
148
+ defn[:requires_approval] = @require_approval && !defn[:read_only]
149
+ tools[name] = defn
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module AI
5
+ class ToolExecutor
6
+ def initialize(chat:)
7
+ @chat = chat
8
+ end
9
+
10
+ def execute(tool_name, arguments)
11
+ tool_name = tool_name.to_sym
12
+ raise ChatSDK::Error, "Unknown tool: #{tool_name}" unless ToolBuilder::TOOL_DEFINITIONS.key?(tool_name)
13
+
14
+ args = arguments.transform_keys(&:to_sym)
15
+ adapter_name = args[:adapter_name].to_sym
16
+
17
+ send(:"execute_#{tool_name}", adapter_name, args)
18
+ end
19
+
20
+ private
21
+
22
+ def execute_fetch_messages(adapter_name, args)
23
+ channel = @chat.channel(args[:channel_id], adapter_name: adapter_name)
24
+ messages, _cursor = channel.adapter.fetch_messages(
25
+ channel_id: args[:channel_id],
26
+ thread_id: args[:thread_id],
27
+ limit: args[:limit] || 20
28
+ )
29
+ serialize_messages(messages)
30
+ end
31
+
32
+ def execute_fetch_thread(adapter_name, args)
33
+ channel = @chat.channel(args[:channel_id], adapter_name: adapter_name)
34
+ messages, _cursor = channel.adapter.fetch_messages(
35
+ channel_id: args[:channel_id],
36
+ thread_id: args[:thread_id]
37
+ )
38
+ serialize_messages(messages)
39
+ end
40
+
41
+ def execute_post_message(adapter_name, args)
42
+ channel = @chat.channel(args[:channel_id], adapter_name: adapter_name)
43
+ if args[:thread_id]
44
+ thread = channel.thread(args[:thread_id])
45
+ result = thread.post(args[:text])
46
+ else
47
+ result = channel.post(args[:text])
48
+ end
49
+ {id: result.id, text: args[:text]}
50
+ end
51
+
52
+ def execute_send_direct_message(adapter_name, args)
53
+ dm_channel = @chat.open_dm(args[:user_id], adapter_name: adapter_name)
54
+ result = dm_channel.post(args[:text])
55
+ {id: result.id, channel_id: dm_channel.id}
56
+ end
57
+
58
+ def execute_edit_message(adapter_name, args)
59
+ thread = @chat.channel(args[:channel_id], adapter_name: adapter_name).thread(args[:channel_id])
60
+ thread.edit(args[:message_id], args[:text])
61
+ {success: true}
62
+ end
63
+
64
+ def execute_delete_message(adapter_name, args)
65
+ thread = @chat.channel(args[:channel_id], adapter_name: adapter_name).thread(args[:channel_id])
66
+ thread.delete(args[:message_id])
67
+ {success: true}
68
+ end
69
+
70
+ def execute_add_reaction(adapter_name, args)
71
+ thread = @chat.channel(args[:channel_id], adapter_name: adapter_name).thread(args[:channel_id])
72
+ thread.react(args[:message_id], args[:emoji])
73
+ {success: true}
74
+ end
75
+
76
+ def execute_remove_reaction(adapter_name, args)
77
+ thread = @chat.channel(args[:channel_id], adapter_name: adapter_name).thread(args[:channel_id])
78
+ thread.unreact(args[:message_id], args[:emoji])
79
+ {success: true}
80
+ end
81
+
82
+ def execute_start_typing(adapter_name, args)
83
+ adapter = @chat.adapter(adapter_name)
84
+ adapter.start_typing(channel_id: args[:channel_id], thread_id: args[:thread_id])
85
+ {success: true}
86
+ end
87
+
88
+ def serialize_messages(messages)
89
+ messages.map { |m| {id: m.id, text: m.text, author: m.author&.name, timestamp: m.timestamp} }
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module AI
5
+ class << self
6
+ def to_ai_messages(messages, include_names: false, &transform)
7
+ Converter.to_ai_messages(messages, include_names: include_names, &transform)
8
+ end
9
+
10
+ def create_tools(chat:, preset: :messenger, require_approval: true)
11
+ ToolBuilder.new(chat: chat, preset: preset, require_approval: require_approval).build
12
+ end
13
+ end
14
+ end
15
+ end
@@ -25,14 +25,10 @@ module ChatSDK
25
25
  def collect_text(nodes = [self])
26
26
  nodes.flat_map do |node|
27
27
  case node.type
28
- when :card
29
- collect_text(node.children)
30
28
  when :text
31
29
  [node.attributes[:content]]
32
30
  when :field
33
31
  ["#{node.attributes[:label]}: #{node.attributes[:value]}"]
34
- when :section
35
- collect_text(node.children)
36
32
  when :button, :link_button
37
33
  [node.attributes[:text]]
38
34
  else
@@ -60,27 +60,13 @@ module ChatSDK
60
60
  lock_key = "chat_sdk:lock:#{thread_key}"
61
61
  return true if @state.acquire_lock(lock_key, owner: lock_owner, ttl: 30)
62
62
 
63
- case @config.on_lock_conflict
64
- when :drop
65
- ChatSDK::Log.debug("Lock conflict, dropping event for #{thread_key}")
66
- false
67
- when :force
68
- @state.force_lock(lock_key, owner: lock_owner, ttl: 30)
69
- true
70
- else
71
- if @config.on_lock_conflict.respond_to?(:call)
72
- policy = @config.on_lock_conflict.call(thread_key, event)
73
- case policy
74
- when :force
75
- @state.force_lock(lock_key, owner: lock_owner, ttl: 30)
76
- true
77
- else
78
- false
79
- end
80
- else
81
- false
82
- end
83
- end
63
+ policy = @config.on_lock_conflict
64
+ policy = policy.call(thread_key, event) if policy.respond_to?(:call)
65
+
66
+ return false unless policy == :force
67
+
68
+ @state.force_lock(lock_key, owner: lock_owner, ttl: 30)
69
+ true
84
70
  end
85
71
 
86
72
  def release_lock(thread_key)
@@ -109,10 +95,7 @@ module ChatSDK
109
95
  end
110
96
 
111
97
  def add_thread_to_event(event, thread)
112
- event.instance_variable_set(:@thread, thread)
113
- unless event.respond_to?(:thread)
114
- event.define_singleton_method(:thread) { @thread }
115
- end
98
+ event.thread = thread if event.respond_to?(:thread=)
116
99
  end
117
100
  end
118
101
  end
@@ -4,6 +4,7 @@ module ChatSDK
4
4
  module Events
5
5
  class Action < Base
6
6
  attr_reader :action_id, :value, :user, :thread_id, :channel_id, :trigger_id
7
+ attr_accessor :thread
7
8
 
8
9
  def initialize(action_id:, thread_id:, channel_id:, value: nil, user: nil, trigger_id: nil, **kwargs)
9
10
  super(type: :action, **kwargs)
@@ -4,6 +4,7 @@ module ChatSDK
4
4
  module Events
5
5
  class Reaction < Base
6
6
  attr_reader :emoji, :user_id, :message_id, :thread_id, :channel_id, :added
7
+ attr_accessor :thread
7
8
 
8
9
  def initialize(emoji:, user_id:, message_id:, thread_id:, channel_id:, added: true, **kwargs)
9
10
  super(type: :reaction, **kwargs)
@@ -4,6 +4,7 @@ module ChatSDK
4
4
  module Events
5
5
  class SlashCommand < Base
6
6
  attr_reader :command, :text, :user_id, :channel_id, :trigger_id
7
+ attr_accessor :thread
7
8
 
8
9
  def initialize(command:, user_id:, channel_id:, text: "", trigger_id: nil, **kwargs)
9
10
  super(type: :slash_command, **kwargs)
@@ -61,6 +61,10 @@ module ChatSDK
61
61
  adapter.remove_reaction(channel_id: channel_id, message_id: message_id, emoji: emoji)
62
62
  end
63
63
 
64
+ def post_ai_stream(enumerable, placeholder: "Thinking...")
65
+ ChatSDK::AI::StreamHandler.stream_to_thread(self, enumerable, placeholder: placeholder)
66
+ end
67
+
64
68
  def upload(io:, filename:, comment: nil)
65
69
  adapter.upload_file(channel_id: channel_id, io: io, filename: filename, thread_id: id, comment: comment)
66
70
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChatSDK
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/chat_sdk.rb CHANGED
@@ -9,7 +9,7 @@ module ChatSDK
9
9
  def loader
10
10
  @loader ||= begin
11
11
  loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
12
- loader.inflector.inflect("chat_sdk" => "ChatSDK")
12
+ loader.inflector.inflect("chat_sdk" => "ChatSDK", "ai" => "AI")
13
13
  loader.ignore("#{__dir__}/chat_sdk/version.rb")
14
14
  loader.ignore("#{__dir__}/chat_sdk/errors.rb")
15
15
  loader.setup
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chat_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rootly
@@ -34,6 +34,11 @@ files:
34
34
  - lib/chat_sdk.rb
35
35
  - lib/chat_sdk/adapter/base.rb
36
36
  - lib/chat_sdk/adapter/capabilities.rb
37
+ - lib/chat_sdk/ai.rb
38
+ - lib/chat_sdk/ai/converter.rb
39
+ - lib/chat_sdk/ai/stream_handler.rb
40
+ - lib/chat_sdk/ai/tool_builder.rb
41
+ - lib/chat_sdk/ai/tool_executor.rb
37
42
  - lib/chat_sdk/author.rb
38
43
  - lib/chat_sdk/cards/actions_context.rb
39
44
  - lib/chat_sdk/cards/builder.rb