chat_sdk 0.2.1 → 0.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 +8 -0
- data/lib/chat_sdk/adapter/meta_verification.rb +42 -0
- data/lib/chat_sdk/ai.rb +6 -2
- data/lib/chat_sdk/api_client/base.rb +99 -0
- data/lib/chat_sdk/cards/text_helpers.rb +34 -0
- data/lib/chat_sdk/dispatcher.rb +33 -22
- data/lib/chat_sdk/instrumentation.rb +55 -0
- data/lib/chat_sdk/version.rb +1 -1
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b44dd1b3b15cb4a83647923c0f75ad2208860060010a1e2c47391d1f4b44ca5a
|
|
4
|
+
data.tar.gz: b6e99a0348c858130e767f6647fa1a430d45e9497e6a7d652343dc4d1e0ef4de
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4983f6b3a956b5b0cbe2d84c23b739d1ae6f5c8eb34484a42ea9e0f90fe37c0a390a2e0d5bbcf9b7e0a5f993e1395d38d7d830405d67f4de5183918d1bab38e6
|
|
7
|
+
data.tar.gz: bbd3377c30c6e5fc53e9959a76d96a4eb907743f834b4c4d554317240861d8ab60b85372ab51e911ba720d601abcc6ddb15646053c8409c878f865a141ab3ba6
|
|
@@ -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(
|
|
11
|
-
ToolBuilder.new(
|
|
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
|
|
@@ -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
|
data/lib/chat_sdk/dispatcher.rb
CHANGED
|
@@ -10,18 +10,20 @@ module ChatSDK
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def dispatch(event, adapter:, adapter_name:)
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
16
|
+
thread = build_thread(event, adapter)
|
|
17
|
+
thread_key = thread_key_for(event, adapter_name)
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
return unless acquire_lock(thread_key, event)
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
65
|
+
acquired = @state.acquire_lock(lock_key, owner: lock_owner, ttl: 30)
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
unless acquired
|
|
68
|
+
policy = @config.on_lock_conflict
|
|
69
|
+
policy = policy.call(thread_key, event) if policy.respond_to?(:call)
|
|
65
70
|
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
data/lib/chat_sdk/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
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
|
-
-
|
|
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/
|
|
83
|
+
homepage: https://github.com/rootlyhq/chat-sdk
|
|
80
84
|
licenses:
|
|
81
85
|
- MIT
|
|
82
86
|
metadata:
|