ruby-mana 0.4.0 → 0.5.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.
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "pycall"
5
+ rescue LoadError
6
+ raise LoadError, "pycall gem is required for Python support. Add `gem 'pycall'` to your Gemfile."
7
+ end
8
+
9
+ require "json"
10
+
11
+ module Mana
12
+ module Engines
13
+ class Python < Base
14
+ # Thread-local persistent Python state
15
+ # PyCall shares a single Python interpreter per process,
16
+ # but we track our own variable namespace
17
+ def self.namespace
18
+ Thread.current[:mana_py_namespace] ||= create_namespace
19
+ end
20
+
21
+ def self.create_namespace
22
+ PyCall.eval("dict()")
23
+ end
24
+
25
+ def self.reset!
26
+ ns = Thread.current[:mana_py_namespace]
27
+ if ns
28
+ PyCall.exec("pass") # ensure interpreter is alive
29
+ Thread.current[:mana_py_namespace] = nil
30
+ end
31
+ end
32
+
33
+ def execute(code)
34
+ ns = self.class.namespace
35
+
36
+ # 1. Inject Ruby variables into Python namespace
37
+ inject_ruby_vars(ns, code)
38
+
39
+ # 2. Inject the Ruby bridge for Python->Ruby callbacks
40
+ inject_ruby_bridge(ns)
41
+
42
+ # 3. Execute Python code in the namespace
43
+ PyCall.exec(code, locals: ns)
44
+
45
+ # 4. Extract declared variables back to Ruby
46
+ extract_py_vars(ns, code)
47
+
48
+ # Return the last expression value if possible
49
+ begin
50
+ ns["result"]
51
+ rescue
52
+ nil
53
+ end
54
+ rescue PyCall::PyError => e
55
+ raise Mana::Error, "Python execution error: #{e.message}"
56
+ rescue ArgumentError => e
57
+ raise Mana::Error, "Python execution error: #{e.message}"
58
+ end
59
+
60
+ private
61
+
62
+ def inject_ruby_vars(ns, code)
63
+ @binding.local_variables.each do |var_name|
64
+ value = @binding.local_variable_get(var_name)
65
+ serialized = serialize_for_py(value)
66
+ begin
67
+ ns[var_name.to_s] = serialized
68
+ rescue => e
69
+ next
70
+ end
71
+ end
72
+ end
73
+
74
+ # Inject a `ruby` bridge into the Python namespace.
75
+ #
76
+ # The bridge is a Ruby object with method_missing that proxies calls
77
+ # back to the Ruby binding. PyCall automatically wraps it so Python
78
+ # can call methods directly:
79
+ #
80
+ # ruby.method_name(arg1, arg2) -- call a Ruby method on the receiver
81
+ # ruby.read("var") -- read a Ruby local variable
82
+ # ruby.write("var", value) -- write a Ruby local variable
83
+ # ruby.call_proc("name", args) -- call a local proc/lambda by name
84
+ #
85
+ # Ruby objects (including the binding receiver) can also be injected
86
+ # directly and called from Python -- PyCall handles the wrapping.
87
+ def inject_ruby_bridge(ns)
88
+ ns["ruby"] = RubyBridge.new(@binding)
89
+ end
90
+
91
+ def extract_py_vars(ns, code)
92
+ declared_vars = extract_declared_vars(code)
93
+ declared_vars.each do |var_name|
94
+ next if var_name == "ruby" # skip the bridge object
95
+ begin
96
+ value = ns[var_name]
97
+ deserialized = deserialize_py(value)
98
+ write_var(var_name, deserialized)
99
+ rescue => e
100
+ next
101
+ end
102
+ end
103
+ end
104
+
105
+ def extract_declared_vars(code)
106
+ vars = []
107
+ # Match Python assignments: x = ..., but not x == ... or x += ...
108
+ code.scan(/^(\w+)\s*=[^=]/).each { |m| vars << m[0] }
109
+ # Also match augmented assignments: x += ..., x -= ...
110
+ code.scan(/^(\w+)\s*[+\-*\/]?=/).each { |m| vars << m[0] }
111
+ vars.uniq
112
+ end
113
+
114
+ # Serialize Ruby values for Python injection.
115
+ # Procs/lambdas and Ruby objects are passed directly --
116
+ # PyCall wraps them so Python can call their methods.
117
+ def serialize_for_py(value)
118
+ case value
119
+ when Numeric, String, TrueClass, FalseClass, NilClass
120
+ value
121
+ when Symbol
122
+ value.to_s
123
+ when Array
124
+ value.map { |v| serialize_for_py(v) }
125
+ when Hash
126
+ value.each_with_object({}) do |(k, v), h|
127
+ key = k.is_a?(Symbol) ? k.to_s : k
128
+ h[key] = serialize_for_py(v)
129
+ end
130
+ when Proc, Method
131
+ # Pass callables directly -- PyCall wraps them as Python callables
132
+ value
133
+ else
134
+ # Pass Ruby objects directly -- Python can call their methods via PyCall
135
+ value
136
+ end
137
+ end
138
+
139
+ def deserialize_py(value)
140
+ if defined?(PyCall::PyObjectWrapper) && value.is_a?(PyCall::PyObjectWrapper)
141
+ begin
142
+ value.to_a rescue value.to_s
143
+ rescue
144
+ value.to_s
145
+ end
146
+ else
147
+ value
148
+ end
149
+ end
150
+ end
151
+
152
+ # Bridge object injected as `ruby` in the Python namespace.
153
+ # Enables Python->Ruby callbacks via method_missing.
154
+ #
155
+ # Python usage:
156
+ # ruby.some_method(arg1, arg2) -- calls method on binding receiver
157
+ # ruby.read("var_name") -- reads a Ruby local variable
158
+ # ruby.write("var_name", val) -- writes a Ruby local variable
159
+ # ruby.call_proc("name", args) -- calls a local proc/lambda by name
160
+ class RubyBridge
161
+ def initialize(caller_binding)
162
+ @binding = caller_binding
163
+ @receiver = caller_binding.receiver
164
+ end
165
+
166
+ # Read a Ruby local variable
167
+ def read(name)
168
+ name = name.to_s.to_sym
169
+ if @binding.local_variables.include?(name)
170
+ @binding.local_variable_get(name)
171
+ else
172
+ raise NameError, "undefined Ruby variable '#{name}'"
173
+ end
174
+ end
175
+
176
+ # Write a Ruby local variable
177
+ def write(name, value)
178
+ @binding.local_variable_set(name.to_s.to_sym, value)
179
+ end
180
+
181
+ # Explicitly call a local proc/lambda by name
182
+ def call_proc(name, *args)
183
+ name = name.to_s.to_sym
184
+ if @binding.local_variables.include?(name)
185
+ val = @binding.local_variable_get(name)
186
+ if val.respond_to?(:call)
187
+ return val.call(*args)
188
+ end
189
+ end
190
+ raise NoMethodError, "no callable '#{name}' in Ruby scope"
191
+ end
192
+
193
+ # Proxy unknown method calls to the binding receiver.
194
+ # This lets Python do: ruby.some_method(args)
195
+ def method_missing(name, *args)
196
+ name_s = name.to_s
197
+
198
+ # First check local procs/lambdas
199
+ name_sym = name_s.to_sym
200
+ if @binding.local_variables.include?(name_sym)
201
+ val = @binding.local_variable_get(name_sym)
202
+ return val.call(*args) if val.respond_to?(:call)
203
+ end
204
+
205
+ # Then try the receiver (public methods only)
206
+ if @receiver.respond_to?(name_s)
207
+ return @receiver.public_send(name_s, *args)
208
+ end
209
+
210
+ super
211
+ end
212
+
213
+ def respond_to_missing?(name, include_private = false)
214
+ name_s = name.to_s
215
+ name_sym = name_s.to_sym
216
+
217
+ # Check local callables
218
+ if @binding.local_variables.include?(name_sym)
219
+ val = @binding.local_variable_get(name_sym)
220
+ return true if val.respond_to?(:call)
221
+ end
222
+
223
+ # Check receiver (public methods only, matching method_missing)
224
+ return true if @receiver.respond_to?(name_s)
225
+
226
+ super
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mana
4
+ module Engines
5
+ class Ruby < Base
6
+ def execute(code)
7
+ eval(code, @binding)
8
+ end
9
+ end
10
+ end
11
+ end
data/lib/mana/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mana
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/mana.rb CHANGED
@@ -11,6 +11,10 @@ require_relative "mana/namespace"
11
11
  require_relative "mana/memory_store"
