chat_sdk 0.2.1 → 0.3.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: a8db1850adc2b313ad351d222eed5ed588f6c46b122bcf9f6f8f3eba445c19d5
4
- data.tar.gz: 5fd507fa0cb224b1e204f5eeaedee74379769f4f9c5a8055af4549ecb1bbc97f
3
+ metadata.gz: 112741546df05e559d84fed5aa333fb613c8654b1cb2833ef301c045b60bece8
4
+ data.tar.gz: 53fbc7f474321ecc3fd546dae9625f450496b482248aee1cac2ca411dc91f5dc
5
5
  SHA512:
6
- metadata.gz: 8af8ee31c13cbc5acccef41282ad5717ad2b2ff149fed4bcec95bfdb9fc6c422560d4d55c965e992cc1df1194232813b64f62653cc4c158ff51b4226768f871c
7
- data.tar.gz: 0d1132b3d7f19f466aeb29c0d5827af6abc7702a917c90b1d679675239d24fda092b7d273830f881dd0b2e9693a1a975b6f3e6f1ff22f4c4652dbb876489915e
6
+ metadata.gz: 75fbe5c2fb02cd9647bb7220eac4deb2f772a52e39df489483c9d939dc52ed18a4bd90fedac2d3afeabedf37f6b5de745b66f4c08e925477c213bfe05bea3df6
7
+ data.tar.gz: 4bb1c49244d388e7f5d6bd3ab3b3e647c7a6fa04d3db66c80f2397c954ed1ad74edd15a6c79bc087d0a4f5104e0baa1c2232b3cc16191884e6ad73d4d18d488c
@@ -89,6 +89,14 @@ module ChatSDK
89
89
  Cards::Renderer.new.render(postable_message.card)
90
90
  end
91
91
 
92
+ protected
93
+
94
+ def read_json_body(rack_request)
95
+ body = rack_request.body.read
96
+ rack_request.body.rewind
97
+ JSON.parse(body)
98
+ end
99
+
92
100
  private
93
101
 
94
102
  def require_capability!(cap)
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "rack/utils"
5
+
6
+ module ChatSDK
7
+ module Adapter
8
+ module MetaVerification
9
+ def verify_meta_signature!(rack_request, secret:, platform_name:)
10
+ signature = rack_request.get_header("HTTP_X_HUB_SIGNATURE_256")
11
+
12
+ unless signature
13
+ raise ChatSDK::SignatureVerificationError, "Missing #{platform_name} signature header"
14
+ end
15
+
16
+ body = rack_request.body.read
17
+ rack_request.body.rewind
18
+
19
+ expected = "sha256=#{OpenSSL::HMAC.hexdigest("SHA256", secret, body)}"
20
+
21
+ unless Rack::Utils.secure_compare(signature, expected)
22
+ raise ChatSDK::SignatureVerificationError, "Invalid #{platform_name} signature"
23
+ end
24
+
25
+ true
26
+ end
27
+
28
+ def meta_ack_response(rack_request, verify_token:)
29
+ return nil unless rack_request.get?
30
+
31
+ params = rack_request.params
32
+ mode = params["hub.mode"]
33
+ token = params["hub.verify_token"]
34
+ challenge = params["hub.challenge"]
35
+
36
+ if mode == "subscribe" && token == verify_token
37
+ [200, {}, [challenge.to_s]]
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
data/lib/chat_sdk/ai.rb CHANGED
@@ -7,8 +7,12 @@ module ChatSDK
7
7
  Converter.to_ai_messages(messages, include_names: include_names, &transform)
8
8
  end
9
9
 
10
- def create_tools(chat:, preset: :messenger, require_approval: true)
11
- ToolBuilder.new(chat: chat, preset: preset, require_approval: require_approval).build
10
+ def create_tools(preset: :messenger, require_approval: true)
11
+ ToolBuilder.new(preset: preset, require_approval: require_approval).build
12
+ end
13
+
14
+ def create_executor(chat:)
15
+ ToolExecutor.new(chat: chat)
12
16
  end
13
17
  end
14
18
  end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module ApiClient
