ruby-mana 0.5.1 → 0.5.7

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.
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mana
4
- module Backends
5
- ANTHROPIC_PATTERNS = /^(claude-)/i
6
- OPENAI_PATTERNS = /^(gpt-|o1-|o3-|chatgpt-|dall-e|tts-|whisper-)/i
7
-
8
- def self.for(config)
9
- return config.backend if config.backend.is_a?(Base)
10
-
11
- case config.backend&.to_s
12
- when "openai" then OpenAI.new(config)
13
- when "anthropic" then Anthropic.new(config)
14
- else
15
- # Auto-detect from model name
16
- case config.model
17
- when ANTHROPIC_PATTERNS then Anthropic.new(config)
18
- else OpenAI.new(config) # Default to OpenAI (most compatible)
19
- end
20
- end
21
- end
22
- end
23
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mana
4
- module ContextWindow
5
- SIZES = {
6
- /claude-3-5-sonnet/ => 200_000,
7
- /claude-sonnet-4/ => 200_000,
8
- /claude-3-5-haiku/ => 200_000,
9
- /claude-3-opus/ => 200_000,
10
- /claude-opus-4/ => 200_000,
11
- /gpt-4o/ => 128_000,
12
- /gpt-4-turbo/ => 128_000,
13
- /gpt-3\.5/ => 16_385
14
- }.freeze
15
-
16
- DEFAULT = 128_000
17
-
18
- def self.detect(model_name)
19
- return DEFAULT unless model_name
20
-
21
- SIZES.each do |pattern, size|
22
- return size if model_name.match?(pattern)
23
- end
24
-
25
- DEFAULT
26
- end
27
- end
28
- end
@@ -1,155 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mana
4
- # Registry for custom effect handlers.
5
- #
6
- # Users define effects that become LLM tools automatically:
7
- #
8
- # Mana.define_effect :query_db,
9
- # description: "Execute a SQL query" do |sql:|
10
- # DB.execute(sql)
11
- # end
12
- #
13
- # The block's keyword parameters become the tool's input schema.
14
- # The block's return value is serialized and sent back to the LLM.
15
- module EffectRegistry
16
- class EffectDefinition
17
- attr_reader :name, :description, :handler, :params
18
-
19
- def initialize(name, description: nil, &handler)
20
- @name = name.to_s
21
- @description = description || @name
22
- @handler = handler
23
- @params = extract_params(handler)
24
- end
25
-
26
- # Convert to LLM tool definition
27
- def to_tool
28
- properties = {}
29
- required = []
30
-
31
- @params.each do |param|
32
- properties[param[:name]] = {
33
- type: infer_type(param[:default]),
34
- description: param[:name]
35
- }
36
- required << param[:name] if param[:required]
37
- end
38
-
39
- tool = {
40
- name: @name,
41
- description: @description,
42
- input_schema: {
43
- type: "object",
44
- properties: properties
45
- }
46
- }
47
- tool[:input_schema][:required] = required unless required.empty?
48
- tool
49
- end
50
-
51
- # Call the handler with LLM-provided input
52
- def call(input)
53
- kwargs = {}
54
- @params.each do |param|
55
- key = param[:name]
56
- if input.key?(key)
57
- kwargs[key.to_sym] = input[key]
58
- elsif param[:default] != :__mana_no_default__
59
- # Use block's default
60
- elsif param[:required]
61
- raise Mana::Error, "missing required parameter: #{key}"
62
- end
63
- end
64
-
65
- if kwargs.empty? && @params.empty?
66
- @handler.call
67
- else
68
- @handler.call(**kwargs)
69
- end
70
- end
71
-
72
- private
73
-
74
- def extract_params(block)
75
- return [] unless block
76
-
77
- block.parameters.map do |(type, name)|
78
- case type
79
- when :keyreq
80
- { name: name.to_s, required: true, default: :__mana_no_default__ }
81
- when :key
82
- # Try to get default value — not possible via reflection,
83
- # so we mark it as optional with unknown default
84
- { name: name.to_s, required: false, default: nil }
85
- when :keyrest
86
- # **kwargs — skip, can't generate schema
87
- nil
88
- else
89
- # Positional args — treat as required string params
90
- { name: name.to_s, required: true, default: :__mana_no_default__ } if name
91
- end
92
- end.compact
93
- end
94
-
95
- def infer_type(default)
96
- case default
97
- when Integer then "integer"
98
- when Float then "number"
99
- when TrueClass, FalseClass then "boolean"
100
- when Array then "array"
101
- when Hash then "object"
102
- else "string"
103
- end
104
- end
105
- end
106
-
107
- RESERVED_EFFECTS = %w[read_var write_var read_attr write_attr call_func done remember].freeze
108
-
109
- class << self
110
- def registry
111
- @registry ||= {}
112
- end
113
-
114
- def define(name, description: nil, &handler)
115
- name_s = name.to_s
116
- if RESERVED_EFFECTS.include?(name_s)
117
- raise Mana::Error, "cannot override built-in effect: #{name_s}"
118
- end
119
-
120
- registry[name_s] = EffectDefinition.new(name, description: description, &handler)
121
- end
122
-
123
- def undefine(name)
124
- registry.delete(name.to_s)
125
- end
126
-
127
- def defined?(name)
128
- registry.key?(name.to_s)
129
- end
130
-
131
- def get(name)
132
- registry[name.to_s]
133
- end
134
-
135
- # Generate tool definitions for all registered effects
136
- def tool_definitions
137
- registry.values.map(&:to_tool)
138
- end
139
-
140
- def clear!
141
- @registry = {}
142
- end
143
-
144
- # Handle a tool call if it matches a registered effect
145
- # Returns [handled, result] — handled is true if we processed it
146
- def handle(name, input)
147
- effect = get(name)
148
- return [false, nil] unless effect
149
-
150
- result = effect.call(input)
151
- [true, result]
152
- end
153
- end
154
- end
155
- end
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mana
4
- module Engines
5
- class Base
6
- attr_reader :config, :binding
7
-
8
- def initialize(caller_binding, config = Mana.config)
9
- @binding = caller_binding
10
- @config = config
11
- end
12
-
13
- # --- Capability queries ---
14
- # Subclasses override to declare what they support.
15
-
16
- # Is this an execution engine (Ruby/JS/Python) or a reasoning engine (LLM)?
17
- # Execution engines can execute code, hold state, and be called bidirectionally.
18
- # Reasoning engines process natural language but cannot hold references or state.
19
- def execution_engine?
20
- true
21
- end
22
-
23
- # Can this engine hold remote references to objects in other engines?
24
- # Derived from execution_engine? — only execution engines support remote refs.
25
- def supports_remote_ref?
26
- execution_engine?
27
- end
28
-
29
- # Can code in this engine call back into another engine (bidirectional)?
30
- # Derived from execution_engine? — only execution engines support bidirectional calls.
31
- def supports_bidirectional?
32
- execution_engine?
33
- end
34
-
35
- # Does this engine maintain mutable state across calls?
36
- # Derived from execution_engine? — only execution engines maintain state.
37
- def supports_state?
38
- execution_engine?
39
- end
40
-
41
- # Execute code/prompt in this engine, return the result
42
- # Subclasses must implement this
43
- def execute(code)
44
- raise NotImplementedError, "#{self.class}#execute not implemented"
45
- end
46
-
47
- # Read a variable from the Ruby binding
48
- def read_var(name)
49
- if @binding.local_variables.include?(name.to_sym)
50
- @binding.local_variable_get(name.to_sym)
51
- elsif @binding.receiver.respond_to?(name.to_sym, true)
52
- @binding.receiver.send(name.to_sym)
53
- else
54
- raise NameError, "undefined variable: #{name}"
55
- end
56
- end
57
-
58
- # Write a variable to the Ruby binding
59
- def write_var(name, value)
60
- @binding.local_variable_set(name.to_sym, value)
61
- end
62
-
63
- # Serialize a Ruby value for cross-language transfer
64
- # Simple types: copy. Complex objects: will be remote refs (future)
65
- def serialize(value)
66
- case value
67
- when Numeric, String, Symbol, TrueClass, FalseClass, NilClass
68
- value
69
- when Array
70
- value.map { |v| serialize(v) }
71
- when Hash
72
- value.transform_values { |v| serialize(v) }
73
- else
74
- value.to_s
75
- end
76
- end
77
- end
78
- end
79
- end
@@ -1,93 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "yaml"
4
-
5
- module Mana
6
- module Engines
7
- RULES_PATH = File.join(__dir__, "..", "..", "..", "data", "lang-rules.yml")
8
-
9
- class Detector
10
- attr_reader :rules
11
-
12
- def initialize(rules_path = RULES_PATH)
13
- @rules = YAML.safe_load(File.read(rules_path))["languages"]
14
- end
15
-
16
- # Detect language, return engine class
17
- # context: previous detection result (for context inference)
18
- def detect(code, context: nil)
19
- scores = {}
20
- @rules.each do |lang, rule_set|
21
- scores[lang] = score(code, rule_set)
22
- end
23
-
24
- # Context inference: boost previous language slightly
25
- # Only boost if there's already some evidence (score > 0)
26
- if context && scores[context] && scores[context] > 0
27
- scores[context] += 2
28
- end
29
-
30
- best = scores.max_by { |_, v| v }
31
-
32
- # If best score is very low, default to natural_language (LLM)
33
- if best[1] <= 0
34
- return engine_for("natural_language")
35
- end
36
-
37
- engine_for(best[0])
38
- end
39
-
40
- private
41
-
42
- def score(code, rule_set)
43
- s = 0
44
- # Strong signals: +3 each
45
- (rule_set["strong"] || []).each { |token| s += 3 if code.include?(token) }
46
- # Weak signals: +1 each
47
- (rule_set["weak"] || []).each { |token| s += 1 if code.include?(token) }
48
- # Anti signals: -5 each (strong negative)
49
- (rule_set["anti"] || []).each { |token| s -= 5 if code.include?(token) }
50
- # Pattern signals: +4 each
51
- (rule_set["patterns"] || []).each do |pattern|
52
- s += 4 if code.match?(Regexp.new(pattern))
53
- end
54
- s
55
- end
56
-
57
- def engine_for(lang)
58
- case lang
59
- when "javascript" then load_js_engine
60
- when "python" then load_py_engine
61
- when "ruby" then Engines::Ruby
62
- else Engines::LLM
63
- end
64
- end
65
-
66
- def load_js_engine
67
- require_relative "javascript"
68
- Engines::JavaScript
69
- rescue LoadError => e
70
- warn "Mana: JavaScript engine unavailable (#{e.message}), falling back to LLM"
71
- Engines::LLM
72
- end
73
-
74
- def load_py_engine
75
- require_relative "python"
76
- Engines::Python
77
- rescue LoadError => e
78
- warn "Mana: Python engine unavailable (#{e.message}), falling back to LLM"
79
- Engines::LLM
80
- end
81
- end
82
-
83
- # Module-level convenience method
84
- def self.detect(code, context: nil)
85
- @detector ||= Detector.new
86
- @detector.detect(code, context: context)
87
- end
88
-
89
- def self.reset_detector!
90
- @detector = nil
91
- end
92
- end
93
- end
@@ -1,314 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- begin
4
- require "mini_racer"
5
- rescue LoadError
6
- raise LoadError, "mini_racer gem is required for JavaScript support. Add `gem 'mini_racer'` to your Gemfile."
7
- end
8
-
9
- require "json"
10
- require "set"
11
-
12
- module Mana
13
- module Engines
14
- class JavaScript < Base
15
- # JS helper code that creates Proxy wrappers for remote Ruby objects.
16
- # Injected once per V8 context.
17
- JS_PROXY_HELPER = <<~JS
18
- (function() {
19
- if (typeof __mana_create_proxy !== 'undefined') return;
20
-
21
- // FinalizationRegistry for automatic GC of remote refs.
22
- // When a JS proxy is garbage collected, notify Ruby to release the original object.
23
- if (typeof FinalizationRegistry !== 'undefined') {
24
- globalThis.__mana_ref_gc = new FinalizationRegistry(function(refId) {
25
- try { ruby.__ref_release(refId); } catch(e) {}
26
- });
27
- }
28
-
29
- globalThis.__mana_create_proxy = function(refId, typeName) {
30
- var proxy = new Proxy({ __mana_ref: refId, __mana_type: typeName }, {
31
- get: function(target, prop) {
32
- if (prop === '__mana_ref') return target.__mana_ref;
33
- if (prop === '__mana_type') return target.__mana_type;
34
- if (prop === '__mana_alive') return ruby.__ref_alive(refId);
35
- if (prop === 'release') return function() { ruby.__ref_release(refId); };
36
- if (prop === 'toString' || prop === Symbol.toPrimitive) {
37
- return function() { return ruby.__ref_to_s(refId); };
38
- }
39
- if (prop === 'inspect') {
40
- return function() { return 'RemoteRef<' + typeName + '#' + refId + '>'; };
41
- }
42
- if (typeof prop === 'symbol') return undefined;
43
- return function() {
44
- var args = Array.prototype.slice.call(arguments);
45
- return ruby.__ref_call(refId, prop, JSON.stringify(args));
46
- };
47
- }
48
- });
49
-
50
- // Register with FinalizationRegistry so GC triggers release
51
- if (globalThis.__mana_ref_gc) {
52
- globalThis.__mana_ref_gc.register(proxy, refId);
53
- }
54
-
55
- return proxy;
56
- };
57
- })();
58
- JS
59
-
60
- # Thread-local persistent V8 context (lazy-loaded, long-running)
61
- def self.context
62
- Thread.current[:mana_js_context] ||= create_context
63
- end
64
-
65
- def self.create_context
66
- MiniRacer::Context.new
67
- end
68
-
69
- def self.reset!
70
- ctx = Thread.current[:mana_js_context]
71
- ctx&.dispose
72
- Thread.current[:mana_js_context] = nil
73
- Thread.current[:mana_js_callbacks_attached] = nil
74
- ObjectRegistry.reset!
75
- end
76
-
77
- def execute(code)
78
- ctx = self.class.context
79
-
80
- # 1. Attach Ruby callbacks (methods + effects + ref operations)
81
- attach_ruby_callbacks(ctx)
82
-
83
- # 2. Inject Ruby variables into JS scope
84
- inject_ruby_vars(ctx, code)
85
-
86
- # 3. Execute the JS code
87
- result = ctx.eval(code)
88
-
89
- # 4. Extract any new/modified variables back to Ruby binding
90
- extract_js_vars(ctx, code)
91
-
92
- result
93
- end
94
-
95
- private
96
-
97
- def attach_ruby_callbacks(ctx)
98
- attached = Thread.current[:mana_js_callbacks_attached] ||= Set.new
99
-
100
- # Install the JS Proxy helper (once per context)
101
- unless attached.include?("__proxy_helper")
102
- ctx.eval(JS_PROXY_HELPER)
103
- attached << "__proxy_helper"
104
- end
105
-
106
- # ruby.read / ruby.write -- variable bridge
107
- unless attached.include?("ruby.read")
108
- bnd = @binding
109
- ctx.attach("ruby.read", proc { |name|
110
- sym = name.to_sym
111
- if bnd.local_variables.include?(sym)
112
- val = bnd.local_variable_get(sym)
113
- json_safe(val)
114
- else
115
- nil
116
- end
117
- })
118
- attached << "ruby.read"
119
- end
120
-
121
- unless attached.include?("ruby.write")
122
- bnd = @binding
123
- ctx.attach("ruby.write", proc { |name, value|
124
- bnd.local_variable_set(name.to_sym, value)
125
- value
126
- })
127
- attached << "ruby.write"
128
- end
129
-
130
- # Remote reference operations
131
- attach_ref_callbacks(ctx, attached)
132
-
133
- # Attach methods from the caller's receiver
134
- attach_receiver_methods(ctx, attached)
135
-
136
- # Attach registered Mana effects
137
- attach_effects(ctx, attached)
138
- end
139
-
140
- # Attach callbacks for operating on remote Ruby object references from JS.
141
- def attach_ref_callbacks(ctx, attached)
142
- registry = ObjectRegistry.current
143
-
144
- unless attached.include?("ruby.__ref_call")
145
- ctx.attach("ruby.__ref_call", proc { |ref_id, method_name, args_json|
146
- obj = registry.get(ref_id)
147
- raise "Remote reference #{ref_id} has been released" unless obj
148
-
149
- args = args_json ? JSON.parse(args_json) : []
150
- result = obj.public_send(method_name.to_sym, *args)
151
- json_safe(result)
152
- })
153
- attached << "ruby.__ref_call"
154
- end
155
-
156
- unless attached.include?("ruby.__ref_release")
157
- ctx.attach("ruby.__ref_release", proc { |ref_id|
158
- registry.release(ref_id)
159
- nil
160
- })
161
- attached << "ruby.__ref_release"
162
- end
163
-
164
- unless attached.include?("ruby.__ref_alive")
165
- ctx.attach("ruby.__ref_alive", proc { |ref_id|
166
- registry.registered?(ref_id)
167
- })
168
- attached << "ruby.__ref_alive"
169
- end
170
-
171
- unless attached.include?("ruby.__ref_to_s")
172
- ctx.attach("ruby.__ref_to_s", proc { |ref_id|
173
- obj = registry.get(ref_id)
174
- obj ? obj.to_s : "released"
175
- })
176
- attached << "ruby.__ref_to_s"
177
- end
178
- end
179
-
180
- def attach_receiver_methods(ctx, attached)
181
- receiver = @binding.receiver
182
- user_methods = receiver.class.instance_methods(false) -
183
- Object.instance_methods -
184
- [:~@]
185
-
186
- user_methods.each do |method_name|
187
- key = "ruby.#{method_name}"
188
- next if attached.include?(key)
189
-
190
- recv = receiver
191
- ctx.attach(key, proc { |*args| json_safe(recv.public_send(method_name, *args)) })
192
- attached << key
193
- end
194
- end
195
-
196
- def attach_effects(ctx, attached)
197
- Mana::EffectRegistry.registry.each do |name, effect|
198
- key = "ruby.#{name}"
199
- next if attached.include?(key)
200
-
201
- eff = effect
202
- ctx.attach(key, proc { |*args|
203
- input = if args.length == 1 && args[0].is_a?(Hash)
204
- args[0]
205
- elsif eff.params.length == args.length
206
- eff.params.zip(args).to_h { |p, v| [p[:name], v] }
207
- else
208
- {}
209
- end
210
- json_safe(eff.call(input))
211
- })
212
- attached << key
213
- end
214
- end
215
-
216
- def inject_ruby_vars(ctx, code)
217
- @binding.local_variables.each do |var_name|
218
- next unless code.match?(/\b#{Regexp.escape(var_name.to_s)}\b/)
219
-
220
- value = @binding.local_variable_get(var_name)
221
- inject_value(ctx, var_name.to_s, value)
222
- rescue => e
223
- next
224
- end
225
- end
226
-
227
- # Inject a single value into the JS context.
228
- # Simple types are serialized to JSON. Complex objects become remote ref proxies.
229
- def inject_value(ctx, name, value)
230
- case value
231
- when Numeric, String, TrueClass, FalseClass, NilClass
232
- ctx.eval("var #{name} = #{JSON.generate(value)}")
233
- when Symbol
234
- ctx.eval("var #{name} = #{JSON.generate(value.to_s)}")
235
- when Array
236
- ctx.eval("var #{name} = #{JSON.generate(serialize_array(value))}")
237
- when Hash
238
- ctx.eval("var #{name} = #{JSON.generate(serialize_hash(value))}")
239
- else
240
- # Complex object → register and create JS Proxy
241
- ref_id = ObjectRegistry.current.register(value)
242
- ctx.eval("var #{name} = __mana_create_proxy(#{ref_id}, #{JSON.generate(value.class.name)})")
243
- end
244
- end
245
-
246
- def serialize_array(arr)
247
- arr.map { |v| simple_value?(v) ? serialize_simple(v) : v.to_s }
248
- end
249
-
250
- def serialize_hash(hash)
251
- hash.transform_keys(&:to_s).transform_values { |v|
252
- simple_value?(v) ? serialize_simple(v) : v.to_s
253
- }
254
- end
255
-
256
- def simple_value?(value)
257
- case value
258
- when Numeric, String, Symbol, TrueClass, FalseClass, NilClass, Array, Hash
259
- true
260
- else
261
- false
262
- end
263
- end
264
-
265
- def serialize_simple(value)
266
- case value
267
- when Symbol then value.to_s
268
- when Array then serialize_array(value)
269
- when Hash then serialize_hash(value)
270
- else value
271
- end
272
- end
273
-
274
- def extract_js_vars(ctx, code)
275
- declared_vars = extract_declared_vars(code)
276
- declared_vars.each do |var_name|
277
- begin
278
- value = ctx.eval(var_name)
279
- deserialized = deserialize(value)
280
- write_var(var_name, deserialized)
281
- rescue MiniRacer::RuntimeError
282
- next
283
- end
284
- end
285
- end
286
-
287
- def extract_declared_vars(code)
288
- vars = []
289
- code.scan(/\b(?:const|let|var)\s+(\w+)\s*=/).each { |m| vars << m[0] }
290
- code.scan(/^(\w+)\s*=[^=>]/).each { |m| vars << m[0] }
291
- vars.uniq
292
- end
293
-
294
- def deserialize(value)
295
- value
296
- end
297
-
298
- def json_safe(value)
299
- case value
300
- when Numeric, String, TrueClass, FalseClass, NilClass
301
- value
302
- when Symbol
303
- value.to_s
304
- when Array
305
- value.map { |v| json_safe(v) }
306
- when Hash
307
- value.transform_keys(&:to_s).transform_values { |v| json_safe(v) }
308
- else
309
- value.to_s
310
- end
311
- end
312
- end
313
- end
314
- end