rlm-rb 0.1.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 +7 -0
- data/CHANGELOG.md +38 -0
- data/LICENSE.txt +21 -0
- data/README.md +233 -0
- data/lib/rlm/config.rb +33 -0
- data/lib/rlm/context.rb +51 -0
- data/lib/rlm/errors.rb +14 -0
- data/lib/rlm/file.rb +101 -0
- data/lib/rlm/limits.rb +61 -0
- data/lib/rlm/predict.rb +41 -0
- data/lib/rlm/result.rb +78 -0
- data/lib/rlm/sandbox/execution_result.rb +47 -0
- data/lib/rlm/sandbox/mock.rb +48 -0
- data/lib/rlm/sandbox.rb +19 -0
- data/lib/rlm/tool.rb +36 -0
- data/lib/rlm/trace.rb +87 -0
- data/lib/rlm/version.rb +5 -0
- data/lib/rlm.rb +35 -0
- metadata +83 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e52a1acbaea2c8a9955a4a75058e6e4d4780c16b327a9b28b68bb620c1ddb406
|
|
4
|
+
data.tar.gz: 75c6d410914cd74a4d949658acf9d2c747cf0a87b9ad4c43c80d6a7cbf9a417f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9e5a488efd6e4ddbd0bb7e9b6df4df2624dd0c127244069120cc78b4e260432fd2e66453c2526734a5114f2c2cdb13220bfaffdc6cca3876282698b77a1c1c92
|
|
7
|
+
data.tar.gz: ee9901f38ffdc87df153319c7a723b801e441da54696118512dee03d5dcef22ccd1adb13f17db6bb785c99aeaa95b315bcd6bd017c2b3051928ab7c33c34d64f
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-05-12
|
|
11
|
+
|
|
12
|
+
Skeleton release. Establishes the public types, configuration surface, sandbox
|
|
13
|
+
interface, and error hierarchy that the runtime milestone will build on.
|
|
14
|
+
`RLM::Predict#call` raises `NotImplementedError` until the runtime loop lands in
|
|
15
|
+
v0.2.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `RLM::VERSION`, `RLM.configure`, `RLM.config`, `RLM.reset_configuration!`.
|
|
20
|
+
- Error hierarchy (`RLM::Error`, `ConfigurationError`, `BudgetExceededError`, `SandboxError`,
|
|
21
|
+
`ValidationError`, `ProviderError`, `ToolError`, `ParseError`, `NoProgressError`).
|
|
22
|
+
- `RLM::Limits` with PRD defaults and validation.
|
|
23
|
+
- `RLM::File` with `from_path`, `from_text`, `from_io`, and `from_active_storage` constructors.
|
|
24
|
+
- `RLM::Context` with file handles and sandbox-safe manifest.
|
|
25
|
+
- `RLM::Trace` with typed events, NDJSON/JSON export, and basic counters.
|
|
26
|
+
- `RLM::Result` with the documented status enum and predicates.
|
|
27
|
+
- `RLM::Sandbox::Base` interface plus `Sandbox::ExecutionResult` and `Sandbox::Mock` for tests.
|
|
28
|
+
- `RLM::Tool` base class with category DSL.
|
|
29
|
+
- `RLM::Predict` skeleton (`#call` raises `NotImplementedError` until the runtime loop lands).
|
|
30
|
+
|
|
31
|
+
### Not yet implemented (tracked for v0.2+)
|
|
32
|
+
|
|
33
|
+
- Runtime execution loop, code extractor, runtime bridge, recursive `predict(...)`.
|
|
34
|
+
- RubyLLM root/sub-LM adapters.
|
|
35
|
+
- dspy.rb signature adapter and output validation.
|
|
36
|
+
- `RLM::Sandbox::Subprocess` backend.
|
|
37
|
+
- Rails integration (Railtie, generator, migrations, ActiveStorage adapter).
|
|
38
|
+
- PDF/CSV/Directory skills.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Paluy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# RLM.rb
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/rlm-rb)
|
|
4
|
+
[](https://github.com/dpaluy/rlm/actions/workflows/ci.yml)
|
|
5
|
+
|
|
6
|
+
Recursive Language Models for Ruby and Rails.
|
|
7
|
+
|
|
8
|
+
RLM.rb is a Ruby/Rails-native runtime for typed, sandboxed, auditable AI jobs over large application context.
|
|
9
|
+
It depends on [RubyLLM](https://github.com/crmne/ruby_llm) for provider access and [dspy.rb](https://github.com/vicentereig/dspy.rb)
|
|
10
|
+
for typed signatures, and adds the missing recursive execution runtime: sandbox, REPL loop, file and context mounting,
|
|
11
|
+
recursive sub-LM calls, typed final output, budget controls, and durable trajectories.
|
|
12
|
+
|
|
13
|
+
> **Status: v0.1.0 skeleton.** Core types are in place. The runtime loop, provider adapters, signature adapter,
|
|
14
|
+
> subprocess sandbox, and Rails integration are not yet implemented and are tracked in the v0.2 milestone in
|
|
15
|
+
> `docs/prd.md`. `RLM::Predict#call` raises `NotImplementedError` in this release.
|
|
16
|
+
|
|
17
|
+
## Why
|
|
18
|
+
|
|
19
|
+
1. Large context breaks simple prompting.
|
|
20
|
+
2. Manual chunking and summarization are brittle.
|
|
21
|
+
3. Hand-rolled agent loops have unclear state, unclear cost, and poor auditability.
|
|
22
|
+
|
|
23
|
+
RLM.rb replaces those with a bounded Ruby runtime where the model explores context programmatically, calls smaller
|
|
24
|
+
typed LLM functions only when needed, and returns validated Ruby objects with a full execution trace.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
Add the gem to your Gemfile:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
gem "rlm-rb"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or install directly:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
gem install rlm-rb
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
RLM.configure do |config|
|
|
44
|
+
# Provider adapters land in the next milestone.
|
|
45
|
+
# config.root_lm = RubyLLM.chat(model: "anthropic/claude-sonnet-4")
|
|
46
|
+
# config.sub_lm = RubyLLM.chat(model: "openai/gpt-5-mini")
|
|
47
|
+
|
|
48
|
+
config.sandbox = RLM::Sandbox::Mock.new
|
|
49
|
+
|
|
50
|
+
config.default_limits = RLM::Limits.new(
|
|
51
|
+
max_iterations: 8,
|
|
52
|
+
max_llm_calls: 25,
|
|
53
|
+
max_tool_calls: 20,
|
|
54
|
+
max_runtime_seconds: 120,
|
|
55
|
+
max_cost_cents: 100,
|
|
56
|
+
max_recursion_depth: 1
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Intended API (not yet executable)
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
class InvoiceExtraction < DSPy::Signature
|
|
65
|
+
description "Extract normalized invoice fields from a vendor invoice."
|
|
66
|
+
|
|
67
|
+
input do
|
|
68
|
+
const :invoice_pdf, RLM::File
|
|
69
|
+
const :vendor_id, Integer
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
output do
|
|
73
|
+
const :vendor_name, String
|
|
74
|
+
const :invoice_number, String
|
|
75
|
+
const :total_cents, Integer
|
|
76
|
+
const :confidence, Float
|
|
77
|
+
const :needs_review, T::Boolean
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
result = RLM.predict(
|
|
82
|
+
InvoiceExtraction,
|
|
83
|
+
input: {
|
|
84
|
+
invoice_pdf: RLM::File.from_path("invoice.pdf"),
|
|
85
|
+
vendor_id: 123
|
|
86
|
+
},
|
|
87
|
+
max_iterations: 10,
|
|
88
|
+
max_llm_calls: 30,
|
|
89
|
+
max_cost_cents: 150
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
result.output # typed object
|
|
93
|
+
result.trace # readable steps, llm calls, tool calls
|
|
94
|
+
result.cost_cents # accumulated cost
|
|
95
|
+
result.status # :completed, :needs_review, :budget_exceeded, ...
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## What's in this skeleton today
|
|
99
|
+
|
|
100
|
+
| Component | Status |
|
|
101
|
+
|-----------|--------|
|
|
102
|
+
| `RLM.configure` / `RLM.config` | Ready |
|
|
103
|
+
| `RLM::Limits` with PRD defaults | Ready |
|
|
104
|
+
| `RLM::File` (path / text / io / ActiveStorage blob) | Ready |
|
|
105
|
+
| `RLM::Context` with sandbox-safe manifest | Ready |
|
|
106
|
+
| `RLM::Trace` with NDJSON / JSON export | Ready |
|
|
107
|
+
| `RLM::Result` with full status enum | Ready |
|
|
108
|
+
| `RLM::Sandbox::Base` interface + `Mock` backend | Ready |
|
|
109
|
+
| `RLM::Tool` base class with category DSL | Ready |
|
|
110
|
+
| Error hierarchy | Ready |
|
|
111
|
+
| `RLM::Predict` skeleton | Stub, raises on `#call` |
|
|
112
|
+
| RubyLLM provider adapter | Not yet |
|
|
113
|
+
| dspy.rb signature adapter | Not yet |
|
|
114
|
+
| Runtime execution loop + recursive `predict` | Not yet |
|
|
115
|
+
| `RLM::Sandbox::Subprocess` | Not yet |
|
|
116
|
+
| Rails Railtie, generator, migrations, ActiveStorage adapter | Not yet |
|
|
117
|
+
|
|
118
|
+
See `docs/prd.md` for the full product spec and v0.2 milestone list.
|
|
119
|
+
|
|
120
|
+
## Rails setup (intended, lands in v0.3)
|
|
121
|
+
|
|
122
|
+
The Rails integration is not yet implemented, but the intended setup is:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# config/initializers/rlm.rb
|
|
126
|
+
RLM.configure do |config|
|
|
127
|
+
config.root_lm = RubyLLM.chat(model: Rails.application.credentials.dig(:rlm, :root_model))
|
|
128
|
+
config.sub_lm = RubyLLM.chat(model: Rails.application.credentials.dig(:rlm, :sub_model))
|
|
129
|
+
|
|
130
|
+
config.sandbox = RLM::Sandbox::Subprocess.new # development
|
|
131
|
+
# config.sandbox = RLM::Sandbox::Docker.new # production (v0.4)
|
|
132
|
+
|
|
133
|
+
config.cache = Rails.cache
|
|
134
|
+
config.logger = Rails.logger
|
|
135
|
+
|
|
136
|
+
config.default_limits = RLM::Limits.new(
|
|
137
|
+
max_iterations: 8,
|
|
138
|
+
max_llm_calls: 25,
|
|
139
|
+
max_tool_calls: 20,
|
|
140
|
+
max_runtime_seconds: 120,
|
|
141
|
+
max_cost_cents: 100,
|
|
142
|
+
max_recursion_depth: 1
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
API keys belong in `Rails.application.credentials`, not env files. Per RubyLLM's
|
|
148
|
+
[Rails integration](https://rubyllm.com/rails/), provider keys are picked up automatically when set there.
|
|
149
|
+
|
|
150
|
+
## Error handling
|
|
151
|
+
|
|
152
|
+
All RLM errors inherit from `RLM::Error`. Rescue the parent to catch every variant, or rescue specific classes
|
|
153
|
+
to handle distinct failure modes.
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
begin
|
|
157
|
+
result = RLM.predict(InvoiceExtraction, input: { invoice_pdf: file })
|
|
158
|
+
rescue RLM::BudgetExceededError => e
|
|
159
|
+
# Hard limits hit: max_iterations, max_llm_calls, max_cost_cents, max_runtime_seconds.
|
|
160
|
+
logger.warn("RLM budget exceeded: #{e.message}")
|
|
161
|
+
rescue RLM::ValidationError => e
|
|
162
|
+
# Final output failed signature validation after repair attempts were exhausted.
|
|
163
|
+
invoice.update!(needs_review: true, review_reasons: ["validation_failed"])
|
|
164
|
+
rescue RLM::SandboxError => e
|
|
165
|
+
# Generated code violated sandbox policy or the sandbox backend crashed.
|
|
166
|
+
raise
|
|
167
|
+
rescue RLM::ProviderError => e
|
|
168
|
+
# RubyLLM provider call failed (transient retries already exhausted).
|
|
169
|
+
raise
|
|
170
|
+
rescue RLM::ToolError => e
|
|
171
|
+
# A registered tool raised an exception or was called with invalid input.
|
|
172
|
+
raise
|
|
173
|
+
rescue RLM::ParseError => e
|
|
174
|
+
# Root LM response could not be parsed into <rlm-code>/<rlm-final>.
|
|
175
|
+
raise
|
|
176
|
+
rescue RLM::NoProgressError => e
|
|
177
|
+
# The model emitted no new progress across iterations.
|
|
178
|
+
raise
|
|
179
|
+
rescue RLM::ConfigurationError => e
|
|
180
|
+
# Missing signature, missing root LM, invalid sandbox, etc.
|
|
181
|
+
raise
|
|
182
|
+
rescue RLM::Error => e
|
|
183
|
+
# Catch-all for any other RLM-originated failure.
|
|
184
|
+
raise
|
|
185
|
+
end
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Soft failures land on `result.status` instead of raising. Inspect `result.success?`, `result.needs_review?`,
|
|
189
|
+
`result.failed?`, and `result.validation_errors` to branch.
|
|
190
|
+
|
|
191
|
+
| Status | Predicate | Meaning |
|
|
192
|
+
|--------|-----------|---------|
|
|
193
|
+
| `:completed` | `success?` | Output valid, ready to use. |
|
|
194
|
+
| `:needs_review` | `needs_review?` | Output present but validation flagged it or budget policy is `:needs_review`. |
|
|
195
|
+
| `:failed_validation` | `failed?` | Output invalid after repair attempts. |
|
|
196
|
+
| `:budget_exceeded` | `failed?` | Hit a hard limit and policy is `:fail`. |
|
|
197
|
+
| `:sandbox_error` | `failed?` | Sandbox violation or crash. |
|
|
198
|
+
| `:tool_error` | `failed?` | Tool raised or returned invalid output. |
|
|
199
|
+
| `:provider_error` | `failed?` | RubyLLM provider failure. |
|
|
200
|
+
| `:aborted` | `failed?` | Run cancelled by caller. |
|
|
201
|
+
|
|
202
|
+
## Production safety (when the runtime loop ships)
|
|
203
|
+
|
|
204
|
+
- The subprocess sandbox planned for v0.2 is intended for local development and low-risk internal use.
|
|
205
|
+
- Production deployments should use the Docker sandbox (v0.4) or a remote isolated runner.
|
|
206
|
+
- Generated code must not execute inside the host Ruby process. The codebase will hold this invariant.
|
|
207
|
+
- Mounted files are data, not instructions. Prompt injection mitigations are documented in the PRD.
|
|
208
|
+
|
|
209
|
+
## Development
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
bundle install
|
|
213
|
+
bundle exec rake test # 58 runs / 139 assertions / 0 failures
|
|
214
|
+
bundle exec rubocop # lint
|
|
215
|
+
bundle exec rake # test + rubocop
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Contributing
|
|
219
|
+
|
|
220
|
+
Issues and pull requests welcome at https://github.com/dpaluy/rlm.
|
|
221
|
+
|
|
222
|
+
## API reference
|
|
223
|
+
|
|
224
|
+
RLM.rb sits on top of two upstream libraries. When you need provider or signature details, go to source:
|
|
225
|
+
|
|
226
|
+
- [RubyLLM](https://github.com/crmne/ruby_llm), [Rails integration guide](https://rubyllm.com/rails/) for provider/chat/file API.
|
|
227
|
+
- [dspy.rb](https://github.com/vicentereig/dspy.rb), [Signatures guide](https://vicentereig.github.io/dspy.rb/core-concepts/signatures/) for typed input/output contracts.
|
|
228
|
+
- The [Recursive Language Models](https://github.com/alexzhang13/rlm) reference implementation and the
|
|
229
|
+
[DSPy RLM module](https://dspy.ai/api/modules/RLM/) for the underlying idea.
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT, see `LICENSE.txt`.
|
data/lib/rlm/config.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RLM
|
|
4
|
+
class Config
|
|
5
|
+
attr_accessor :root_lm, :sub_lm, :sandbox, :cache, :default_limits, :trace_store
|
|
6
|
+
attr_writer :logger
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@root_lm = nil
|
|
10
|
+
@sub_lm = nil
|
|
11
|
+
@sandbox = Sandbox::Mock.new
|
|
12
|
+
@cache = nil
|
|
13
|
+
@default_limits = Limits.new
|
|
14
|
+
@trace_store = nil
|
|
15
|
+
@logger = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def logger
|
|
19
|
+
@logger ||= default_logger
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def default_logger
|
|
25
|
+
if defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
|
|
26
|
+
::Rails.logger
|
|
27
|
+
else
|
|
28
|
+
require "logger"
|
|
29
|
+
::Logger.new($stderr)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/rlm/context.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RLM
|
|
4
|
+
class Context
|
|
5
|
+
SANDBOX_FILES_ROOT = "/mnt/rlm/files"
|
|
6
|
+
|
|
7
|
+
attr_reader :inputs, :files
|
|
8
|
+
|
|
9
|
+
def initialize(inputs: {}, files: [])
|
|
10
|
+
@inputs = inputs.dup.freeze
|
|
11
|
+
@files = Array(files).dup.freeze
|
|
12
|
+
@handles = build_handles(@files)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def manifest
|
|
16
|
+
{
|
|
17
|
+
files: @files.map do |file|
|
|
18
|
+
handle = handle_for(file)
|
|
19
|
+
{
|
|
20
|
+
handle: handle,
|
|
21
|
+
filename: file.filename,
|
|
22
|
+
content_type: file.content_type,
|
|
23
|
+
size_bytes: file.size_bytes,
|
|
24
|
+
sandbox_path: ::File.join(SANDBOX_FILES_ROOT, file.filename)
|
|
25
|
+
}
|
|
26
|
+
end,
|
|
27
|
+
inputs: serializable_inputs
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def file_for(handle)
|
|
32
|
+
@handles[handle]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def handle_for(file)
|
|
36
|
+
@handles.key(file)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def build_handles(files)
|
|
42
|
+
files.each_with_index.to_h { |file, i| ["file_#{i + 1}", file] }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def serializable_inputs
|
|
46
|
+
@inputs.each_with_object({}) do |(key, value), acc|
|
|
47
|
+
acc[key] = value.is_a?(File) ? { file_handle: handle_for(value) } : value
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/rlm/errors.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RLM
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class ConfigurationError < Error; end
|
|
7
|
+
class BudgetExceededError < Error; end
|
|
8
|
+
class SandboxError < Error; end
|
|
9
|
+
class ValidationError < Error; end
|
|
10
|
+
class ProviderError < Error; end
|
|
11
|
+
class ToolError < Error; end
|
|
12
|
+
class ParseError < Error; end
|
|
13
|
+
class NoProgressError < Error; end
|
|
14
|
+
end
|
data/lib/rlm/file.rb
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module RLM
|
|
6
|
+
class File
|
|
7
|
+
CONTENT_TYPES = {
|
|
8
|
+
".txt" => "text/plain",
|
|
9
|
+
".md" => "text/markdown",
|
|
10
|
+
".markdown" => "text/markdown",
|
|
11
|
+
".csv" => "text/csv",
|
|
12
|
+
".json" => "application/json",
|
|
13
|
+
".pdf" => "application/pdf",
|
|
14
|
+
".html" => "text/html",
|
|
15
|
+
".htm" => "text/html",
|
|
16
|
+
".xml" => "application/xml",
|
|
17
|
+
".yml" => "application/yaml",
|
|
18
|
+
".yaml" => "application/yaml",
|
|
19
|
+
".rb" => "application/x-ruby"
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
DEFAULT_CONTENT_TYPE = "application/octet-stream"
|
|
23
|
+
|
|
24
|
+
attr_reader :filename, :content_type, :size_bytes, :source
|
|
25
|
+
|
|
26
|
+
def self.from_path(path)
|
|
27
|
+
pathname = Pathname.new(path)
|
|
28
|
+
raise ArgumentError, "File not found: #{path}" unless pathname.file?
|
|
29
|
+
|
|
30
|
+
new(
|
|
31
|
+
filename: pathname.basename.to_s,
|
|
32
|
+
content_type: content_type_for(pathname.extname),
|
|
33
|
+
size_bytes: pathname.size,
|
|
34
|
+
source: { kind: :path, path: pathname.expand_path.to_s }
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.from_text(filename, text)
|
|
39
|
+
raise ArgumentError, "filename is required" if filename.to_s.empty?
|
|
40
|
+
|
|
41
|
+
new(
|
|
42
|
+
filename: filename,
|
|
43
|
+
content_type: content_type_for(::File.extname(filename)),
|
|
44
|
+
size_bytes: text.bytesize,
|
|
45
|
+
source: { kind: :text, text: text }
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.from_io(io, filename:, content_type: nil)
|
|
50
|
+
raise ArgumentError, "filename is required" if filename.to_s.empty?
|
|
51
|
+
|
|
52
|
+
data = io.read
|
|
53
|
+
new(
|
|
54
|
+
filename: filename,
|
|
55
|
+
content_type: content_type || content_type_for(::File.extname(filename)),
|
|
56
|
+
size_bytes: data.bytesize,
|
|
57
|
+
source: { kind: :io, text: data }
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.from_active_storage(blob)
|
|
62
|
+
raise ArgumentError, "blob cannot be nil" if blob.nil?
|
|
63
|
+
|
|
64
|
+
new(
|
|
65
|
+
filename: blob.filename.to_s,
|
|
66
|
+
content_type: blob.content_type,
|
|
67
|
+
size_bytes: blob.byte_size,
|
|
68
|
+
source: { kind: :active_storage, blob: blob }
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.content_type_for(extname)
|
|
73
|
+
CONTENT_TYPES[extname.to_s.downcase] || DEFAULT_CONTENT_TYPE
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def initialize(filename:, content_type:, size_bytes:, source:)
|
|
77
|
+
@filename = filename
|
|
78
|
+
@content_type = content_type
|
|
79
|
+
@size_bytes = size_bytes
|
|
80
|
+
@source = source
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def read
|
|
84
|
+
case source[:kind]
|
|
85
|
+
when :path then ::File.read(source[:path])
|
|
86
|
+
when :text, :io then source[:text]
|
|
87
|
+
when :active_storage then source[:blob].download
|
|
88
|
+
else raise SandboxError, "Unknown file source kind: #{source[:kind].inspect}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def to_h
|
|
93
|
+
{
|
|
94
|
+
filename: filename,
|
|
95
|
+
content_type: content_type,
|
|
96
|
+
size_bytes: size_bytes,
|
|
97
|
+
source_kind: source[:kind]
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
data/lib/rlm/limits.rb
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RLM
|
|
4
|
+
class Limits
|
|
5
|
+
INTEGER_DEFAULTS = {
|
|
6
|
+
max_iterations: 8,
|
|
7
|
+
max_llm_calls: 25,
|
|
8
|
+
max_sub_lm_calls: 20,
|
|
9
|
+
max_tool_calls: 20,
|
|
10
|
+
max_runtime_seconds: 120,
|
|
11
|
+
max_cost_cents: 100,
|
|
12
|
+
max_input_bytes: 25 * 1024 * 1024,
|
|
13
|
+
max_output_bytes: 1 * 1024 * 1024,
|
|
14
|
+
max_stdout_bytes: 256 * 1024,
|
|
15
|
+
max_files: 50,
|
|
16
|
+
max_file_bytes: 25 * 1024 * 1024,
|
|
17
|
+
max_recursion_depth: 1
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
BUDGET_POLICIES = %i[fail return_partial needs_review].freeze
|
|
21
|
+
DEFAULT_POLICY = :needs_review
|
|
22
|
+
|
|
23
|
+
DEFAULTS = INTEGER_DEFAULTS.merge(on_budget_exceeded: DEFAULT_POLICY).freeze
|
|
24
|
+
|
|
25
|
+
attr_reader(*DEFAULTS.keys)
|
|
26
|
+
|
|
27
|
+
def initialize(**overrides)
|
|
28
|
+
unknown = overrides.keys - DEFAULTS.keys
|
|
29
|
+
raise ArgumentError, "Unknown limit keys: #{unknown.join(", ")}" if unknown.any?
|
|
30
|
+
|
|
31
|
+
DEFAULTS.merge(overrides).each do |key, value|
|
|
32
|
+
instance_variable_set("@#{key}", value)
|
|
33
|
+
end
|
|
34
|
+
validate!
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def merge(**overrides)
|
|
38
|
+
self.class.new(**to_h, **overrides)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_h
|
|
42
|
+
DEFAULTS.keys.to_h { |k| [k, public_send(k)] }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def validate!
|
|
48
|
+
INTEGER_DEFAULTS.each_key do |key|
|
|
49
|
+
value = public_send(key)
|
|
50
|
+
next if value.is_a?(Integer) && value >= 0
|
|
51
|
+
|
|
52
|
+
raise ArgumentError, "#{key} must be a non-negative integer, got #{value.inspect}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
return if BUDGET_POLICIES.include?(on_budget_exceeded)
|
|
56
|
+
|
|
57
|
+
raise ArgumentError,
|
|
58
|
+
"on_budget_exceeded must be one of #{BUDGET_POLICIES.inspect}, got #{on_budget_exceeded.inspect}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/rlm/predict.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RLM
|
|
4
|
+
class Predict
|
|
5
|
+
attr_reader :signature, :lm, :sub_lm, :tools, :skills, :sandbox,
|
|
6
|
+
:limits, :trace_store, :validators
|
|
7
|
+
|
|
8
|
+
def initialize(
|
|
9
|
+
signature,
|
|
10
|
+
lm: nil,
|
|
11
|
+
sub_lm: nil,
|
|
12
|
+
tools: [],
|
|
13
|
+
skills: [],
|
|
14
|
+
sandbox: nil,
|
|
15
|
+
limits: nil,
|
|
16
|
+
trace_store: nil,
|
|
17
|
+
validators: []
|
|
18
|
+
)
|
|
19
|
+
raise ConfigurationError, "signature is required" if signature.nil?
|
|
20
|
+
|
|
21
|
+
@signature = signature
|
|
22
|
+
@lm = lm || RLM.config.root_lm
|
|
23
|
+
@sub_lm = sub_lm || RLM.config.sub_lm
|
|
24
|
+
@tools = Array(tools)
|
|
25
|
+
@skills = Array(skills)
|
|
26
|
+
@sandbox = sandbox || RLM.config.sandbox
|
|
27
|
+
@limits = limits || RLM.config.default_limits
|
|
28
|
+
@trace_store = trace_store || RLM.config.trace_store
|
|
29
|
+
@validators = Array(validators)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def call(_input = {})
|
|
33
|
+
raise NotImplementedError,
|
|
34
|
+
"RLM::Predict#call is not implemented in v0.1.0. " \
|
|
35
|
+
"The runtime loop, RubyLLM root/sub-LM adapters, and dspy.rb " \
|
|
36
|
+
"signature adapter land in v0.2. The skeleton exists so that " \
|
|
37
|
+
"downstream code can wire up signatures, tools, sandboxes, and " \
|
|
38
|
+
"limits against a stable API."
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/rlm/result.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RLM
|
|
4
|
+
class Result
|
|
5
|
+
STATUSES = %i[
|
|
6
|
+
completed
|
|
7
|
+
needs_review
|
|
8
|
+
failed_validation
|
|
9
|
+
budget_exceeded
|
|
10
|
+
sandbox_error
|
|
11
|
+
tool_error
|
|
12
|
+
provider_error
|
|
13
|
+
aborted
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
FAILURE_STATUSES = %i[
|
|
17
|
+
failed_validation
|
|
18
|
+
budget_exceeded
|
|
19
|
+
sandbox_error
|
|
20
|
+
tool_error
|
|
21
|
+
provider_error
|
|
22
|
+
aborted
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
attr_reader :output, :trace, :status, :error, :cost_cents,
|
|
26
|
+
:duration_ms, :llm_calls, :iterations, :validation_errors
|
|
27
|
+
|
|
28
|
+
def initialize(
|
|
29
|
+
trace:,
|
|
30
|
+
status:,
|
|
31
|
+
output: nil,
|
|
32
|
+
error: nil,
|
|
33
|
+
cost_cents: 0,
|
|
34
|
+
duration_ms: 0,
|
|
35
|
+
llm_calls: 0,
|
|
36
|
+
iterations: 0,
|
|
37
|
+
validation_errors: []
|
|
38
|
+
)
|
|
39
|
+
raise ArgumentError, "Unknown status: #{status.inspect}" unless STATUSES.include?(status)
|
|
40
|
+
|
|
41
|
+
@output = output
|
|
42
|
+
@trace = trace
|
|
43
|
+
@status = status
|
|
44
|
+
@error = error
|
|
45
|
+
@cost_cents = cost_cents
|
|
46
|
+
@duration_ms = duration_ms
|
|
47
|
+
@llm_calls = llm_calls
|
|
48
|
+
@iterations = iterations
|
|
49
|
+
@validation_errors = validation_errors
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def success?
|
|
53
|
+
status == :completed
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def needs_review?
|
|
57
|
+
status == :needs_review
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def failed?
|
|
61
|
+
FAILURE_STATUSES.include?(status)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def to_h
|
|
65
|
+
{
|
|
66
|
+
output: output,
|
|
67
|
+
status: status,
|
|
68
|
+
error: error&.message,
|
|
69
|
+
cost_cents: cost_cents,
|
|
70
|
+
duration_ms: duration_ms,
|
|
71
|
+
llm_calls: llm_calls,
|
|
72
|
+
iterations: iterations,
|
|
73
|
+
validation_errors: validation_errors,
|
|
74
|
+
trace_id: trace&.id
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RLM
|
|
4
|
+
module Sandbox
|
|
5
|
+
class ExecutionResult
|
|
6
|
+
STATUSES = %i[ok error timeout budget_exceeded].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :stdout, :stderr, :exit_code, :duration_ms, :events, :status, :error
|
|
9
|
+
|
|
10
|
+
def initialize(
|
|
11
|
+
stdout: "",
|
|
12
|
+
stderr: "",
|
|
13
|
+
exit_code: 0,
|
|
14
|
+
duration_ms: 0,
|
|
15
|
+
events: [],
|
|
16
|
+
status: :ok,
|
|
17
|
+
error: nil
|
|
18
|
+
)
|
|
19
|
+
raise ArgumentError, "Unknown status: #{status.inspect}" unless STATUSES.include?(status)
|
|
20
|
+
|
|
21
|
+
@stdout = stdout
|
|
22
|
+
@stderr = stderr
|
|
23
|
+
@exit_code = exit_code
|
|
24
|
+
@duration_ms = duration_ms
|
|
25
|
+
@events = events
|
|
26
|
+
@status = status
|
|
27
|
+
@error = error
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ok?
|
|
31
|
+
status == :ok
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_h
|
|
35
|
+
{
|
|
36
|
+
status: status,
|
|
37
|
+
exit_code: exit_code,
|
|
38
|
+
duration_ms: duration_ms,
|
|
39
|
+
stdout: stdout,
|
|
40
|
+
stderr: stderr,
|
|
41
|
+
events: events,
|
|
42
|
+
error: error&.message
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RLM
|
|
4
|
+
module Sandbox
|
|
5
|
+
class Mock < Base
|
|
6
|
+
attr_reader :executed_code, :context, :tools, :skills, :runtime_bridge
|
|
7
|
+
|
|
8
|
+
def initialize(handler: nil)
|
|
9
|
+
super()
|
|
10
|
+
@handler = handler
|
|
11
|
+
@executed_code = []
|
|
12
|
+
@prepared = false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def prepared?
|
|
16
|
+
@prepared
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def prepare(context:, tools:, skills:, runtime_bridge:)
|
|
20
|
+
@prepared = true
|
|
21
|
+
@context = context
|
|
22
|
+
@tools = tools
|
|
23
|
+
@skills = skills
|
|
24
|
+
@runtime_bridge = runtime_bridge
|
|
25
|
+
ExecutionResult.new(status: :ok)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def exec(code)
|
|
29
|
+
raise SandboxError, "Sandbox not prepared" unless @prepared
|
|
30
|
+
|
|
31
|
+
@executed_code << code
|
|
32
|
+
return ExecutionResult.new(status: :ok, stdout: "") if @handler.nil?
|
|
33
|
+
|
|
34
|
+
result = @handler.call(code, context: @context, bridge: @runtime_bridge)
|
|
35
|
+
result.is_a?(ExecutionResult) ? result : ExecutionResult.new(stdout: result.to_s)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def cleanup
|
|
39
|
+
@prepared = false
|
|
40
|
+
@executed_code.clear
|
|
41
|
+
@context = nil
|
|
42
|
+
@tools = nil
|
|
43
|
+
@skills = nil
|
|
44
|
+
@runtime_bridge = nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/rlm/sandbox.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RLM
|
|
4
|
+
module Sandbox
|
|
5
|
+
class Base
|
|
6
|
+
def prepare(context:, tools:, skills:, runtime_bridge:)
|
|
7
|
+
raise NotImplementedError, "#{self.class} must implement #prepare"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def exec(code)
|
|
11
|
+
raise NotImplementedError, "#{self.class} must implement #exec"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def cleanup
|
|
15
|
+
raise NotImplementedError, "#{self.class} must implement #cleanup"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/rlm/tool.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RLM
|
|
4
|
+
class Tool
|
|
5
|
+
CATEGORIES = %i[read_only write_requires_approval write_allowed dangerous_disabled].freeze
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def description(text = nil)
|
|
9
|
+
return @description if text.nil?
|
|
10
|
+
|
|
11
|
+
@description = text
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def category(value = nil)
|
|
15
|
+
return @category || :read_only if value.nil?
|
|
16
|
+
raise ArgumentError, "Unknown category: #{value.inspect}" unless CATEGORIES.include?(value)
|
|
17
|
+
|
|
18
|
+
@category = value
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def registry_name
|
|
22
|
+
@registry_name ||= name.to_s.split("::").last
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def inherited(subclass)
|
|
26
|
+
super
|
|
27
|
+
subclass.instance_variable_set(:@description, nil)
|
|
28
|
+
subclass.instance_variable_set(:@category, nil)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def call(**kwargs)
|
|
33
|
+
raise NotImplementedError, "#{self.class} must implement #call"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/rlm/trace.rb
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module RLM
|
|
8
|
+
class Trace
|
|
9
|
+
EVENT_TYPES = %i[
|
|
10
|
+
run_started
|
|
11
|
+
root_prompt_created
|
|
12
|
+
root_lm_called
|
|
13
|
+
code_generated
|
|
14
|
+
code_executed
|
|
15
|
+
file_read
|
|
16
|
+
tool_called
|
|
17
|
+
sub_lm_called
|
|
18
|
+
validation_attempted
|
|
19
|
+
validation_failed
|
|
20
|
+
budget_checked
|
|
21
|
+
run_completed
|
|
22
|
+
run_failed
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
attr_reader :id, :events, :started_at
|
|
26
|
+
|
|
27
|
+
def initialize(id: SecureRandom.uuid, clock: Time.method(:now))
|
|
28
|
+
@id = id
|
|
29
|
+
@events = []
|
|
30
|
+
@clock = clock
|
|
31
|
+
@started_at = clock.call
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def record(type, payload = {})
|
|
35
|
+
raise ArgumentError, "Unknown trace event type: #{type.inspect}" unless EVENT_TYPES.include?(type)
|
|
36
|
+
|
|
37
|
+
events << { type: type, payload: payload, at: @clock.call.iso8601(6) }
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def steps
|
|
42
|
+
events.select { |e| %i[code_generated code_executed].include?(e[:type]) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def llm_calls
|
|
46
|
+
events.select { |e| %i[root_lm_called sub_lm_called].include?(e[:type]) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def tool_calls
|
|
50
|
+
events.select { |e| e[:type] == :tool_called }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def files_read
|
|
54
|
+
events.select { |e| e[:type] == :file_read }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def validation_errors
|
|
58
|
+
events.select { |e| e[:type] == :validation_failed }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def cost_cents
|
|
62
|
+
llm_calls.sum { |e| e[:payload][:cost_cents].to_i }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def duration_ms
|
|
66
|
+
return 0 if events.empty?
|
|
67
|
+
|
|
68
|
+
((@clock.call - @started_at) * 1000).to_i
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def to_h
|
|
72
|
+
{
|
|
73
|
+
id: id,
|
|
74
|
+
started_at: started_at.iso8601(6),
|
|
75
|
+
events: events
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def to_json(*)
|
|
80
|
+
JSON.generate(to_h, *)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def to_ndjson
|
|
84
|
+
events.map { |e| JSON.generate(e) }.join("\n")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/rlm/version.rb
ADDED
data/lib/rlm.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rlm/version"
|
|
4
|
+
require_relative "rlm/errors"
|
|
5
|
+
require_relative "rlm/limits"
|
|
6
|
+
require_relative "rlm/file"
|
|
7
|
+
require_relative "rlm/context"
|
|
8
|
+
require_relative "rlm/trace"
|
|
9
|
+
require_relative "rlm/result"
|
|
10
|
+
require_relative "rlm/sandbox"
|
|
11
|
+
require_relative "rlm/sandbox/execution_result"
|
|
12
|
+
require_relative "rlm/sandbox/mock"
|
|
13
|
+
require_relative "rlm/tool"
|
|
14
|
+
require_relative "rlm/config"
|
|
15
|
+
require_relative "rlm/predict"
|
|
16
|
+
|
|
17
|
+
module RLM
|
|
18
|
+
class << self
|
|
19
|
+
def config
|
|
20
|
+
@config ||= Config.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def configure
|
|
24
|
+
yield config
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reset_configuration!
|
|
28
|
+
@config = nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def predict(signature, input:, **)
|
|
32
|
+
Predict.new(signature, **).call(input)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rlm-rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- David Paluy
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: logger
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.6'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.6'
|
|
26
|
+
description: |-
|
|
27
|
+
RLM.rb is a Ruby/Rails-native runtime for Recursive Language Models. It runs bounded, typed, auditable AI jobs
|
|
28
|
+
over large files, records, and application context. RLM.rb uses RubyLLM for provider access and dspy.rb for
|
|
29
|
+
typed signatures, and adds the missing recursive execution runtime: sandbox, REPL loop, file/context mounting,
|
|
30
|
+
recursive sub-LM calls, typed final output, budget controls, and durable trajectories.
|
|
31
|
+
email:
|
|
32
|
+
- dpaluy@users.noreply.github.com
|
|
33
|
+
executables: []
|
|
34
|
+
extensions: []
|
|
35
|
+
extra_rdoc_files:
|
|
36
|
+
- CHANGELOG.md
|
|
37
|
+
- LICENSE.txt
|
|
38
|
+
- README.md
|
|
39
|
+
files:
|
|
40
|
+
- CHANGELOG.md
|
|
41
|
+
- LICENSE.txt
|
|
42
|
+
- README.md
|
|
43
|
+
- lib/rlm.rb
|
|
44
|
+
- lib/rlm/config.rb
|
|
45
|
+
- lib/rlm/context.rb
|
|
46
|
+
- lib/rlm/errors.rb
|
|
47
|
+
- lib/rlm/file.rb
|
|
48
|
+
- lib/rlm/limits.rb
|
|
49
|
+
- lib/rlm/predict.rb
|
|
50
|
+
- lib/rlm/result.rb
|
|
51
|
+
- lib/rlm/sandbox.rb
|
|
52
|
+
- lib/rlm/sandbox/execution_result.rb
|
|
53
|
+
- lib/rlm/sandbox/mock.rb
|
|
54
|
+
- lib/rlm/tool.rb
|
|
55
|
+
- lib/rlm/trace.rb
|
|
56
|
+
- lib/rlm/version.rb
|
|
57
|
+
homepage: https://github.com/dpaluy/rlm-rb
|
|
58
|
+
licenses:
|
|
59
|
+
- MIT
|
|
60
|
+
metadata:
|
|
61
|
+
rubygems_mfa_required: 'true'
|
|
62
|
+
source_code_uri: https://github.com/dpaluy/rlm-rb
|
|
63
|
+
changelog_uri: https://github.com/dpaluy/rlm-rb/blob/main/CHANGELOG.md
|
|
64
|
+
bug_tracker_uri: https://github.com/dpaluy/rlm-rb/issues
|
|
65
|
+
rdoc_options: []
|
|
66
|
+
require_paths:
|
|
67
|
+
- lib
|
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: 3.2.0
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
78
|
+
requirements: []
|
|
79
|
+
rubygems_version: 4.0.6
|
|
80
|
+
specification_version: 4
|
|
81
|
+
summary: Ruby/Rails-native runtime for typed, sandboxed, auditable AI jobs over large
|
|
82
|
+
application context.
|
|
83
|
+
test_files: []
|