5
+ class Base
6
+ MAX_RETRIES = 2
7
+
8
+ private
9
+
10
+ def connection
11
+ @connection ||= build_connection { |f| f.request :json }
12
+ end
13
+
14
+ def upload_connection
15
+ @upload_connection ||= build_connection { |f| f.request :multipart }
16
+ end
17
+
18
+ def build_connection
19
+ Faraday.new(url: base_url) do |f|
20
+ yield f
21
+ configure_auth(f)
22
+ f.response :json
23
+ f.adapter :net_http
24
+ end
25
+ end
26
+
27
+ def base_url
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def adapter_name
32
+ raise NotImplementedError
33
+ end
34
+
35
+ def configure_auth(faraday)
36
+ raise NotImplementedError
37
+ end
38
+
39
+ def resolve_path(path)
40
+ path
41
+ end
42
+
43
+ def request(method, path, body = nil)
44
+ retries = 0
45
+ resolved = resolve_path(path)
46
+ begin
47
+ ChatSDK::Instrumentation.instrument("api_request.chat_sdk", adapter: adapter_name, method: method, path: resolved) do
48
+ response = connection.public_send(method, resolved) do |req|
49
+ req.body = body if body && method != :get
50
+ end
51
+ handle_response(response)
52
+ end
53
+ rescue ChatSDK::RateLimitedError => e
54
+ ChatSDK::Instrumentation.instrument("rate_limited.chat_sdk", adapter: adapter_name, retry_after: e.retry_after, attempt: retries + 1)
55
+ retries += 1
56
+ raise if retries > MAX_RETRIES
57
+ sleep(e.retry_after || (2**retries * 0.5))
58
+ retry
59
+ end
60
+ end
61
+
62
+ def handle_response(response)
63
+ return extract_success_body(response) if response.success?
64
+
65
+ body = response.body
66
+
67
+ if response.status == 429
68
+ raise ChatSDK::RateLimitedError.new(
69
+ "#{adapter_name} API rate limited",
70
+ retry_after: extract_retry_after(response),
71
+ status: response.status,
72
+ body: body,
73
+ adapter_name: adapter_name
74
+ )
75
+ end
76
+
77
+ raise ChatSDK::PlatformError.new(
78
+ "#{adapter_name} API error: #{extract_error_message(response)}",
79
+ status: response.status,
80
+ body: body,
81
+ adapter_name: adapter_name
82
+ )
83
+ end
84
+
85
+ def extract_success_body(response)
86
+ body = response.body
87
+ body.is_a?(Hash) ? body : {}
88
+ end
89
+
90
+ def extract_retry_after(_response)
91
+ nil
92
+ end
93
+
94
+ def extract_error_message(response)
95
+ response.status.to_s
96
+ end
97
+ end
98
+ end
99
+ end
@@ -2,13 +2,14 @@
2
2
 
3
3
  module ChatSDK
4
4
  class Author
5
- attr_reader :id, :name, :platform, :raw
5
+ attr_reader :id, :name, :platform, :locale, :raw
6
6
 
7
- def initialize(id:, name:, platform:, bot: false, raw: nil)
7
+ def initialize(id:, name:, platform:, bot: false, locale: nil, raw: nil)
8
8
  @id = id
9
9
  @name = name
10
10
  @platform = platform
11
11
  @bot = bot
12
+ @locale = locale
12
13
  @raw = raw
13
14
  end
14
15
 
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Cards
5
+ module TextHelpers
6
+ private
7
+
8
+ def collect_text_parts(node)
9
+ parts = []
10
+ node.children.each do |child|
11
+ case child.type
12
+ when :text
13
+ parts << child.attributes[:content]
14
+ when :divider
15
+ parts << "---"
16
+ when :fields
17
+ child.children.each do |field|
18
+ parts << "#{field.attributes[:label]}: #{field.attributes[:value]}"
19
+ end
20
+ when :section
21
+ parts << child.attributes[:title] if child.attributes[:title]
22
+ parts.concat(collect_text_parts(child))
23
+ end
24
+ end
25
+ parts
26
+ end
27
+
28
+ def truncate(text, max)
29
+ return "" unless text
30
+ (text.length > max) ? "#{text[0..max - 4]}..." : text
31
+ end
32
+ end
33
+ end
34
+ end
@@ -10,18 +10,20 @@ module ChatSDK
10
10
  end
