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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -1
- data/README.md +68 -0
- data/data/lang-rules.yml +196 -0
- data/lib/mana/compiler.rb +1 -1
- data/lib/mana/engine.rb +38 -432
- data/lib/mana/engines/base.rb +51 -0
- data/lib/mana/engines/detect.rb +93 -0
- data/lib/mana/engines/javascript.rb +90 -0
- data/lib/mana/engines/llm.rb +459 -0
- data/lib/mana/engines/python.rb +230 -0
- data/lib/mana/engines/ruby_eval.rb +11 -0
- data/lib/mana/version.rb +1 -1
- data/lib/mana.rb +8 -0
- metadata +23 -2
|
@@ -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
|
data/lib/mana/version.rb
CHANGED
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
|
+
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-
|
|
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
|