tama-rb 0.1.2
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 +7 -0
- data/CHANGELOG.md +11 -0
- data/LICENSE +21 -0
- data/README.md +228 -0
- data/lib/tama/agentic/author.rb +23 -0
- data/lib/tama/agentic/message.rb +11 -0
- data/lib/tama/agentic/message_params.rb +46 -0
- data/lib/tama/agentic/service.rb +50 -0
- data/lib/tama/agentic/thread.rb +22 -0
- data/lib/tama/broadcast.rb +76 -0
- data/lib/tama/client.rb +20 -0
- data/lib/tama/error.rb +45 -0
- data/lib/tama/http.rb +169 -0
- data/lib/tama/memory/entity.rb +11 -0
- data/lib/tama/memory/entity_params.rb +34 -0
- data/lib/tama/memory/service.rb +21 -0
- data/lib/tama/model.rb +81 -0
- data/lib/tama/neural/klass.rb +14 -0
- data/lib/tama/neural/operation.rb +20 -0
- data/lib/tama/neural/operation_params.rb +27 -0
- data/lib/tama/neural/service.rb +34 -0
- data/lib/tama/neural/space.rb +11 -0
- data/lib/tama/params.rb +29 -0
- data/lib/tama/perception/chain.rb +11 -0
- data/lib/tama/perception/concept.rb +22 -0
- data/lib/tama/perception/generator.rb +19 -0
- data/lib/tama/perception/service.rb +29 -0
- data/lib/tama/sse_parser.rb +60 -0
- data/lib/tama/version.rb +5 -0
- data/lib/tama.rb +27 -0
- metadata +125 -0
data/lib/tama/http.rb
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
require "faraday"
|
|
5
|
+
require "faraday/retry"
|
|
6
|
+
require "json"
|
|
7
|
+
require "uri"
|
|
8
|
+
|
|
9
|
+
module Tama
|
|
10
|
+
class HTTP
|
|
11
|
+
RETRY_STATUSES = [429, 500, 502, 503, 504].freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :base_uri, :connection
|
|
14
|
+
|
|
15
|
+
def initialize(base_url:, headers:, timeout:, retries:, connection: nil)
|
|
16
|
+
@base_uri = parse_base_url(base_url)
|
|
17
|
+
@headers = Faraday::Utils::Headers.new(headers)
|
|
18
|
+
@timeout = validate_timeout(timeout)
|
|
19
|
+
@retries = validate_retries(retries)
|
|
20
|
+
unless connection.nil? || connection.respond_to?(:run_request)
|
|
21
|
+
raise Error::ConfigurationError, "connection must be a Faraday-compatible connection"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@connection = connection || build_connection
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def namespace!
|
|
28
|
+
segments = base_uri.path.split("/").reject(&:empty?)
|
|
29
|
+
raise Error::ConfigurationError, "Base URL must end with an operation namespace" if segments.empty?
|
|
30
|
+
|
|
31
|
+
segments.last
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def validate_namespace!(*allowed)
|
|
35
|
+
namespace = namespace!
|
|
36
|
+
return if allowed.include?(namespace)
|
|
37
|
+
|
|
38
|
+
raise Error::InvalidNamespaceError,
|
|
39
|
+
"Invalid client namespace. Expected one of #{allowed.inspect}, got #{namespace.inspect}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def request(method, path, body: nil, query: nil, headers: {}, timeout: nil, &on_data)
|
|
43
|
+
connection.run_request(method, absolute_url(path), body, merged_headers(headers)) do |request|
|
|
44
|
+
request.options.timeout = validate_timeout(timeout || @timeout)
|
|
45
|
+
request.options.on_data = on_data if on_data
|
|
46
|
+
query&.each_pair { |key, value| request.params[key] = value }
|
|
47
|
+
end
|
|
48
|
+
rescue Error
|
|
49
|
+
raise
|
|
50
|
+
rescue Faraday::Error => e
|
|
51
|
+
raise Error::TransportError.new(e.message, cause: e)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def parse_data(response, model, list: false)
|
|
55
|
+
check_status!(response)
|
|
56
|
+
document = parse_json(response.body)
|
|
57
|
+
raise Error::ParseError, "Response JSON must be an object with a data key" unless document.is_a?(Hash) && document.key?("data")
|
|
58
|
+
|
|
59
|
+
data = document["data"]
|
|
60
|
+
if list
|
|
61
|
+
raise Error::ParseError, "Response data must be an array" unless data.is_a?(Array)
|
|
62
|
+
|
|
63
|
+
data.map { |item| model.parse(item) }
|
|
64
|
+
else
|
|
65
|
+
model.parse(data)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_body(response, model)
|
|
70
|
+
check_status!(response)
|
|
71
|
+
model.parse(parse_json(response.body))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def check_status!(response)
|
|
75
|
+
status = response.status.to_i
|
|
76
|
+
return response if status.between?(200, 299)
|
|
77
|
+
|
|
78
|
+
body = parse_error_body(response.body)
|
|
79
|
+
raise Error::NotFoundError.new(body:) if status == 404
|
|
80
|
+
if status == 422
|
|
81
|
+
errors = (body.is_a?(Hash) && body.key?("errors")) ? body["errors"] : body
|
|
82
|
+
raise Error::ValidationError.new("API validation failed", errors:, status:)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
raise Error::HTTPError.new(status:, body:)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.segment(value, name)
|
|
89
|
+
unless value.is_a?(String) && !value.empty?
|
|
90
|
+
raise Error::ValidationError.new("#{name} must be a non-empty string", errors: {name => "must be a non-empty string"})
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
CGI.escape(value).gsub("+", "%20")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def parse_base_url(value)
|
|
99
|
+
raise Error::ConfigurationError, "base_url must be a string" unless value.is_a?(String) && !value.empty?
|
|
100
|
+
|
|
101
|
+
uri = URI.parse(value)
|
|
102
|
+
unless %w[http https].include?(uri.scheme) && uri.host && !uri.host.empty?
|
|
103
|
+
raise Error::ConfigurationError, "base_url must be an absolute HTTP(S) URL"
|
|
104
|
+
end
|
|
105
|
+
unless uri.port.between?(1, 65_535)
|
|
106
|
+
raise Error::ConfigurationError, "base_url port must be between 1 and 65535"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
uri.path = "/" if uri.path.empty?
|
|
110
|
+
uri
|
|
111
|
+
rescue URI::InvalidURIError => e
|
|
112
|
+
raise Error::ConfigurationError, "Invalid base_url: #{e.message}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def validate_timeout(value)
|
|
116
|
+
return value if value.is_a?(Numeric) && value.positive?
|
|
117
|
+
|
|
118
|
+
raise Error::ConfigurationError, "timeout must be a positive number"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def validate_retries(value)
|
|
122
|
+
return value if value.is_a?(Integer) && value >= 0
|
|
123
|
+
|
|
124
|
+
raise Error::ConfigurationError, "retries must be a non-negative integer"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def build_connection
|
|
128
|
+
Faraday.new do |faraday|
|
|
129
|
+
faraday.request :json
|
|
130
|
+
faraday.request :retry, max: @retries, retry_statuses: RETRY_STATUSES
|
|
131
|
+
faraday.adapter Faraday.default_adapter
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def absolute_url(path)
|
|
136
|
+
uri = base_uri.dup
|
|
137
|
+
base_path = uri.path.sub(%r{/+\z}, "")
|
|
138
|
+
endpoint = path.sub(%r{\A/+}, "")
|
|
139
|
+
uri.path = "#{base_path}/#{endpoint}"
|
|
140
|
+
uri.query = nil
|
|
141
|
+
uri.fragment = nil
|
|
142
|
+
uri.to_s
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def merged_headers(headers)
|
|
146
|
+
@headers.merge(headers).tap do |merged|
|
|
147
|
+
merged["Accept"] ||= "application/json"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def parse_json(body)
|
|
152
|
+
return body if body.is_a?(Hash) || body.is_a?(Array)
|
|
153
|
+
raise Error::ParseError, "Response body must contain JSON" unless body.is_a?(String) && !body.empty?
|
|
154
|
+
|
|
155
|
+
JSON.parse(body)
|
|
156
|
+
rescue JSON::ParserError => e
|
|
157
|
+
raise Error::ParseError, "Malformed JSON response: #{e.message}"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def parse_error_body(body)
|
|
161
|
+
return body unless body.is_a?(String)
|
|
162
|
+
return body if body.empty?
|
|
163
|
+
|
|
164
|
+
JSON.parse(body)
|
|
165
|
+
rescue JSON::ParserError
|
|
166
|
+
body
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tama
|
|
4
|
+
module Memory
|
|
5
|
+
Entity = Model.define(
|
|
6
|
+
:id, :class_id, :current_state, :identifier,
|
|
7
|
+
required: %i[class_id current_state identifier],
|
|
8
|
+
types: {id: String, class_id: String, current_state: String, identifier: String}
|
|
9
|
+
)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tama
|
|
4
|
+
module Memory
|
|
5
|
+
EntityParams = Data.define(:identifier, :record, :validate_record) do
|
|
6
|
+
def initialize(identifier:, record:, validate_record: true)
|
|
7
|
+
unless identifier.is_a?(String) && !identifier.empty?
|
|
8
|
+
raise Error::ValidationError.new("identifier is required", errors: {identifier: "must be a non-empty string"})
|
|
9
|
+
end
|
|
10
|
+
unless record.is_a?(Hash)
|
|
11
|
+
raise Error::ValidationError.new("record is required", errors: {record: "must be a hash"})
|
|
12
|
+
end
|
|
13
|
+
unless [true, false].include?(validate_record)
|
|
14
|
+
raise Error::ValidationError.new("validate_record must be boolean", errors: {validate_record: "must be boolean"})
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.parse(value)
|
|
21
|
+
data = Params.hash(value, "entity")
|
|
22
|
+
new(
|
|
23
|
+
identifier: data["identifier"],
|
|
24
|
+
record: data["record"],
|
|
25
|
+
validate_record: data.fetch("validate_record", true)
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
{"identifier" => identifier, "record" => record, "validate_record" => validate_record}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tama
|
|
4
|
+
module Memory
|
|
5
|
+
class Service
|
|
6
|
+
def initialize(http)
|
|
7
|
+
@http = http
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def create_entity(klass, attributes = nil, headers: {}, timeout: nil, **attribute_keywords)
|
|
11
|
+
@http.validate_namespace!("ingest")
|
|
12
|
+
class_id = klass.is_a?(Neural::Klass) ? klass.id : klass
|
|
13
|
+
attributes ||= attribute_keywords
|
|
14
|
+
params = attributes.is_a?(EntityParams) ? attributes : EntityParams.parse(attributes)
|
|
15
|
+
path = "memory/classes/#{HTTP.segment(class_id, "class_id")}/entities"
|
|
16
|
+
response = @http.request(:post, path, body: {"entity" => params.to_h}, headers:, timeout:)
|
|
17
|
+
@http.parse_data(response, Entity)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/tama/model.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Tama
|
|
6
|
+
module Model
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def define(*fields, required: [], types: {}, defaults: {}, transforms: {})
|
|
10
|
+
Class.new(Data.define(*fields)) do
|
|
11
|
+
define_singleton_method(:fields) { fields }
|
|
12
|
+
|
|
13
|
+
define_method(:initialize) do |**attributes|
|
|
14
|
+
unknown = attributes.keys - fields
|
|
15
|
+
unless unknown.empty?
|
|
16
|
+
raise Tama::Error::ValidationError.new("Unknown attributes: #{unknown.join(", ")}", errors: {attributes: unknown})
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
values = fields.to_h { |field| [field, attributes.fetch(field, defaults.fetch(field, nil))] }
|
|
20
|
+
Tama::Model.validate!(values, required:, types:, name: self.class.name)
|
|
21
|
+
super(**values)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
define_singleton_method(:parse) do |input|
|
|
25
|
+
data = Tama::Model.hash(input)
|
|
26
|
+
values = fields.to_h do |field|
|
|
27
|
+
value = data.key?(field.to_s) ? data[field.to_s] : data[field]
|
|
28
|
+
value = transforms.fetch(field).call(value) if !value.nil? && transforms.key?(field)
|
|
29
|
+
[field, value.nil? ? defaults.fetch(field, nil) : value]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
new(**values)
|
|
33
|
+
rescue Tama::Error::ParseError
|
|
34
|
+
raise
|
|
35
|
+
rescue Tama::Error => e
|
|
36
|
+
raise Tama::Error::ParseError, "Invalid #{name || "model"}: #{e.message}"
|
|
37
|
+
rescue => e
|
|
38
|
+
raise Tama::Error::ParseError, "Invalid #{name || "model"}: #{e.message}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def hash(input)
|
|
44
|
+
input = JSON.parse(input) if input.is_a?(String)
|
|
45
|
+
raise Tama::Error::ParseError, "Expected a JSON object" unless input.is_a?(Hash)
|
|
46
|
+
|
|
47
|
+
input
|
|
48
|
+
rescue JSON::ParserError => e
|
|
49
|
+
raise Tama::Error::ParseError, "Malformed JSON: #{e.message}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def validate!(values, required:, types:, name:)
|
|
53
|
+
missing = required.select { |field| blank?(values[field]) }
|
|
54
|
+
unless missing.empty?
|
|
55
|
+
raise Tama::Error::ValidationError.new("Missing required fields: #{missing.join(", ")}", errors: {required: missing})
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
types.each do |field, expected|
|
|
59
|
+
value = values[field]
|
|
60
|
+
next if value.nil? || valid_type?(value, expected)
|
|
61
|
+
|
|
62
|
+
raise Tama::Error::ValidationError.new(
|
|
63
|
+
"#{field} has an invalid type for #{name || "model"}",
|
|
64
|
+
errors: {field => "must be #{type_name(expected)}"}
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def blank?(value)
|
|
70
|
+
value.nil? || (value.is_a?(String) && value.empty?)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def valid_type?(value, expected)
|
|
74
|
+
Array(expected).any? { |type| value.is_a?(type) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def type_name(expected)
|
|
78
|
+
Array(expected).join(" or ")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tama
|
|
4
|
+
module Neural
|
|
5
|
+
Klass = Model.define(
|
|
6
|
+
:id, :space_id, :provision_state, :schema, :name, :description,
|
|
7
|
+
required: %i[provision_state name],
|
|
8
|
+
types: {
|
|
9
|
+
id: String, space_id: String, provision_state: String, schema: Hash,
|
|
10
|
+
name: String, description: String
|
|
11
|
+
}
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tama
|
|
4
|
+
module Neural
|
|
5
|
+
Operation = Model.define(
|
|
6
|
+
:id, :current_state, :class_id, :node_ids,
|
|
7
|
+
required: %i[current_state class_id],
|
|
8
|
+
types: {id: String, current_state: String, class_id: String, node_ids: Array},
|
|
9
|
+
transforms: {
|
|
10
|
+
node_ids: lambda do |value|
|
|
11
|
+
unless value.is_a?(Array) && value.all? { |node_id| node_id.is_a?(String) }
|
|
12
|
+
raise Error::ValidationError, "node_ids must be an array of strings"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
value
|
|
16
|
+
end
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tama
|
|
4
|
+
module Neural
|
|
5
|
+
OperationParams = Data.define(:chain_ids, :node_type) do
|
|
6
|
+
def initialize(chain_ids:, node_type: nil)
|
|
7
|
+
unless chain_ids.is_a?(Array) && !chain_ids.empty? && chain_ids.all? { |id| id.is_a?(String) && !id.empty? }
|
|
8
|
+
raise Error::ValidationError.new("chain_ids must contain at least one chain ID", errors: {chain_ids: "must contain non-empty strings"})
|
|
9
|
+
end
|
|
10
|
+
unless node_type.nil? || node_type.is_a?(String)
|
|
11
|
+
raise Error::ValidationError.new("node_type must be a string", errors: {node_type: "must be a string"})
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.parse(value)
|
|
18
|
+
data = Params.hash(value, "operation")
|
|
19
|
+
new(chain_ids: data["chain_ids"], node_type: data["node_type"])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_h
|
|
23
|
+
{"chain_ids" => chain_ids}.tap { |body| body["node_type"] = node_type unless node_type.nil? }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tama
|
|
4
|
+
module Neural
|
|
5
|
+
class Service
|
|
6
|
+
def initialize(http)
|
|
7
|
+
@http = http
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def get_space(slug, headers: {}, timeout: nil)
|
|
11
|
+
@http.validate_namespace!("provision")
|
|
12
|
+
path = "neural/spaces/#{HTTP.segment(slug, "slug")}"
|
|
13
|
+
@http.parse_data(@http.request(:get, path, headers:, timeout:), Space)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def get_class(space, name, headers: {}, timeout: nil)
|
|
17
|
+
@http.validate_namespace!("provision")
|
|
18
|
+
space_id = space.is_a?(Space) ? space.id : space
|
|
19
|
+
path = "neural/spaces/#{HTTP.segment(space_id, "space_id")}/classes/#{HTTP.segment(name, "name")}"
|
|
20
|
+
@http.parse_data(@http.request(:get, path, headers:, timeout:), Klass)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create_class_operation(klass, attributes = nil, headers: {}, timeout: nil, **attribute_keywords)
|
|
24
|
+
@http.validate_namespace!("provision")
|
|
25
|
+
class_id = klass.is_a?(Klass) ? klass.id : klass
|
|
26
|
+
attributes ||= attribute_keywords
|
|
27
|
+
params = attributes.is_a?(OperationParams) ? attributes : OperationParams.parse(attributes)
|
|
28
|
+
path = "neural/classes/#{HTTP.segment(class_id, "class_id")}/operations"
|
|
29
|
+
response = @http.request(:post, path, body: {"operation" => params.to_h}, headers:, timeout:)
|
|
30
|
+
@http.parse_data(response, Operation)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tama
|
|
4
|
+
module Neural
|
|
5
|
+
Space = Model.define(
|
|
6
|
+
:id, :name, :slug, :type, :provision_state,
|
|
7
|
+
required: %i[name type provision_state],
|
|
8
|
+
types: {id: String, name: String, slug: String, type: String, provision_state: String}
|
|
9
|
+
)
|
|
10
|
+
end
|
|
11
|
+
end
|
data/lib/tama/params.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tama
|
|
4
|
+
module Params
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def hash(value, name)
|
|
8
|
+
value = value.to_h if value.respond_to?(:to_h) && !value.is_a?(Hash)
|
|
9
|
+
raise Error::ValidationError.new("#{name} must be a hash", errors: {name => "must be a hash"}) unless value.is_a?(Hash)
|
|
10
|
+
|
|
11
|
+
value.transform_keys(&:to_s)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def require_string!(data, key)
|
|
15
|
+
value = data[key]
|
|
16
|
+
return value if value.is_a?(String) && !value.empty?
|
|
17
|
+
|
|
18
|
+
raise Error::ValidationError.new("#{key} is required", errors: {key => "must be a non-empty string"})
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def optional_string!(data, key)
|
|
22
|
+
value = data[key]
|
|
23
|
+
return if value.nil?
|
|
24
|
+
return value if value.is_a?(String)
|
|
25
|
+
|
|
26
|
+
raise Error::ValidationError.new("#{key} is invalid", errors: {key => "must be a string"})
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tama
|
|
4
|
+
module Perception
|
|
5
|
+
Chain = Model.define(
|
|
6
|
+
:id, :space_id, :name, :slug, :provision_state,
|
|
7
|
+
required: %i[name provision_state],
|
|
8
|
+
types: {id: String, space_id: String, name: String, slug: String, provision_state: String}
|
|
9
|
+
)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tama
|
|
4
|
+
module Perception
|
|
5
|
+
Concept = Model.define(
|
|
6
|
+
:id, :relation, :content, :generator,
|
|
7
|
+
required: %i[id relation content],
|
|
8
|
+
types: {id: String, relation: String, content: Hash, generator: Generator},
|
|
9
|
+
transforms: {generator: ->(value) { Generator.parse(value) }}
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
class << Concept
|
|
13
|
+
alias_method :parse_one, :parse
|
|
14
|
+
|
|
15
|
+
def parse(input)
|
|
16
|
+
return input.map { |item| parse_one(item) } if input.is_a?(Array)
|
|
17
|
+
|
|
18
|
+
parse_one(input)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tama
|
|
4
|
+
module Perception
|
|
5
|
+
Generator = Model.define(
|
|
6
|
+
:type, :reference, :parameters,
|
|
7
|
+
required: %i[type reference parameters],
|
|
8
|
+
types: {type: Symbol, reference: String, parameters: Hash},
|
|
9
|
+
transforms: {
|
|
10
|
+
type: lambda do |value|
|
|
11
|
+
type = value.to_sym
|
|
12
|
+
raise Error::ValidationError, "generator type must be model or module" unless %i[model module].include?(type)
|
|
13
|
+
|
|
14
|
+
type
|
|
15
|
+
end
|
|
16
|
+
}
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tama
|
|
4
|
+
module Perception
|
|
5
|
+
class Service
|
|
6
|
+
def initialize(http)
|
|
7
|
+
@http = http
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def get_chain(space, slug, headers: {}, timeout: nil)
|
|
11
|
+
@http.validate_namespace!("provision")
|
|
12
|
+
space_id = space.is_a?(Neural::Space) ? space.id : space
|
|
13
|
+
path = "perception/spaces/#{HTTP.segment(space_id, "space_id")}/chains/#{HTTP.segment(slug, "slug")}"
|
|
14
|
+
@http.parse_data(@http.request(:get, path, headers:, timeout:), Chain)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def list_concepts(entity_id, query: {}, headers: {}, timeout: nil)
|
|
18
|
+
@http.validate_namespace!("perception")
|
|
19
|
+
unless query.respond_to?(:each_pair)
|
|
20
|
+
raise Error::ValidationError.new("query must be a hash", errors: {query: "must be a hash"})
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
path = "entities/#{HTTP.segment(entity_id, "entity_id")}/concepts"
|
|
24
|
+
response = @http.request(:get, path, query:, headers:, timeout:)
|
|
25
|
+
@http.parse_data(response, Concept, list: true)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Tama
|
|
6
|
+
class SSEParser
|
|
7
|
+
def initialize(callback)
|
|
8
|
+
@callback = callback
|
|
9
|
+
@buffer = +""
|
|
10
|
+
@done = false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def feed(chunk)
|
|
14
|
+
return if @done
|
|
15
|
+
raise Error::ParseError, "SSE chunk must be a string" unless chunk.is_a?(String)
|
|
16
|
+
|
|
17
|
+
@buffer << chunk
|
|
18
|
+
consume_complete_events
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def finish
|
|
23
|
+
process_event(@buffer) unless @done || @buffer.empty?
|
|
24
|
+
@buffer.clear
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def consume_complete_events
|
|
31
|
+
while (match = @buffer.match(/\r\n\r\n|\n\n|\r\r/))
|
|
32
|
+
event = @buffer.slice!(0, match.begin(0))
|
|
33
|
+
@buffer.slice!(0, match[0].bytesize)
|
|
34
|
+
process_event(event)
|
|
35
|
+
break if @done
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def process_event(event)
|
|
40
|
+
data = event.split(/\r\n|\n|\r/).filter_map do |line|
|
|
41
|
+
next unless line.start_with?("data:")
|
|
42
|
+
|
|
43
|
+
value = line.delete_prefix("data:")
|
|
44
|
+
value = value.delete_prefix(" ")
|
|
45
|
+
value
|
|
46
|
+
end.join("\n")
|
|
47
|
+
|
|
48
|
+
return if data.empty?
|
|
49
|
+
|
|
50
|
+
if data == "[DONE]"
|
|
51
|
+
@done = true
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
@callback.call(JSON.parse(data))
|
|
56
|
+
rescue JSON::ParserError => e
|
|
57
|
+
raise Error::ParseError, "Malformed SSE data: #{e.message}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/tama/version.rb
ADDED
data/lib/tama.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "tama/version"
|
|
4
|
+
require_relative "tama/error"
|
|
5
|
+
require_relative "tama/model"
|
|
6
|
+
require_relative "tama/params"
|
|
7
|
+
require_relative "tama/neural/space"
|
|
8
|
+
require_relative "tama/neural/klass"
|
|
9
|
+
require_relative "tama/neural/operation"
|
|
10
|
+
require_relative "tama/neural/operation_params"
|
|
11
|
+
require_relative "tama/memory/entity"
|
|
12
|
+
require_relative "tama/memory/entity_params"
|
|
13
|
+
require_relative "tama/perception/chain"
|
|
14
|
+
require_relative "tama/perception/generator"
|
|
15
|
+
require_relative "tama/perception/concept"
|
|
16
|
+
require_relative "tama/agentic/author"
|
|
17
|
+
require_relative "tama/agentic/thread"
|
|
18
|
+
require_relative "tama/agentic/message_params"
|
|
19
|
+
require_relative "tama/agentic/message"
|
|
20
|
+
require_relative "tama/broadcast"
|
|
21
|
+
require_relative "tama/sse_parser"
|
|
22
|
+
require_relative "tama/http"
|
|
23
|
+
require_relative "tama/neural/service"
|
|
24
|
+
require_relative "tama/memory/service"
|
|
25
|
+
require_relative "tama/perception/service"
|
|
26
|
+
require_relative "tama/agentic/service"
|
|
27
|
+
require_relative "tama/client"
|