11
11
 
12
12
  def dispatch(event, adapter:, adapter_name:)
13
- return unless dedupe(event, adapter_name)
13
+ ChatSDK::Instrumentation.instrument("dispatch.chat_sdk", adapter: adapter_name, event_type: event.type) do
14
+ return unless dedupe(event, adapter_name)
14
15
 
15
- thread = build_thread(event, adapter)
16
- thread_key = thread_key_for(event, adapter_name)
16
+ thread = build_thread(event, adapter)
17
+ thread_key = thread_key_for(event, adapter_name)
17
18
 
18
- return unless acquire_lock(thread_key, event)
19
+ return unless acquire_lock(thread_key, event)
19
20
 
20
- begin
21
- handlers = @registry.handlers_for(event)
22
- handlers.each { |handler| execute_handler(handler, event, thread) }
23
- ensure
24
- release_lock(thread_key)
21
+ begin
22
+ handlers = @registry.handlers_for(event)
23
+ handlers.each { |handler| execute_handler(handler, event, thread) }
24
+ ensure
25
+ release_lock(thread_key)
26
+ end
25
27
  end
26
28
  end
27
29
 
@@ -31,11 +33,13 @@ module ChatSDK
31
33
  event_id = extract_event_id(event)
32
34
  return true unless event_id
33
35
 
