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
data/lib/mana/engine.rb
CHANGED
|
@@ -1,467 +1,73 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
3
|
module Mana
|
|
6
4
|
class Engine
|
|
7
|
-
TOOLS = [
|
|
8
|
-
{
|
|
9
|
-
name: "read_var",
|
|
10
|
-
description: "Read a variable value from the Ruby scope.",
|
|
11
|
-
input_schema: {
|
|
12
|
-
type: "object",
|
|
13
|
-
properties: { name: { type: "string", description: "Variable name" } },
|
|
14
|
-
required: ["name"]
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
name: "write_var",
|
|
19
|
-
description: "Write a value to a variable in the Ruby scope. Creates the variable if it doesn't exist.",
|
|
20
|
-
input_schema: {
|
|
21
|
-
type: "object",
|
|
22
|
-
properties: {
|
|
23
|
-
name: { type: "string", description: "Variable name" },
|
|
24
|
-
value: { description: "Value to assign (any JSON type)" }
|
|
25
|
-
},
|
|
26
|
-
required: %w[name value]
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
name: "read_attr",
|
|
31
|
-
description: "Read an attribute from a Ruby object.",
|
|
32
|
-
input_schema: {
|
|
33
|
-
type: "object",
|
|
34
|
-
properties: {
|
|
35
|
-
obj: { type: "string", description: "Variable name holding the object" },
|
|
36
|
-
attr: { type: "string", description: "Attribute name to read" }
|
|
37
|
-
},
|
|
38
|
-
required: %w[obj attr]
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
name: "write_attr",
|
|
43
|
-
description: "Set an attribute on a Ruby object.",
|
|
44
|
-
input_schema: {
|
|
45
|
-
type: "object",
|
|
46
|
-
properties: {
|
|
47
|
-
obj: { type: "string", description: "Variable name holding the object" },
|
|
48
|
-
attr: { type: "string", description: "Attribute name to set" },
|
|
49
|
-
value: { description: "Value to assign" }
|
|
50
|
-
},
|
|
51
|
-
required: %w[obj attr value]
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
name: "call_func",
|
|
56
|
-
description: "Call a Ruby method/function available in the current scope.",
|
|
57
|
-
input_schema: {
|
|
58
|
-
type: "object",
|
|
59
|
-
properties: {
|
|
60
|
-
name: { type: "string", description: "Function/method name" },
|
|
61
|
-
args: { type: "array", description: "Arguments to pass", items: {} }
|
|
62
|
-
},
|
|
63
|
-
required: ["name"]
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
name: "done",
|
|
68
|
-
description: "Signal that the task is complete.",
|
|
69
|
-
input_schema: {
|
|
70
|
-
type: "object",
|
|
71
|
-
properties: {
|
|
72
|
-
result: { description: "Optional return value" }
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
].freeze
|
|
77
|
-
|
|
78
|
-
REMEMBER_TOOL = {
|
|
79
|
-
name: "remember",
|
|
80
|
-
description: "Store a fact in long-term memory. This memory persists across script executions. Use when the user explicitly asks to remember something.",
|
|
81
|
-
input_schema: {
|
|
82
|
-
type: "object",
|
|
83
|
-
properties: { content: { type: "string", description: "The fact to remember" } },
|
|
84
|
-
required: ["content"]
|
|
85
|
-
}
|
|
86
|
-
}.freeze
|
|
87
|
-
|
|
88
5
|
class << self
|
|
89
6
|
def run(prompt, caller_binding)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def handler_stack
|
|
94
|
-
Thread.current[:mana_handlers] ||= []
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def with_handler(handler = nil, **opts, &block)
|
|
98
|
-
handler_stack.push(handler)
|
|
99
|
-
block.call
|
|
100
|
-
ensure
|
|
101
|
-
handler_stack.pop
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Built-in tools + remember + any registered custom effects
|
|
105
|
-
def all_tools
|
|
106
|
-
tools = TOOLS.dup
|
|
107
|
-
tools << REMEMBER_TOOL unless Memory.incognito?
|
|
108
|
-
tools + Mana::EffectRegistry.tool_definitions
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def initialize(prompt, caller_binding)
|
|
113
|
-
@prompt = prompt
|
|
114
|
-
@binding = caller_binding
|
|
115
|
-
@config = Mana.config
|
|
116
|
-
@caller_path = caller_source_path
|
|
117
|
-
@incognito = Memory.incognito?
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def execute
|
|
121
|
-
# Mock mode: match prompt and return stubbed values, no API calls
|
|
122
|
-
return handle_mock(@prompt) if Mana.mock_active?
|
|
123
|
-
|
|
124
|
-
Thread.current[:mana_depth] ||= 0
|
|
125
|
-
Thread.current[:mana_depth] += 1
|
|
126
|
-
nested = Thread.current[:mana_depth] > 1
|
|
127
|
-
|
|
128
|
-
# Nested calls get fresh short-term memory but share long-term
|
|
129
|
-
if nested && !@incognito
|
|
130
|
-
outer_memory = Thread.current[:mana_memory]
|
|
131
|
-
inner_memory = Mana::Memory.new
|
|
132
|
-
long_term = outer_memory&.long_term || []
|
|
133
|
-
inner_memory.instance_variable_set(:@long_term, long_term)
|
|
134
|
-
inner_memory.instance_variable_set(:@next_id, (long_term.map { |m| m[:id] }.max || 0) + 1)
|
|
135
|
-
Thread.current[:mana_memory] = inner_memory
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
context = build_context(@prompt)
|
|
139
|
-
system_prompt = build_system_prompt(context)
|
|
140
|
-
|
|
141
|
-
# Use memory's short_term messages (auto per-thread), or fresh if incognito
|
|
142
|
-
memory = @incognito ? nil : Memory.current
|
|
143
|
-
memory&.wait_for_compaction
|
|
144
|
-
|
|
145
|
-
messages = memory ? memory.short_term : []
|
|
146
|
-
messages << { role: "user", content: @prompt }
|
|
147
|
-
|
|
148
|
-
iterations = 0
|
|
149
|
-
done_result = nil
|
|
150
|
-
|
|
151
|
-
loop do
|
|
152
|
-
iterations += 1
|
|
153
|
-
raise MaxIterationsError, "exceeded #{@config.max_iterations} iterations" if iterations > @config.max_iterations
|
|
154
|
-
|
|
155
|
-
response = llm_call(system_prompt, messages)
|
|
156
|
-
tool_uses = extract_tool_uses(response)
|
|
157
|
-
|
|
158
|
-
break if tool_uses.empty?
|
|
159
|
-
|
|
160
|
-
# Append assistant message
|
|
161
|
-
messages << { role: "assistant", content: response }
|
|
162
|
-
|
|
163
|
-
# Process each tool use
|
|
164
|
-
tool_results = tool_uses.map do |tu|
|
|
165
|
-
result = handle_effect(tu, memory)
|
|
166
|
-
done_result = (tu[:input][:result] || tu[:input]["result"]) if tu[:name] == "done"
|
|
167
|
-
{ type: "tool_result", tool_use_id: tu[:id], content: result.to_s }
|
|
7
|
+
# Mock mode check (before anything else)
|
|
8
|
+
if Mana.mock_active?
|
|
9
|
+
return Engines::LLM.new(caller_binding).handle_mock(prompt)
|
|
168
10
|
end
|
|
169
11
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# Append a final assistant summary so LLM has full context next call
|
|
176
|
-
if memory && done_result
|
|
177
|
-
messages << { role: "assistant", content: [{ type: "text", text: "Done: #{done_result}" }] }
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# Schedule compaction if needed (runs in background, skip for nested)
|
|
181
|
-
memory&.schedule_compaction unless nested
|
|
182
|
-
|
|
183
|
-
done_result
|
|
184
|
-
ensure
|
|
185
|
-
if nested && !@incognito
|
|
186
|
-
Thread.current[:mana_memory] = outer_memory
|
|
187
|
-
end
|
|
188
|
-
Thread.current[:mana_depth] -= 1 if Thread.current[:mana_depth]
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
private
|
|
192
|
-
|
|
193
|
-
# --- Mock Handling ---
|
|
194
|
-
|
|
195
|
-
def handle_mock(prompt)
|
|
196
|
-
mock = Mana.current_mock
|
|
197
|
-
stub = mock.match(prompt)
|
|
198
|
-
|
|
199
|
-
unless stub
|
|
200
|
-
truncated = prompt.length > 60 ? "#{prompt[0..57]}..." : prompt
|
|
201
|
-
raise MockError, "No mock matched: \"#{truncated}\"\n Add: mock_prompt \"#{truncated}\", _return: \"...\""
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
values = if stub.block
|
|
205
|
-
stub.block.call(prompt)
|
|
206
|
-
else
|
|
207
|
-
stub.values.dup
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
return_value = values.delete(:_return)
|
|
12
|
+
# Detect language engine
|
|
13
|
+
engine_class = detect_engine(prompt)
|
|
14
|
+
Thread.current[:mana_last_engine] = engine_name(engine_class)
|
|
211
15
|
|
|
212
|
-
|
|
213
|
-
|
|
16
|
+
# Create engine and execute
|
|
17
|
+
engine = engine_class.new(caller_binding)
|
|
18
|
+
engine.execute(prompt)
|
|
214
19
|
end
|
|
215
20
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
memory = Memory.current
|
|
219
|
-
if memory
|
|
220
|
-
memory.short_term << { role: "user", content: prompt }
|
|
221
|
-
memory.short_term << { role: "assistant", content: [{ type: "text", text: "Done: #{return_value || values.inspect}" }] }
|
|
222
|
-
end
|
|
21
|
+
def detect_engine(code)
|
|
22
|
+
Engines.detect(code, context: Thread.current[:mana_last_engine])
|
|
223
23
|
end
|
|
224
24
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
var_names = prompt.scan(/<(\w+)>/).flatten.uniq
|
|
232
|
-
ctx = {}
|
|
233
|
-
var_names.each do |name|
|
|
234
|
-
val = resolve(name)
|
|
235
|
-
ctx[name] = serialize_value(val)
|
|
236
|
-
rescue NameError
|
|
237
|
-
# Variable doesn't exist yet — will be created by LLM
|
|
238
|
-
end
|
|
239
|
-
ctx
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def build_system_prompt(context)
|
|
243
|
-
parts = [
|
|
244
|
-
"You are embedded inside a Ruby program. You interact with the program's live state using the provided tools.",
|
|
245
|
-
"",
|
|
246
|
-
"Rules:",
|
|
247
|
-
"- Use read_var / read_attr to inspect variables and objects.",
|
|
248
|
-
"- Use write_var to create or update variables in the Ruby scope.",
|
|
249
|
-
"- Use write_attr to set attributes on Ruby objects.",
|
|
250
|
-
"- Use call_func to call Ruby methods available in scope.",
|
|
251
|
-
"- Call done when the task is complete.",
|
|
252
|
-
"- When the user references <var>, that's a variable in scope.",
|
|
253
|
-
"- If a referenced variable doesn't exist yet, the user expects you to create it with write_var.",
|
|
254
|
-
"- Be precise with types: use numbers for numeric values, arrays for lists, strings for text."
|
|
255
|
-
]
|
|
256
|
-
|
|
257
|
-
# Inject long-term memories or incognito notice
|
|
258
|
-
if @incognito
|
|
259
|
-
parts << ""
|
|
260
|
-
parts << "You are in incognito mode. The remember tool is disabled. No memories will be loaded or saved."
|
|
261
|
-
else
|
|
262
|
-
memory = Memory.current
|
|
263
|
-
if memory
|
|
264
|
-
# Inject summaries from compaction
|
|
265
|
-
unless memory.summaries.empty?
|
|
266
|
-
parts << ""
|
|
267
|
-
parts << "Previous conversation summary:"
|
|
268
|
-
memory.summaries.each { |s| parts << " #{s}" }
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
unless memory.long_term.empty?
|
|
272
|
-
parts << ""
|
|
273
|
-
parts << "Long-term memories (persistent across executions):"
|
|
274
|
-
memory.long_term.each { |m| parts << "- #{m[:content]}" }
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
unless memory.long_term.empty?
|
|
278
|
-
parts << ""
|
|
279
|
-
parts << "You have a `remember` tool to store new facts in long-term memory when the user asks."
|
|
280
|
-
end
|
|
25
|
+
def engine_name(klass)
|
|
26
|
+
case klass.name
|
|
27
|
+
when /JavaScript/ then "javascript"
|
|
28
|
+
when /Python/ then "python"
|
|
29
|
+
when /Ruby/ then "ruby"
|
|
30
|
+
else "natural_language"
|
|
281
31
|
end
|
|
282
32
|
end
|
|
283
33
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
context.each { |k, v| parts << " #{k} = #{v}" }
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
# Discover available functions from caller's source
|
|
291
|
-
methods = begin
|
|
292
|
-
Mana::Introspect.methods_from_file(@caller_path)
|
|
293
|
-
rescue => _e
|
|
294
|
-
[]
|
|
295
|
-
end
|
|
296
|
-
unless methods.empty?
|
|
297
|
-
parts << ""
|
|
298
|
-
parts << Mana::Introspect.format_for_prompt(methods)
|
|
34
|
+
# Delegate to LLM engine for backward compatibility
|
|
35
|
+
def handler_stack
|
|
36
|
+
Engines::LLM.handler_stack
|
|
299
37
|
end
|
|
300
38
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
unless custom_effects.empty?
|
|
304
|
-
parts << ""
|
|
305
|
-
parts << "Custom tools available:"
|
|
306
|
-
custom_effects.each do |t|
|
|
307
|
-
params = (t[:input_schema][:properties] || {}).keys.join(", ")
|
|
308
|
-
parts << " #{t[:name]}(#{params}) — #{t[:description]}"
|
|
309
|
-
end
|
|
39
|
+
def with_handler(handler = nil, **opts, &block)
|
|
40
|
+
Engines::LLM.with_handler(handler, **opts, &block)
|
|
310
41
|
end
|
|
311
42
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
# --- Effect Handling ---
|
|
316
|
-
|
|
317
|
-
def handle_effect(tool_use, memory = nil)
|
|
318
|
-
name = tool_use[:name]
|
|
319
|
-
input = tool_use[:input] || {}
|
|
320
|
-
# Normalize keys to strings for consistent access
|
|
321
|
-
input = input.transform_keys(&:to_s) if input.is_a?(Hash)
|
|
322
|
-
|
|
323
|
-
# Check handler stack first (legacy)
|
|
324
|
-
handler = self.class.handler_stack.last
|
|
325
|
-
return handler.call(name, input) if handler && handler.respond_to?(:call)
|
|
326
|
-
|
|
327
|
-
# Check custom effect registry
|
|
328
|
-
handled, result = Mana::EffectRegistry.handle(name, input)
|
|
329
|
-
return serialize_value(result) if handled
|
|
330
|
-
|
|
331
|
-
case name
|
|
332
|
-
when "read_var"
|
|
333
|
-
serialize_value(resolve(input["name"]))
|
|
334
|
-
|
|
335
|
-
when "write_var"
|
|
336
|
-
var_name = input["name"]
|
|
337
|
-
value = input["value"]
|
|
338
|
-
write_local(var_name, value)
|
|
339
|
-
"ok: #{var_name} = #{value.inspect}"
|
|
340
|
-
|
|
341
|
-
when "read_attr"
|
|
342
|
-
obj = resolve(input["obj"])
|
|
343
|
-
validate_name!(input["attr"])
|
|
344
|
-
serialize_value(obj.public_send(input["attr"]))
|
|
345
|
-
|
|
346
|
-
when "write_attr"
|
|
347
|
-
obj = resolve(input["obj"])
|
|
348
|
-
validate_name!(input["attr"])
|
|
349
|
-
obj.public_send("#{input['attr']}=", input["value"])
|
|
350
|
-
"ok: #{input['obj']}.#{input['attr']} = #{input['value'].inspect}"
|
|
351
|
-
|
|
352
|
-
when "call_func"
|
|
353
|
-
func = input["name"]
|
|
354
|
-
validate_name!(func)
|
|
355
|
-
args = input["args"] || []
|
|
356
|
-
# Try method first, then local variable (supports lambdas/procs)
|
|
357
|
-
callable = if @binding.receiver.respond_to?(func.to_sym, true)
|
|
358
|
-
@binding.receiver.method(func.to_sym)
|
|
359
|
-
elsif @binding.local_variables.include?(func.to_sym)
|
|
360
|
-
@binding.local_variable_get(func.to_sym)
|
|
361
|
-
else
|
|
362
|
-
@binding.receiver.method(func.to_sym) # raise NameError
|
|
363
|
-
end
|
|
364
|
-
result = callable.call(*args)
|
|
365
|
-
serialize_value(result)
|
|
366
|
-
|
|
367
|
-
when "remember"
|
|
368
|
-
if @incognito
|
|
369
|
-
"Memory not saved (incognito mode)"
|
|
370
|
-
elsif memory
|
|
371
|
-
entry = memory.remember(input["content"])
|
|
372
|
-
"Remembered (id=#{entry[:id]}): #{input['content']}"
|
|
373
|
-
else
|
|
374
|
-
"Memory not available"
|
|
375
|
-
end
|
|
376
|
-
|
|
377
|
-
when "done"
|
|
378
|
-
input["result"].to_s
|
|
379
|
-
|
|
380
|
-
else
|
|
381
|
-
"error: unknown tool #{name}"
|
|
43
|
+
def all_tools
|
|
44
|
+
Engines::LLM.all_tools
|
|
382
45
|
end
|
|
383
|
-
rescue => e
|
|
384
|
-
"error: #{e.class}: #{e.message}"
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
# --- Binding Helpers ---
|
|
388
|
-
|
|
389
|
-
VALID_IDENTIFIER = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
390
|
-
|
|
391
|
-
def validate_name!(name)
|
|
392
|
-
raise Mana::Error, "invalid identifier: #{name.inspect}" unless name.match?(VALID_IDENTIFIER)
|
|
393
46
|
end
|
|
394
47
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
@binding.receiver.send(name.to_sym)
|
|
401
|
-
else
|
|
402
|
-
raise NameError, "undefined variable or method '#{name}'"
|
|
403
|
-
end
|
|
48
|
+
# Backward compatibility: Engine.new(prompt, binding) delegates to Engines::LLM
|
|
49
|
+
# Some tests instantiate Engine directly and call private methods
|
|
50
|
+
def initialize(prompt, caller_binding)
|
|
51
|
+
@delegate = Engines::LLM.new(caller_binding)
|
|
52
|
+
@prompt = prompt
|
|
404
53
|
end
|
|
405
54
|
|
|
406
|
-
def
|
|
407
|
-
|
|
408
|
-
@binding.local_variable_set(name.to_sym, value)
|
|
55
|
+
def execute
|
|
56
|
+
@delegate.execute(@prompt)
|
|
409
57
|
end
|
|
410
58
|
|
|
411
|
-
|
|
412
|
-
# Walk up the call stack to find the first non-mana source file
|
|
413
|
-
loc = @binding.source_location
|
|
414
|
-
return loc[0] if loc.is_a?(Array)
|
|
415
|
-
|
|
416
|
-
# Fallback: search caller_locations
|
|
417
|
-
caller_locations(4, 20)&.each do |frame|
|
|
418
|
-
path = frame.absolute_path || frame.path
|
|
419
|
-
next if path.nil? || path.include?("mana/")
|
|
420
|
-
return path
|
|
421
|
-
end
|
|
422
|
-
nil
|
|
423
|
-
end
|
|
59
|
+
private
|
|
424
60
|
|
|
425
|
-
def
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
val.inspect
|
|
429
|
-
when Symbol
|
|
430
|
-
val.to_s.inspect
|
|
431
|
-
when Array
|
|
432
|
-
"[#{val.map { |v| serialize_value(v) }.join(', ')}]"
|
|
433
|
-
when Hash
|
|
434
|
-
pairs = val.map { |k, v| "#{serialize_value(k)} => #{serialize_value(v)}" }
|
|
435
|
-
"{#{pairs.join(', ')}}"
|
|
61
|
+
def method_missing(method, *args, **kwargs, &block)
|
|
62
|
+
if @delegate.respond_to?(method, true)
|
|
63
|
+
@delegate.send(method, *args, **kwargs, &block)
|
|
436
64
|
else
|
|
437
|
-
|
|
438
|
-
obj_repr = ivars.map do |ivar|
|
|
439
|
-
attr_name = ivar.to_s.delete_prefix("@")
|
|
440
|
-
"#{attr_name}: #{val.instance_variable_get(ivar).inspect}" rescue nil
|
|
441
|
-
end.compact.join(", ")
|
|
442
|
-
"#<#{val.class} #{obj_repr}>"
|
|
65
|
+
super
|
|
443
66
|
end
|
|
444
67
|
end
|
|
445
68
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
def llm_call(system, messages)
|
|
449
|
-
backend = Backends.for(@config)
|
|
450
|
-
backend.chat(
|
|
451
|
-
system: system,
|
|
452
|
-
messages: messages,
|
|
453
|
-
tools: self.class.all_tools,
|
|
454
|
-
model: @config.model,
|
|
455
|
-
max_tokens: 4096
|
|
456
|
-
)
|
|
457
|
-
end
|
|
458
|
-
|
|
459
|
-
def extract_tool_uses(content)
|
|
460
|
-
return [] unless content.is_a?(Array)
|
|
461
|
-
|
|
462
|
-
content
|
|
463
|
-
.select { |block| block[:type] == "tool_use" }
|
|
464
|
-
.map { |block| { id: block[:id], name: block[:name], input: block[:input] || {} } }
|
|
69
|
+
def respond_to_missing?(method, include_private = false)
|
|
70
|
+
@delegate.respond_to?(method, include_private) || super
|
|
465
71
|
end
|
|
466
72
|
end
|
|
467
73
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
# Execute code/prompt in this engine, return the result
|
|
14
|
+
# Subclasses must implement this
|
|
15
|
+
def execute(code)
|
|
16
|
+
raise NotImplementedError, "#{self.class}#execute not implemented"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Read a variable from the Ruby binding
|
|
20
|
+
def read_var(name)
|
|
21
|
+
if @binding.local_variables.include?(name.to_sym)
|
|
22
|
+
@binding.local_variable_get(name.to_sym)
|
|
23
|
+
elsif @binding.receiver.respond_to?(name.to_sym, true)
|
|
24
|
+
@binding.receiver.send(name.to_sym)
|
|
25
|
+
else
|
|
26
|
+
raise NameError, "undefined variable: #{name}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Write a variable to the Ruby binding
|
|
31
|
+
def write_var(name, value)
|
|
32
|
+
@binding.local_variable_set(name.to_sym, value)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Serialize a Ruby value for cross-language transfer
|
|
36
|
+
# Simple types: copy. Complex objects: will be remote refs (future)
|
|
37
|
+
def serialize(value)
|
|
38
|
+
case value
|
|
39
|
+
when Numeric, String, Symbol, TrueClass, FalseClass, NilClass
|
|
40
|
+
value
|
|
41
|
+
when Array
|
|
42
|
+
value.map { |v| serialize(v) }
|
|
43
|
+
when Hash
|
|
44
|
+
value.transform_values { |v| serialize(v) }
|
|
45
|
+
else
|
|
46
|
+
value.to_s
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|