12
12
  require_relative "mana/context_window"
13
13
  require_relative "mana/memory"
14
+ require_relative "mana/engines/base"
15
+ require_relative "mana/engines/llm"
16
+ require_relative "mana/engines/ruby_eval"
17
+ require_relative "mana/engines/detect"
14
18
  require_relative "mana/engine"
15
19
  require_relative "mana/introspect"
16
20
  require_relative "mana/compiler"
@@ -44,8 +48,12 @@ module Mana
44
48
  def reset!
45
49
  @config = Config.new
46
50
  EffectRegistry.clear!
51
+ Engines.reset_detector!
52
+ Engines::JavaScript.reset! if defined?(Engines::JavaScript)
53
+ Engines::Python.reset! if defined?(Engines::Python)
47
54
  Thread.current[:mana_memory] = nil
48
55
  Thread.current[:mana_mock] = nil
56
+ Thread.current[:mana_last_engine] = nil
49
57
  end
50
58
 
51
59
  # Define a custom effect that becomes an LLM tool
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-mana
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carl
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-21 00:00:00.000000000 Z
11
+ date: 2026-02-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: binding_of_caller
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mini_racer
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.16'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.16'
27
41
  description: |
28
42
  Mana lets you write natural language strings in Ruby that execute via LLM
29
43
  with full access to your program's live state. Read/write variables, call
@@ -36,6 +50,7 @@ files:
36
50
  - CHANGELOG.md
37
51
  - LICENSE
38
52
  - README.md
53
+ - data/lang-rules.yml
39
54
  - lib/mana.rb
40
55
  - lib/mana/backends/anthropic.rb
41
56
  - lib/mana/backends/base.rb
@@ -46,6 +61,12 @@ files:
46
61
  - lib/mana/context_window.rb
47
62
  - lib/mana/effect_registry.rb
48
63
  - lib/mana/engine.rb
64
+ - lib/mana/engines/base.rb
65
+ - lib/mana/engines/detect.rb
66
+ - lib/mana/engines/javascript.rb
67
+ - lib/mana/engines/llm.rb
68
+ - lib/mana/engines/python.rb
69
+ - lib/mana/engines/ruby_eval.rb
49
70
  - lib/mana/introspect.rb
50
71
  - lib/mana/memory.rb
51
72
  - lib/mana/memory_store.rb