ruby-mana 0.3.1 → 0.4.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/README.md +79 -1
- data/lib/mana/backends/anthropic.rb +38 -0
- data/lib/mana/backends/base.rb +18 -0
- data/lib/mana/backends/openai.rb +167 -0
- data/lib/mana/backends/registry.rb +23 -0
- data/lib/mana/config.rb +2 -0
- data/lib/mana/engine.rb +45 -25
- data/lib/mana/mock.rb +53 -0
- data/lib/mana/test.rb +18 -0
- data/lib/mana/version.rb +1 -1
- data/lib/mana.rb +8 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c5998b08056a2657d73e7ab5382fd9ad3a96a53c9cbaa3268a98f6798c7290da
|
|
4
|
+
data.tar.gz: 82326fc68f9c15694f70dcbea19b43b321674f766497864c824ee574023a7f1c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a612ba5f45d2bd5abf09fd68e7225945e5d3defa70f0bd5298e55215010bfe71dd1a7f53adf415e126fe3a97c654fa209812e8db8b3720d8258355054767eb7d
|
|
7
|
+
data.tar.gz: 3649cc64a401d6d8e92b16ae8cc05999b9a6e5813a16d25a62c7661573827e3086b30e49f9d62e0c57c48a2ea6d081307a053d64d53e5de4d565db607dc76574
|
data/README.md
CHANGED
|
@@ -28,10 +28,12 @@ Or in your Gemfile:
|
|
|
28
28
|
gem "ruby-mana"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Requires Ruby 3.3+ and an Anthropic
|
|
31
|
+
Requires Ruby 3.3+ and an API key (Anthropic, OpenAI, or compatible):
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
34
|
export ANTHROPIC_API_KEY=your_key_here
|
|
35
|
+
# or
|
|
36
|
+
export OPENAI_API_KEY=your_key_here
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
## Usage
|
|
@@ -146,6 +148,41 @@ Mana.configure do |c|
|
|
|
146
148
|
end
|
|
147
149
|
```
|
|
148
150
|
|
|
151
|
+
### Multiple LLM backends
|
|
152
|
+
|
|
153
|
+
Mana supports Anthropic and OpenAI-compatible APIs (including Ollama, DeepSeek, Groq, etc.):
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
# Anthropic (default for claude-* models)
|
|
157
|
+
Mana.configure do |c|
|
|
158
|
+
c.api_key = ENV["ANTHROPIC_API_KEY"]
|
|
159
|
+
c.model = "claude-sonnet-4-20250514"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# OpenAI
|
|
163
|
+
Mana.configure do |c|
|
|
164
|
+
c.api_key = ENV["OPENAI_API_KEY"]
|
|
165
|
+
c.base_url = "https://api.openai.com"
|
|
166
|
+
c.model = "gpt-4o"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Ollama (local, no API key needed)
|
|
170
|
+
Mana.configure do |c|
|
|
171
|
+
c.api_key = "unused"
|
|
172
|
+
c.base_url = "http://localhost:11434"
|
|
173
|
+
c.model = "llama3"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Explicit backend override
|
|
177
|
+
Mana.configure do |c|
|
|
178
|
+
c.backend = :openai # force OpenAI format
|
|
179
|
+
c.base_url = "https://api.groq.com/openai"
|
|
180
|
+
c.model = "llama-3.3-70b-versatile"
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Backend is auto-detected from model name: `claude-*` → Anthropic, everything else → OpenAI.
|
|
185
|
+
|
|
149
186
|
### Custom effect handlers
|
|
150
187
|
|
|
151
188
|
Define your own tools that the LLM can call. Each effect becomes an LLM tool automatically — the block's keyword parameters define the tool's input schema.
|
|
@@ -217,6 +254,47 @@ Mana.incognito do
|
|
|
217
254
|
end
|
|
218
255
|
```
|
|
219
256
|
|
|
257
|
+
### Testing
|
|
258
|
+
|
|
259
|
+
Use `Mana.mock` to test code that uses `~"..."` without calling any API:
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
require "mana/test"
|
|
263
|
+
|
|
264
|
+
RSpec.describe MyApp do
|
|
265
|
+
include Mana::TestHelpers
|
|
266
|
+
|
|
267
|
+
it "analyzes code" do
|
|
268
|
+
mock_prompt "analyze", bugs: ["XSS"], score: 8.5
|
|
269
|
+
|
|
270
|
+
result = MyApp.analyze("user_input")
|
|
271
|
+
expect(result[:bugs]).to include("XSS")
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
it "translates with dynamic response" do
|
|
275
|
+
mock_prompt(/translate.*to\s+\w+/) do |prompt|
|
|
276
|
+
{ output: prompt.include?("Chinese") ? "你好" : "hello" }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
expect(MyApp.translate("hi", "Chinese")).to eq("你好")
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Block mode for inline tests:
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
Mana.mock do
|
|
288
|
+
prompt "summarize", summary: "A brief overview"
|
|
289
|
+
|
|
290
|
+
text = "Long article..."
|
|
291
|
+
~"summarize <text> and store in <summary>"
|
|
292
|
+
puts summary # => "A brief overview"
|
|
293
|
+
end
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Unmatched prompts raise `Mana::MockError` with a helpful message suggesting the stub to add.
|
|
297
|
+
|
|
220
298
|
### Nested prompts
|
|
221
299
|
|
|
222
300
|
Functions called by LLM can themselves contain `~"..."` prompts:
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Mana
|
|
8
|
+
module Backends
|
|
9
|
+
class Anthropic < Base
|
|
10
|
+
def chat(system:, messages:, tools:, model:, max_tokens: 4096)
|
|
11
|
+
uri = URI("#{@config.base_url}/v1/messages")
|
|
12
|
+
body = {
|
|
13
|
+
model: model,
|
|
14
|
+
max_tokens: max_tokens,
|
|
15
|
+
system: system,
|
|
16
|
+
tools: tools,
|
|
17
|
+
messages: messages
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
21
|
+
http.use_ssl = uri.scheme == "https"
|
|
22
|
+
http.read_timeout = 120
|
|
23
|
+
|
|
24
|
+
req = Net::HTTP::Post.new(uri)
|
|
25
|
+
req["Content-Type"] = "application/json"
|
|
26
|
+
req["x-api-key"] = @config.api_key
|
|
27
|
+
req["anthropic-version"] = "2023-06-01"
|
|
28
|
+
req.body = JSON.generate(body)
|
|
29
|
+
|
|
30
|
+
res = http.request(req)
|
|
31
|
+
raise LLMError, "HTTP #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
|
|
32
|
+
|
|
33
|
+
parsed = JSON.parse(res.body, symbolize_names: true)
|
|
34
|
+
parsed[:content] || []
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mana
|
|
4
|
+
module Backends
|
|
5
|
+
class Base
|
|
6
|
+
def initialize(config)
|
|
7
|
+
@config = config
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Send a chat request, return array of content blocks in Anthropic format
|
|
11
|
+
# (normalized). Each backend converts its native response to this format.
|
|
12
|
+
# Returns: [{ type: "text", text: "..." }, { type: "tool_use", id: "...", name: "...", input: {...} }]
|
|
13
|
+
def chat(system:, messages:, tools:, model:, max_tokens: 4096)
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Mana
|
|
8
|
+
module Backends
|
|
9
|
+
class OpenAI < Base
|
|
10
|
+
def chat(system:, messages:, tools:, model:, max_tokens: 4096)
|
|
11
|
+
uri = URI("#{@config.base_url}/v1/chat/completions")
|
|
12
|
+
body = {
|
|
13
|
+
model: model,
|
|
14
|
+
max_completion_tokens: max_tokens,
|
|
15
|
+
messages: convert_messages(system, messages),
|
|
16
|
+
tools: convert_tools(tools)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
20
|
+
http.use_ssl = uri.scheme == "https"
|
|
21
|
+
http.read_timeout = 120
|
|
22
|
+
|
|
23
|
+
req = Net::HTTP::Post.new(uri)
|
|
24
|
+
req["Content-Type"] = "application/json"
|
|
25
|
+
req["Authorization"] = "Bearer #{@config.api_key}"
|
|
26
|
+
req.body = JSON.generate(body)
|
|
27
|
+
|
|
28
|
+
res = http.request(req)
|
|
29
|
+
raise LLMError, "HTTP #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
|
|
30
|
+
|
|
31
|
+
parsed = JSON.parse(res.body, symbolize_names: true)
|
|
32
|
+
normalize_response(parsed)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Convert Anthropic-style messages to OpenAI format
|
|
38
|
+
def convert_messages(system, messages)
|
|
39
|
+
result = [{ role: "system", content: system }]
|
|
40
|
+
|
|
41
|
+
messages.each do |msg|
|
|
42
|
+
case msg[:role]
|
|
43
|
+
when "user"
|
|
44
|
+
converted = convert_user_message(msg)
|
|
45
|
+
if converted.is_a?(Array)
|
|
46
|
+
result.concat(converted)
|
|
47
|
+
else
|
|
48
|
+
result << converted
|
|
49
|
+
end
|
|
50
|
+
when "assistant"
|
|
51
|
+
result << convert_assistant_message(msg)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
result
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def convert_user_message(msg)
|
|
59
|
+
content = msg[:content]
|
|
60
|
+
|
|
61
|
+
# Plain text user message
|
|
62
|
+
return { role: "user", content: content } if content.is_a?(String)
|
|
63
|
+
|
|
64
|
+
# Array of content blocks — may contain tool_result blocks
|
|
65
|
+
if content.is_a?(Array) && content.all? { |b| b[:type] == "tool_result" || b["type"] == "tool_result" }
|
|
66
|
+
# Convert each tool_result to an OpenAI tool message
|
|
67
|
+
return content.map do |block|
|
|
68
|
+
{
|
|
69
|
+
role: "tool",
|
|
70
|
+
tool_call_id: block[:tool_use_id] || block["tool_use_id"],
|
|
71
|
+
content: (block[:content] || block["content"]).to_s
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Other array content (e.g. text blocks) — join as string
|
|
77
|
+
if content.is_a?(Array)
|
|
78
|
+
texts = content.map { |b| b[:text] || b["text"] }.compact
|
|
79
|
+
return { role: "user", content: texts.join("\n") }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
{ role: "user", content: content.to_s }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def convert_assistant_message(msg)
|
|
86
|
+
content = msg[:content]
|
|
87
|
+
|
|
88
|
+
# Simple text response
|
|
89
|
+
if content.is_a?(String)
|
|
90
|
+
return { role: "assistant", content: content }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Array of content blocks — may contain tool_use
|
|
94
|
+
if content.is_a?(Array)
|
|
95
|
+
text_parts = []
|
|
96
|
+
tool_calls = []
|
|
97
|
+
|
|
98
|
+
content.each do |block|
|
|
99
|
+
type = block[:type] || block["type"]
|
|
100
|
+
case type
|
|
101
|
+
when "text"
|
|
102
|
+
text_parts << (block[:text] || block["text"])
|
|
103
|
+
when "tool_use"
|
|
104
|
+
tool_calls << {
|
|
105
|
+
id: block[:id] || block["id"],
|
|
106
|
+
type: "function",
|
|
107
|
+
function: {
|
|
108
|
+
name: block[:name] || block["name"],
|
|
109
|
+
arguments: JSON.generate(block[:input] || block["input"] || {})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
msg_hash = { role: "assistant" }
|
|
116
|
+
msg_hash[:content] = text_parts.join("\n") unless text_parts.empty?
|
|
117
|
+
msg_hash[:tool_calls] = tool_calls unless tool_calls.empty?
|
|
118
|
+
return msg_hash
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
{ role: "assistant", content: content.to_s }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Convert Anthropic tool definitions to OpenAI function calling format
|
|
125
|
+
def convert_tools(tools)
|
|
126
|
+
tools.map do |tool|
|
|
127
|
+
{
|
|
128
|
+
type: "function",
|
|
129
|
+
function: {
|
|
130
|
+
name: tool[:name],
|
|
131
|
+
description: tool[:description] || "",
|
|
132
|
+
parameters: tool[:input_schema] || {}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Convert OpenAI response back to Anthropic-style content blocks
|
|
139
|
+
def normalize_response(parsed)
|
|
140
|
+
choice = parsed.dig(:choices, 0, :message)
|
|
141
|
+
return [] unless choice
|
|
142
|
+
|
|
143
|
+
blocks = []
|
|
144
|
+
|
|
145
|
+
# Text content
|
|
146
|
+
if choice[:content] && !choice[:content].empty?
|
|
147
|
+
blocks << { type: "text", text: choice[:content] }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Tool calls
|
|
151
|
+
if choice[:tool_calls]
|
|
152
|
+
choice[:tool_calls].each do |tc|
|
|
153
|
+
func = tc[:function]
|
|
154
|
+
blocks << {
|
|
155
|
+
type: "tool_use",
|
|
156
|
+
id: tc[:id],
|
|
157
|
+
name: func[:name],
|
|
158
|
+
input: JSON.parse(func[:arguments], symbolize_names: true)
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
blocks
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
data/lib/mana/config.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Mana
|
|
4
4
|
class Config
|
|
5
5
|
attr_accessor :model, :temperature, :api_key, :max_iterations, :base_url,
|
|
6
|
+
:backend,
|
|
6
7
|
:namespace, :memory_store, :memory_path,
|
|
7
8
|
:context_window, :memory_pressure, :memory_keep_recent,
|
|
8
9
|
:compact_model, :on_compact
|
|
@@ -13,6 +14,7 @@ module Mana
|
|
|
13
14
|
@api_key = ENV["ANTHROPIC_API_KEY"]
|
|
14
15
|
@max_iterations = 50
|
|
15
16
|
@base_url = "https://api.anthropic.com"
|
|
17
|
+
@backend = nil
|
|
16
18
|
@namespace = nil
|
|
17
19
|
@memory_store = nil
|
|
18
20
|
@memory_path = nil
|
data/lib/mana/engine.rb
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
-
require "net/http"
|
|
5
|
-
require "uri"
|
|
6
4
|
|
|
7
5
|
module Mana
|
|
8
6
|
class Engine
|
|
@@ -120,6 +118,9 @@ module Mana
|
|
|
120
118
|
end
|
|
121
119
|
|
|
122
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
|
+
|
|
123
124
|
Thread.current[:mana_depth] ||= 0
|
|
124
125
|
Thread.current[:mana_depth] += 1
|
|
125
126
|
nested = Thread.current[:mana_depth] > 1
|
|
@@ -184,11 +185,46 @@ module Mana
|
|
|
184
185
|
if nested && !@incognito
|
|
185
186
|
Thread.current[:mana_memory] = outer_memory
|
|
186
187
|
end
|
|
187
|
-
Thread.current[:mana_depth] -= 1
|
|
188
|
+
Thread.current[:mana_depth] -= 1 if Thread.current[:mana_depth]
|
|
188
189
|
end
|
|
189
190
|
|
|
190
191
|
private
|
|
191
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)
|
|
211
|
+
|
|
212
|
+
values.each do |name, value|
|
|
213
|
+
write_local(name.to_s, value)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Record in short-term memory if not incognito
|
|
217
|
+
if !@incognito
|
|
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
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
return_value || values.values.first
|
|
226
|
+
end
|
|
227
|
+
|
|
192
228
|
# --- Context Building ---
|
|
193
229
|
|
|
194
230
|
def build_context(prompt)
|
|
@@ -410,30 +446,14 @@ module Mana
|
|
|
410
446
|
# --- LLM Client ---
|
|
411
447
|
|
|
412
448
|
def llm_call(system, messages)
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
model: @config.model,
|
|
416
|
-
max_tokens: 4096,
|
|
449
|
+
backend = Backends.for(@config)
|
|
450
|
+
backend.chat(
|
|
417
451
|
system: system,
|
|
452
|
+
messages: messages,
|
|
418
453
|
tools: self.class.all_tools,
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
423
|
-
http.use_ssl = uri.scheme == "https"
|
|
424
|
-
http.read_timeout = 120
|
|
425
|
-
|
|
426
|
-
req = Net::HTTP::Post.new(uri)
|
|
427
|
-
req["Content-Type"] = "application/json"
|
|
428
|
-
req["x-api-key"] = @config.api_key
|
|
429
|
-
req["anthropic-version"] = "2023-06-01"
|
|
430
|
-
req.body = JSON.generate(body)
|
|
431
|
-
|
|
432
|
-
res = http.request(req)
|
|
433
|
-
raise LLMError, "HTTP #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
|
|
434
|
-
|
|
435
|
-
parsed = JSON.parse(res.body, symbolize_names: true)
|
|
436
|
-
parsed[:content] || []
|
|
454
|
+
model: @config.model,
|
|
455
|
+
max_tokens: 4096
|
|
456
|
+
)
|
|
437
457
|
end
|
|
438
458
|
|
|
439
459
|
def extract_tool_uses(content)
|
data/lib/mana/mock.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mana
|
|
4
|
+
class Mock
|
|
5
|
+
Stub = Struct.new(:pattern, :values, :block, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
attr_reader :stubs
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@stubs = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def prompt(pattern, **values, &block)
|
|
14
|
+
@stubs << Stub.new(pattern: pattern, values: values, block: block)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def match(prompt_text)
|
|
18
|
+
@stubs.find do |stub|
|
|
19
|
+
case stub.pattern
|
|
20
|
+
when Regexp then prompt_text.match?(stub.pattern)
|
|
21
|
+
when String then prompt_text.include?(stub.pattern)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
def mock(&block)
|
|
29
|
+
old_mock = Thread.current[:mana_mock]
|
|
30
|
+
m = Mock.new
|
|
31
|
+
Thread.current[:mana_mock] = m
|
|
32
|
+
m.instance_eval(&block)
|
|
33
|
+
ensure
|
|
34
|
+
Thread.current[:mana_mock] = old_mock
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def mock!
|
|
38
|
+
Thread.current[:mana_mock] = Mock.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def unmock!
|
|
42
|
+
Thread.current[:mana_mock] = nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def mock_active?
|
|
46
|
+
!Thread.current[:mana_mock].nil?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def current_mock
|
|
50
|
+
Thread.current[:mana_mock]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/mana/test.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mana"
|
|
4
|
+
|
|
5
|
+
module Mana
|
|
6
|
+
module TestHelpers
|
|
7
|
+
def self.included(base)
|
|
8
|
+
base.before { Mana.mock! }
|
|
9
|
+
base.after { Mana.unmock! }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def mock_prompt(pattern, **values, &block)
|
|
13
|
+
raise Mana::MockError, "Mana mock mode not active. Call Mana.mock! first" unless Mana.mock_active?
|
|
14
|
+
|
|
15
|
+
Mana.current_mock.prompt(pattern, **values, &block)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/mana/version.rb
CHANGED
data/lib/mana.rb
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "mana/version"
|
|
4
4
|
require_relative "mana/config"
|
|
5
|
+
require_relative "mana/backends/base"
|
|
6
|
+
require_relative "mana/backends/anthropic"
|
|
7
|
+
require_relative "mana/backends/openai"
|
|
8
|
+
require_relative "mana/backends/registry"
|
|
5
9
|
require_relative "mana/effect_registry"
|
|
6
10
|
require_relative "mana/namespace"
|
|
7
11
|
require_relative "mana/memory_store"
|
|
@@ -17,6 +21,7 @@ module Mana
|
|
|
17
21
|
class Error < StandardError; end
|
|
18
22
|
class MaxIterationsError < Error; end
|
|
19
23
|
class LLMError < Error; end
|
|
24
|
+
class MockError < Error; end
|
|
20
25
|
|
|
21
26
|
class << self
|
|
22
27
|
def config
|
|
@@ -40,6 +45,7 @@ module Mana
|
|
|
40
45
|
@config = Config.new
|
|
41
46
|
EffectRegistry.clear!
|
|
42
47
|
Thread.current[:mana_memory] = nil
|
|
48
|
+
Thread.current[:mana_mock] = nil
|
|
43
49
|
end
|
|
44
50
|
|
|
45
51
|
# Define a custom effect that becomes an LLM tool
|
|
@@ -73,3 +79,5 @@ module Mana
|
|
|
73
79
|
end
|
|
74
80
|
end
|
|
75
81
|
end
|
|
82
|
+
|
|
83
|
+
require_relative "mana/mock"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby-mana
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Carl
|
|
@@ -37,6 +37,10 @@ files:
|
|
|
37
37
|
- LICENSE
|
|
38
38
|
- README.md
|
|
39
39
|
- lib/mana.rb
|
|
40
|
+
- lib/mana/backends/anthropic.rb
|
|
41
|
+
- lib/mana/backends/base.rb
|
|
42
|
+
- lib/mana/backends/openai.rb
|
|
43
|
+
- lib/mana/backends/registry.rb
|
|
40
44
|
- lib/mana/compiler.rb
|
|
41
45
|
- lib/mana/config.rb
|
|
42
46
|
- lib/mana/context_window.rb
|
|
@@ -46,8 +50,10 @@ files:
|
|
|
46
50
|
- lib/mana/memory.rb
|
|
47
51
|
- lib/mana/memory_store.rb
|
|
48
52
|
- lib/mana/mixin.rb
|
|
53
|
+
- lib/mana/mock.rb
|
|
49
54
|
- lib/mana/namespace.rb
|
|
50
55
|
- lib/mana/string_ext.rb
|
|
56
|
+
- lib/mana/test.rb
|
|
51
57
|
- lib/mana/version.rb
|
|
52
58
|
homepage: https://github.com/carlnoah6/ruby-mana
|
|
53
59
|
licenses:
|