bynn-decisionnode 0.4.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.
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DecisionNode
4
+ # One frame of a video: its time in seconds and its image.
5
+ #
6
+ # DecisionNode::VideoFrame.new(30.0, data: jpeg_bytes) # media type from the bytes
7
+ # DecisionNode::VideoFrame.new(30.5, path: "frames/0001.jpg")
8
+ # DecisionNode::VideoFrame.new(31.0, base64: "/9j/4AAQ...", media_type: "image/jpeg")
9
+ #
10
+ # Bytes are sent as base64; base64 text is sent exactly as given and needs
11
+ # its `media_type`. A frame is always sent inline: it is never uploaded and
12
+ # never counted toward the inline threshold of images. It is never resized
13
+ # or re-encoded.
14
+ #
15
+ # The values are checked with the request, before anything is sent: a time
16
+ # below 0, times that do not increase, or a media type other than
17
+ # image/jpeg, image/png or image/webp raise BadRequestError as the API
18
+ # would. A time that is not a number, or a source of the wrong type, raises
19
+ # ArgumentError here.
20
+ class VideoFrame
21
+ # The frame's time in seconds.
22
+ attr_reader :t
23
+ # "image/jpeg", "image/png" or "image/webp": as given, else detected from
24
+ # the bytes; nil when neither says.
25
+ attr_reader :media_type
26
+ # The bytes (a frozen binary String), or nil for base64 text.
27
+ attr_reader :data
28
+
29
+ def initialize(t, data: nil, base64: nil, path: nil, media_type: nil)
30
+ raise ArgumentError, "frame time t must be a number of seconds, got #{t.inspect}" unless t.is_a?(Integer) || t.is_a?(Float)
31
+ raise ArgumentError, "give exactly one of data:, base64: or path:" unless [data, base64, path].compact.size == 1
32
+
33
+ @t = t
34
+ @base64 = base64.nil? ? nil : text(base64)
35
+ @data = (@base64 ? nil : read_bytes(data, path)).freeze
36
+ declared = media_type.nil? || media_type.to_s.empty? ? nil : media_type.to_s
37
+ @media_type = declared || (@data && ImageHeader.media_type(@data))
38
+ end
39
+
40
+ # The frame's wire form: `{"t", "media_type", "data"}`.
41
+ def to_h
42
+ { "t" => t, "media_type" => media_type, "data" => @base64 || [@data].pack("m0") }
43
+ end
44
+
45
+ def inspect
46
+ size = @base64 ? "#{@base64.bytesize} base64 characters" : "#{@data.bytesize} bytes"
47
+ "#<#{self.class.name} t=#{t} #{media_type || 'unknown type'} #{size}>"
48
+ end
49
+
50
+ private
51
+
52
+ def text(value)
53
+ raise ArgumentError, "frame base64: must be a String of base64 text" unless value.is_a?(String)
54
+
55
+ value
56
+ end
57
+
58
+ def read_bytes(data, path)
59
+ if path
60
+ begin
61
+ File.binread(path.to_s)
62
+ rescue SystemCallError, IOError => e
63
+ raise ArgumentError, "frame at #{t}: cannot read #{path}: #{e.message}"
64
+ end
65
+ elsif data.respond_to?(:read)
66
+ data.read.to_s.b
67
+ elsif data.is_a?(String)
68
+ data.b
69
+ else
70
+ raise ArgumentError, "frame data: must be a String of bytes or an IO"
71
+ end
72
+ end
73
+ end
74
+
75
+ # A video sent as the frames the caller sampled, each with its time.
76
+ #
77
+ # frames = jpegs.each_with_index.map { |bytes, i| DecisionNode::VideoFrame.new(i * 0.5, data: bytes) }
78
+ # DecisionNode::Video.new("dock", frames: frames)
79
+ # DecisionNode::Video.new("dock", frames: frames, fps: 4, start: 30, end: 90)
80
+ #
81
+ # `fps` caps the rate the API reads (above 0, at most 4; the API's default
82
+ # is 2); `start` and `end` are the window read, on the frames' own times.
83
+ # Each is sent only when given. Only these frames are sent, never a video
84
+ # file, a URL or sound.
85
+ #
86
+ # The id is unique across the request's images and videos. The model sees
87
+ # the video as "Video <n>" (its place in the request's videos, from 1) with
88
+ # this id beside it, so text in the state or the instructions can refer to
89
+ # it by either.
90
+ class Video
91
+ attr_reader :id, :frames, :fps, :start, :end
92
+
93
+ def initialize(id, frames:, fps: nil, start: nil, end: nil)
94
+ last = binding.local_variable_get(:end)
95
+ raise ArgumentError, "frames: must be an Array of DecisionNode::VideoFrame" unless frames.is_a?(Array)
96
+
97
+ frames.each do |frame|
98
+ next if frame.is_a?(VideoFrame) || frame.is_a?(Hash)
99
+
100
+ raise ArgumentError, "each frame must be a DecisionNode::VideoFrame or a wire-form Hash, got #{frame.class}"
101
+ end
102
+ { "fps" => fps, "start" => start, "end" => last }.each do |name, value|
103
+ next if value.nil? || value.is_a?(Integer) || value.is_a?(Float)
104
+
105
+ raise ArgumentError, "#{name} must be a number, got #{value.inspect}"
106
+ end
107
+ @id = id.is_a?(Symbol) ? id.to_s : id
108
+ @frames = frames.dup.freeze
109
+ @fps = fps
110
+ @start = start
111
+ @end = last
112
+ end
113
+
114
+ # The video's wire form, `fps`, `start` and `end` only when given.
115
+ def to_h
116
+ wire = { "id" => id }
117
+ wire["fps"] = fps unless fps.nil?
118
+ wire["start"] = start unless start.nil?
119
+ wire["end"] = self.end unless self.end.nil?
120
+ wire["frames"] = frames.map { |f| f.is_a?(VideoFrame) ? f.to_h : f }
121
+ wire
122
+ end
123
+
124
+ def inspect
125
+ "#<#{self.class.name} #{id.inspect} #{frames.size} frames>"
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # DecisionNode is both the client and the namespace of this gem:
4
+ #
5
+ # require "decisionnode"
6
+ #
7
+ # dn = DecisionNode.new # reads DECISIONNODE_API_KEY
8
+ # d = dn.decide(state: "...", questions: { "escalate" => DecisionNode.truth("Escalate?") })
9
+ # d["escalate"].truth
10
+ class DecisionNode
11
+ # One result of #decide_many: a Decision, or the error that request ended
12
+ # with. Both include this module, so `result.is_a?(DecisionNode::DecisionResult)`
13
+ # holds for each, and `result.is_a?(DecisionNode::Decision)` tells them apart.
14
+ module DecisionResult; end
15
+ end
16
+
17
+ require_relative "decisionnode/version"
18
+ require_relative "decisionnode/errors"
19
+ require_relative "decisionnode/questions"
20
+ require_relative "decisionnode/image_header"
21
+ require_relative "decisionnode/image"
22
+ require_relative "decisionnode/document"
23
+ require_relative "decisionnode/video"
24
+ require_relative "decisionnode/boosters"
25
+ require_relative "decisionnode/checks"
26
+ require_relative "decisionnode/results"
27
+ require_relative "decisionnode/transport"
28
+ require_relative "decisionnode/uploads"
29
+ require_relative "decisionnode/session"
30
+ require_relative "decisionnode/supervisor"
31
+ require_relative "decisionnode/batches"
32
+ require_relative "decisionnode/collections"
33
+ require_relative "decisionnode/keys"
34
+ require_relative "decisionnode/client"
35
+
36
+ class DecisionNode
37
+ # Loaded with the WebSocket library on the first session.
38
+ autoload :SessionStream, File.expand_path("decisionnode/session_stream", __dir__)
39
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bynn-decisionnode
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.1
5
+ platform: ruby
6
+ authors:
7
+ - Bynn Intelligence, Inc.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-10-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: websocket-driver
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.8.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.8.2
27
+ description: 'A typed client for DecisionNode, a decision engine: ask truth, choice,
28
+ score and number questions about any state, its images and its videos, and get calibrated
29
+ answers back, with image uploads, sessions, the Batch API, retries, typed errors
30
+ and the safety check result.'
31
+ email:
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - CHANGELOG.md
37
+ - LICENSE
38
+ - README.md
39
+ - lib/decisionnode.rb
40
+ - lib/decisionnode/batches.rb
41
+ - lib/decisionnode/boosters.rb
42
+ - lib/decisionnode/checks.rb
43
+ - lib/decisionnode/client.rb
44
+ - lib/decisionnode/collections.rb
45
+ - lib/decisionnode/document.rb
46
+ - lib/decisionnode/errors.rb
47
+ - lib/decisionnode/image.rb
48
+ - lib/decisionnode/image_header.rb
49
+ - lib/decisionnode/keys.rb
50
+ - lib/decisionnode/questions.rb
51
+ - lib/decisionnode/results.rb
52
+ - lib/decisionnode/session.rb
53
+ - lib/decisionnode/session_stream.rb
54
+ - lib/decisionnode/supervisor.rb
55
+ - lib/decisionnode/transport.rb
56
+ - lib/decisionnode/uploads.rb
57
+ - lib/decisionnode/version.rb
58
+ - lib/decisionnode/video.rb
59
+ homepage: https://decisionnode.com
60
+ licenses:
61
+ - MIT
62
+ metadata:
63
+ source_code_uri: https://github.com/Bynn-Intelligence/decision-node-ruby
64
+ changelog_uri: https://github.com/Bynn-Intelligence/decision-node-ruby/blob/main/CHANGELOG.md
65
+ rubygems_mfa_required: 'true'
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '3.2'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubygems_version: 3.5.22
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: Ruby client for the DecisionNode decision engine API.
85
+ test_files: []