34
- @state.set_if_absent(
36
+ is_new = @state.set_if_absent(
35
37
  "chat_sdk:dedupe:#{adapter_name}:#{event_id}",
36
38
  true,
37
39
  ttl: @config.dedupe_ttl
38
40
  )
41
+ ChatSDK::Instrumentation.instrument("dedupe.chat_sdk", adapter: adapter_name, event_id: event_id, duplicate: !is_new)
42
+ is_new
39
43
  end
40
44
 
41
45
  def extract_event_id(event)
@@ -58,15 +62,20 @@ module ChatSDK
58
62
 
59
63
  def acquire_lock(thread_key, event)
60
64
  lock_key = "chat_sdk:lock:#{thread_key}"
61
- return true if @state.acquire_lock(lock_key, owner: lock_owner, ttl: 30)
65
+ acquired = @state.acquire_lock(lock_key, owner: lock_owner, ttl: 30)
62
66
 
63
- policy = @config.on_lock_conflict
64
- policy = policy.call(thread_key, event) if policy.respond_to?(:call)
67
+ unless acquired
68
+ policy = @config.on_lock_conflict
69
+ policy = policy.call(thread_key, event) if policy.respond_to?(:call)
65
70
 
66
- return false unless policy == :force
71
+ if policy == :force
72
+ @state.force_lock(lock_key, owner: lock_owner, ttl: 30)
73
+ acquired = true
74
+ end
75
+ end
67
76
 
68
- @state.force_lock(lock_key, owner: lock_owner, ttl: 30)
69
- true
77
+ ChatSDK::Instrumentation.instrument("lock.chat_sdk", key: thread_key, acquired: acquired, policy: acquired ? nil : policy)
78
+ acquired
70
79
  end
71
80
 
72
81
  def release_lock(thread_key)
@@ -82,12 +91,14 @@ module ChatSDK
82
91
  end
83
92
 
84
93
  def execute_handler(handler, event, thread)
85
- case event.type
86
- when :mention, :subscribed_message, :direct_message
87
- handler.block.call(thread, event.message)
88
- when :reaction, :action, :slash_command
89
- add_thread_to_event(event, thread)
90
- handler.block.call(event)
94
+ ChatSDK::Instrumentation.instrument("handler.chat_sdk", handler_type: event.type) do
95
+ case event.type
96
+ when :mention, :subscribed_message, :direct_message
97
+ handler.block.call(thread, event.message)
98
+ when :reaction, :action, :slash_command
99
+ add_thread_to_event(event, thread)
100
+ handler.block.call(event)
101
+ end
91
102
  end
92
103
  rescue => e
93
104
  ChatSDK::Log.error("Handler error (#{event.type}): #{e.message}")
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Instrumentation
5
+ @subscribers = Hash.new { |h, k| h[k] = [] }
6
+ @mutex = Mutex.new
7
+
8
+ class << self
9
+ def subscribe(event_name, &block)
10
+ @mutex.synchronize { @subscribers[event_name] << block }
11
+ block
12
+ end
13
+
14
+ def unsubscribe(event_name, block)
15
+ @mutex.synchronize { @subscribers[event_name].delete(block) }
16
+ end
17
+
18
+ def instrument(event_name, payload = {})
19
+ listeners = @mutex.synchronize { @subscribers[event_name]&.dup }
20
+ unless listeners&.any?
21
+ return yield if block_given?
22
+
23
+ return
24
+ end
25
+
26
+ start = monotonic_now
27
+ result = yield if block_given?
28
+ payload[:duration] = monotonic_now - start
29
+ fire(listeners, event_name, payload)
30
+ result
31
+ rescue => e
32
+ payload[:duration] = monotonic_now - start if start
33
+ payload[:error] = e
34
+ fire(listeners, event_name, payload) if listeners&.any?
35
+ raise
36
+ end
37
+
38
+ def reset!
39
+ @mutex.synchronize { @subscribers.clear }
40
+ end
41
+
42
+ private
43
+
44
+ def fire(listeners, event_name, payload)
45
+ listeners.each { |block| block.call(event_name, payload) }
46
+ rescue => e
47
+ ChatSDK::Log.debug("Instrumentation subscriber error: #{e.message}")
48
+ end
49
+
50
+ def monotonic_now
51
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChatSDK
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.1"
5
5
  end
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chat_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
- - Rootly
7
+ - Quentin Rousseau
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
@@ -26,7 +26,7 @@ dependencies:
26
26
  description: Platform-agnostic chat bot framework with normalized events, cards DSL,
27
27
  streaming, and pluggable adapters
28
28
  email:
29
- - eng@rootly.com
29
+ - quentin@rootly.com
30
30
  executables: []
31
31
  extensions: []
32
32
  extra_rdoc_files: []
@@ -34,11 +34,13 @@ 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/adapter/meta_verification.rb
37
38
  - lib/chat_sdk/ai.rb
38
39
  - lib/chat_sdk/ai/converter.rb
39
40
  - lib/chat_sdk/ai/stream_handler.rb
40
41
  - lib/chat_sdk/ai/tool_builder.rb
41
42
  - lib/chat_sdk/ai/tool_executor.rb
43
+ - lib/chat_sdk/api_client/base.rb
42
44
  - lib/chat_sdk/author.rb
43
45
  - lib/chat_sdk/cards/actions_context.rb
44
46
  - lib/chat_sdk/cards/builder.rb
@@ -46,6 +48,7 @@ files:
46
48
  - lib/chat_sdk/cards/node.rb
47
49
  - lib/chat_sdk/cards/renderer.rb
48
50
  - lib/chat_sdk/cards/select_context.rb
51
+ - lib/chat_sdk/cards/text_helpers.rb
49
52
  - lib/chat_sdk/channel.rb
50
53
  - lib/chat_sdk/chat.rb
51
54
  - lib/chat_sdk/config.rb
@@ -59,6 +62,7 @@ files:
59
62
  - lib/chat_sdk/events/reaction.rb
60
63
  - lib/chat_sdk/events/slash_command.rb
61
64
  - lib/chat_sdk/events/subscribed_message.rb
65
+ - lib/chat_sdk/instrumentation.rb
62
66
  - lib/chat_sdk/log.rb
63
67
  - lib/chat_sdk/message.rb
64
68
  - lib/chat_sdk/modals/builder.rb
@@ -76,7 +80,7 @@ files:
76
80
  - lib/chat_sdk/version.rb
77
81
  - lib/chat_sdk/webhook/endpoint.rb
78
82
  - lib/chat_sdk/webhook/router.rb
79
- homepage: https://github.com/rootlyhq/rootly-chat-sdk
83
+ homepage: https://github.com/rootlyhq/chat-sdk
80
84
  licenses:
81
85
  - MIT
82
86
  metadata: