rubyrlm 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 +32 -0
- data/LICENSE +21 -0
- data/README.md +300 -0
- data/bin/rubyrlm +168 -0
- data/lib/rubyrlm/backends/base.rb +9 -0
- data/lib/rubyrlm/backends/gemini_rest.rb +317 -0
- data/lib/rubyrlm/client.rb +643 -0
- data/lib/rubyrlm/completion.rb +71 -0
- data/lib/rubyrlm/errors.rb +9 -0
- data/lib/rubyrlm/logger/jsonl_logger.rb +27 -0
- data/lib/rubyrlm/pricing.rb +88 -0
- data/lib/rubyrlm/prompts/system_prompt.rb +108 -0
- data/lib/rubyrlm/protocol/action_parser.rb +84 -0
- data/lib/rubyrlm/repl/code_validator.rb +113 -0
- data/lib/rubyrlm/repl/docker_repl/container_manager.rb +158 -0
- data/lib/rubyrlm/repl/docker_repl/host_rpc_server.rb +164 -0
- data/lib/rubyrlm/repl/docker_repl/protocol.rb +26 -0
- data/lib/rubyrlm/repl/docker_repl.rb +190 -0
- data/lib/rubyrlm/repl/execution_result.rb +41 -0
- data/lib/rubyrlm/repl/local_repl.rb +476 -0
- data/lib/rubyrlm/sub_call_cache.rb +47 -0
- data/lib/rubyrlm/version.rb +3 -0
- data/lib/rubyrlm/web/app.rb +41 -0
- data/lib/rubyrlm/web/public/css/components.css +649 -0
- data/lib/rubyrlm/web/public/css/design-system.css +1396 -0
- data/lib/rubyrlm/web/public/js/app.js +1016 -0
- data/lib/rubyrlm/web/public/js/components/charts.js +68 -0
- data/lib/rubyrlm/web/public/js/components/context-inspector.js +94 -0
- data/lib/rubyrlm/web/public/js/components/exec-chain.js +105 -0
- data/lib/rubyrlm/web/public/js/components/kpi-dashboard.js +187 -0
- data/lib/rubyrlm/web/public/js/components/query-panel.js +335 -0
- data/lib/rubyrlm/web/public/js/components/recursion-tree.js +83 -0
- data/lib/rubyrlm/web/public/js/components/session-list.js +160 -0
- data/lib/rubyrlm/web/public/js/components/step-navigator.js +129 -0
- data/lib/rubyrlm/web/public/js/components/timeline.js +281 -0
- data/lib/rubyrlm/web/public/js/lib/animation.js +46 -0
- data/lib/rubyrlm/web/public/js/lib/chart-renderer.js +116 -0
- data/lib/rubyrlm/web/public/js/lib/diagram-renderer.js +233 -0
- data/lib/rubyrlm/web/public/js/lib/sse-client.js +94 -0
- data/lib/rubyrlm/web/public/js/lib/theme-manager.js +39 -0
- data/lib/rubyrlm/web/public/js/utils.js +57 -0
- data/lib/rubyrlm/web/routes/api.rb +129 -0
- data/lib/rubyrlm/web/routes/pages.rb +365 -0
- data/lib/rubyrlm/web/routes/sse.rb +95 -0
- data/lib/rubyrlm/web/services/event_broadcaster.rb +36 -0
- data/lib/rubyrlm/web/services/export_service.rb +903 -0
- data/lib/rubyrlm/web/services/query_service.rb +221 -0
- data/lib/rubyrlm/web/services/session_loader.rb +356 -0
- data/lib/rubyrlm/web/services/streaming_logger.rb +22 -0
- data/lib/rubyrlm.rb +18 -0
- metadata +208 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: be38bd741250692279b5207395414b43c3d473dd00dd8835e208a8a5ffe3fb43
|
|
4
|
+
data.tar.gz: 8d8facc2f1c7b0773345bd6930f361501a0f75fb7017a5f45d905bd9614098fb
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d53be5b4873058dec0dbdb564806d66fdcc63555365613e3df19e8eca49e356ec78df3fd72eca503bba3d12411172b71d3f66061f7ac84c41f05411dc9cfebac
|
|
7
|
+
data.tar.gz: e33f04262ccd83d8f867e49d98d6b8b93a64f1eb5745c0a54a97c45f61e0a51c4e4abfd996c49dc77483c223624de59205150adeb08a51a02e1842c3916583e6
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-03-12
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Core RLM client with Gemini backend, multi-turn conversation, and streaming support
|
|
9
|
+
- Local and Docker-isolated REPL runtimes with configurable execution timeout
|
|
10
|
+
- AST-based code validation (Ripper syntax checking + dangerous call detection)
|
|
11
|
+
- Sub-call caching with SHA256-keyed deduplication for `llm_query`
|
|
12
|
+
- Patch tracking with undo support (`undo_last_patch` / `undo_all_patches`)
|
|
13
|
+
- Per-model USD cost tracking with cache-aware billing
|
|
14
|
+
- Shared backend client for child RLMs to reduce per-subcall overhead
|
|
15
|
+
- Web UI with session management, streaming timeline, and Mermaid diagram rendering
|
|
16
|
+
- Theme-aware HTML and PNG exports with glassmorphism styling
|
|
17
|
+
- Session continuation and Controller view with inline prompt
|
|
18
|
+
- Time-scoped filtering and cache tracking in analytics dashboard
|
|
19
|
+
- Docker session reuse and keep-alive configuration options
|
|
20
|
+
- LocalRepl helper primitives for common agent workflows
|
|
21
|
+
- JSONL structured logging
|
|
22
|
+
- `rlm` CLI executable
|
|
23
|
+
- Custom Night Owl syntax highlighting theme
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- Docker container DNS resolution and `network_mode` wiring
|
|
27
|
+
- Docker agent symbol/string key mismatch for `allow_network`
|
|
28
|
+
- Session continuation logic and UI display
|
|
29
|
+
- Kramdown rendering with GFM parser dependency
|
|
30
|
+
- CSS specificity for headless Chrome export rendering
|
|
31
|
+
- UI pivot masking during live stream execution tracking
|
|
32
|
+
- Cache-busting for static assets
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Taylor Weibley
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# RubyRLM
|
|
2
|
+
|
|
3
|
+
RubyRLM is an MVP Ruby implementation of Recursive Language Models (RLMs) that uses Gemini as the model backend and a Ruby REPL for iterative reasoning.
|
|
4
|
+
|
|
5
|
+
## What This MVP Includes
|
|
6
|
+
|
|
7
|
+
- `RubyRLM::Client` API similar to `rlm.completion(...)`.
|
|
8
|
+
- Gemini backend via direct REST (`generateContent`).
|
|
9
|
+
- Local and Docker-isolated REPL backends with iterative `exec` actions and `final` answer action.
|
|
10
|
+
- Recursive sub-calls with `llm_query(...)` up to `max_depth`.
|
|
11
|
+
- **AST code validation** — Ripper-based syntax checking and dangerous call detection before eval.
|
|
12
|
+
- **Sub-call caching** — SHA256-keyed deduplication of `llm_query` calls within a session.
|
|
13
|
+
- **Patch tracking** — Audit trail for all `patch_file` operations with undo support.
|
|
14
|
+
- JSONL trajectory logging for iteration debugging.
|
|
15
|
+
- Web UI with session replay, continuation, and environment selection.
|
|
16
|
+
- RSpec test suite for parser, loop, REPL, recursion, and backend retries.
|
|
17
|
+
|
|
18
|
+
## Safety Model
|
|
19
|
+
|
|
20
|
+
RubyRLM executes model-produced Ruby code. Choose execution environment based on your trust boundary.
|
|
21
|
+
|
|
22
|
+
- `environment: "local"` runs code directly on the host process (unsafe for untrusted prompts).
|
|
23
|
+
- `environment: "docker"` runs code in a Docker container with isolation defaults.
|
|
24
|
+
- Keep side effects disabled unless intentionally requested.
|
|
25
|
+
|
|
26
|
+
### Code Validation
|
|
27
|
+
|
|
28
|
+
Before executing any LLM-generated code, RubyRLM validates it using Ruby's `Ripper` parser:
|
|
29
|
+
|
|
30
|
+
- **Syntax errors** are caught immediately without running the code, saving an iteration.
|
|
31
|
+
- **Dangerous calls** (`system`, `exec`, `fork`, `exit`, `File.delete`, `Kernel.exit`, etc.) are detected and surfaced as warnings in the `ExecutionResult`. These are non-blocking since the REPL intentionally provides safe wrappers like `sh()`, but they alert you when the model bypasses those wrappers.
|
|
32
|
+
|
|
33
|
+
Warnings appear in iteration metadata:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
result.metadata[:iterations].each do |it|
|
|
37
|
+
puts it[:execution][:warnings] if it.dig(:execution, :warnings)&.any?
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Requirements
|
|
42
|
+
|
|
43
|
+
- Ruby `>= 3.1`
|
|
44
|
+
- `GEMINI_API_KEY` in your shell environment
|
|
45
|
+
- Docker (optional, required only for `environment: "docker"`)
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
Add to your Gemfile:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
gem "rubyrlm"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or install directly:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
gem install rubyrlm
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Development Setup
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone https://github.com/tweibley/rubyrlm.git
|
|
65
|
+
cd rubyrlm
|
|
66
|
+
bundle install
|
|
67
|
+
bundle exec rspec
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If you plan to use Docker execution, build the REPL image once:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
docker build -t rubyrlm/repl:latest -f docker/Dockerfile.repl docker/
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Quickstart
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
require "rubyrlm"
|
|
80
|
+
|
|
81
|
+
client = RubyRLM::Client.new(
|
|
82
|
+
backend: "gemini",
|
|
83
|
+
model_name: "gemini-3.1-pro-preview",
|
|
84
|
+
api_key: ENV["GEMINI_API_KEY"],
|
|
85
|
+
max_depth: 1,
|
|
86
|
+
max_iterations: 20,
|
|
87
|
+
logger: RubyRLM::Logger::JsonlLogger.new(log_dir: "./logs"),
|
|
88
|
+
verbose: true
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
result = client.completion(prompt: "Calculate 2^(2^(2^2)) with Ruby and explain the result.")
|
|
92
|
+
puts result.response
|
|
93
|
+
puts result.usage_summary.to_h
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Run in Docker-isolated mode:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
client = RubyRLM::Client.new(
|
|
100
|
+
backend: "gemini",
|
|
101
|
+
model_name: "gemini-3.1-pro-preview",
|
|
102
|
+
api_key: ENV["GEMINI_API_KEY"],
|
|
103
|
+
environment: "docker",
|
|
104
|
+
environment_options: {
|
|
105
|
+
memory_limit: "256m",
|
|
106
|
+
allow_network: true
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
You can also run:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
ruby examples/quickstart.rb
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
For an interactive session with a preloaded client:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
bundle exec bin/console
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Inside console:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
ask(client, "What is the latency to google.com from this machine?")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
With `verbose: true`, you'll now see each iteration's actual `exec` Ruby code plus execution output/error summaries, not just the action names.
|
|
130
|
+
|
|
131
|
+
## CLI
|
|
132
|
+
|
|
133
|
+
RubyRLM ships with a `rubyrlm` command:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
rubyrlm "Calculate 2^(2^(2^2)) with Ruby and explain the result."
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Options:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
-m, --model MODEL Model name (default: gemini-3.1-pro-preview)
|
|
143
|
+
-e, --env ENV Execution environment: local or docker (default: local)
|
|
144
|
+
--max-iterations NUM Maximum iterations (default: 30)
|
|
145
|
+
--max-depth NUM Maximum recursion depth (default: 1)
|
|
146
|
+
--timeout SECS Iteration execution timeout (default: 60)
|
|
147
|
+
--thinking LEVEL Thinking level: low|medium|high (default: medium)
|
|
148
|
+
--keep-alive Keep docker container alive after run
|
|
149
|
+
--reuse-container-id ID Reuse existing docker container
|
|
150
|
+
--allow-network Allow docker container to access host networking
|
|
151
|
+
-v, --verbose Enable verbose debug output
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Prompts can also be piped via stdin:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
echo "What is 1+1?" | rubyrlm
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## How the Action Protocol Works
|
|
161
|
+
|
|
162
|
+
The model must return exactly one JSON object per turn:
|
|
163
|
+
|
|
164
|
+
- `{"action":"exec","code":"<ruby code>"}` to run code in REPL
|
|
165
|
+
- `{"action":"final","answer":"<final answer>"}` to finish
|
|
166
|
+
|
|
167
|
+
If model output is malformed, RubyRLM issues one repair re-prompt. If `max_iterations` is reached, RubyRLM forces a final response.
|
|
168
|
+
|
|
169
|
+
## REPL Variables and Helpers
|
|
170
|
+
|
|
171
|
+
Within `exec` code:
|
|
172
|
+
|
|
173
|
+
- `context` is the original prompt/context
|
|
174
|
+
- `llm_query(sub_prompt, model_name: nil)` launches a recursive sub-call
|
|
175
|
+
- `fetch(url, headers: {})` performs HTTP GET with redirect following
|
|
176
|
+
- `sh(command, timeout: 5)` runs a shell command safely
|
|
177
|
+
- `patch_file(path, old_text, new_text)` replaces text exactly once (tracked for undo)
|
|
178
|
+
- `grep(pattern, path: ".")` searches with ripgrep
|
|
179
|
+
- `chunk_text(text, max_length: 2000)` splits long text semantically
|
|
180
|
+
|
|
181
|
+
RubyRLM sends a compact context summary to the model and keeps full data in REPL memory. This significantly reduces repeated prompt tokens for large datasets.
|
|
182
|
+
|
|
183
|
+
For state between iterations, prefer instance variables (for example `@memo`) or helper methods.
|
|
184
|
+
|
|
185
|
+
## Sub-Call Caching
|
|
186
|
+
|
|
187
|
+
Identical `llm_query` calls within a session are automatically deduplicated. The cache keys on `SHA256(model_name + prompt)`, so the same question to the same model returns the cached result.
|
|
188
|
+
|
|
189
|
+
Cache stats are included in the completion result:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
result = client.completion(prompt: data)
|
|
193
|
+
puts result.metadata[:sub_call_cache]
|
|
194
|
+
# => { hits: 3, misses: 2, size: 2 }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Patch Tracking & Undo
|
|
198
|
+
|
|
199
|
+
Every `patch_file` call is recorded with old/new text and a timestamp. The modification log is surfaced in the completion result:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
result = client.completion(prompt: "Fix the typo in config.yml")
|
|
203
|
+
puts result.metadata[:file_modifications]
|
|
204
|
+
# => [{ path: "config.yml", timestamp: "2026-02-28T12:34:56-05:00" }]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Patches can be undone programmatically through the REPL:
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
# Inside exec code
|
|
211
|
+
undo_result = undo_last_patch # reverses the most recent patch_file
|
|
212
|
+
undo_all = undo_all_patches # reverses all patches in LIFO order
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Docker Environment Options
|
|
216
|
+
|
|
217
|
+
When `environment: "docker"` is selected, `environment_options` supports:
|
|
218
|
+
|
|
219
|
+
- `image` (default: `"rubyrlm/repl:latest"`)
|
|
220
|
+
- `memory_limit` (default: `"256m"`)
|
|
221
|
+
- `cpu_quota` (default: `50000`)
|
|
222
|
+
- `network_mode` (`"none"` by default, `"bridge"` to allow outbound)
|
|
223
|
+
- `allow_network` (boolean shorthand for bridge networking)
|
|
224
|
+
- `keep_alive` (optional boolean to bypass container teardown on completion)
|
|
225
|
+
- `reuse_container_id` (optional Docker container ID to eagerly attach to instead of spinning up a new instance)
|
|
226
|
+
- `connect_timeout` (default: `10` seconds)
|
|
227
|
+
- `gemini_api_key_secret` (default: `"gemini_api_key"`)
|
|
228
|
+
- `gemini_api_key_secret_path` (optional absolute path to a host secret file)
|
|
229
|
+
|
|
230
|
+
Notes:
|
|
231
|
+
|
|
232
|
+
- Docker mode is strict isolation by default (no project workspace mount).
|
|
233
|
+
- `llm_query`, `fetch`, `sh`, and `chunk_text` run inside the container.
|
|
234
|
+
- `patch_file` and `grep` are intentionally disabled in strict Docker mode.
|
|
235
|
+
- Gemini credentials are read in-container from `GEMINI_API_KEY_FILE` (mounted from your secret file).
|
|
236
|
+
|
|
237
|
+
Example secret-file setup for Docker mode:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
mkdir -p .secrets
|
|
241
|
+
printf '%s\n' "$GEMINI_API_KEY" > .secrets/gemini_api_key
|
|
242
|
+
chmod 600 .secrets/gemini_api_key
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
client = RubyRLM::Client.new(
|
|
247
|
+
backend: "gemini",
|
|
248
|
+
model_name: "gemini-3.1-pro-preview",
|
|
249
|
+
environment: "docker",
|
|
250
|
+
environment_options: {
|
|
251
|
+
gemini_api_key_secret_path: File.expand_path(".secrets/gemini_api_key")
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Web UI
|
|
257
|
+
|
|
258
|
+
Start the web UI:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
ruby viewer.rb -p 8080
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
or in dev mode:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
bin/dev -p 8080
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
In the **Controller** sidebar you can select:
|
|
271
|
+
|
|
272
|
+
- **Execution Environment**: Local or Docker
|
|
273
|
+
- **Allow Docker Network Access**: enable outbound networking in Docker mode
|
|
274
|
+
- **Keep Container Alive**: prevents Docker from terminating and removing the instance when the run completes
|
|
275
|
+
- **Reuse Container Instance**: actively queries running isolate workers and allows you to submit queries directly into persistent host environments
|
|
276
|
+
|
|
277
|
+
## Logging
|
|
278
|
+
|
|
279
|
+
Pass `RubyRLM::Logger::JsonlLogger.new(log_dir: "./logs")` to the client.
|
|
280
|
+
|
|
281
|
+
Events are written per-run as JSONL and include:
|
|
282
|
+
|
|
283
|
+
- run start/end
|
|
284
|
+
- per-iteration actions and execution results
|
|
285
|
+
- parent-child run relationship for recursive sub-calls
|
|
286
|
+
|
|
287
|
+
## Examples
|
|
288
|
+
|
|
289
|
+
- `examples/quickstart.rb`: single prompt run with logger
|
|
290
|
+
- `examples/needle_in_haystack.rb`: synthetic long-context retrieval task
|
|
291
|
+
|
|
292
|
+
## Testing
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
bundle exec rspec
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Future Extensions
|
|
299
|
+
|
|
300
|
+
- More backend adapters
|
data/bin/rubyrlm
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
5
|
+
require "rubyrlm"
|
|
6
|
+
require "optparse"
|
|
7
|
+
|
|
8
|
+
options = {
|
|
9
|
+
model_name: "gemini-3.1-pro-preview",
|
|
10
|
+
environment: "local",
|
|
11
|
+
max_iterations: 30,
|
|
12
|
+
max_depth: 1,
|
|
13
|
+
iteration_timeout: 60,
|
|
14
|
+
thinking_level: "medium",
|
|
15
|
+
environment_options: {},
|
|
16
|
+
verbose: false,
|
|
17
|
+
log_dir: File.expand_path("../logs", __dir__)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
parser = OptionParser.new do |opts|
|
|
21
|
+
opts.banner = "Usage: rubyrlm [options] \"<prompt>\""
|
|
22
|
+
|
|
23
|
+
opts.on("-m", "--model MODEL", "Model name (default: gemini-3.1-pro-preview)") do |v|
|
|
24
|
+
options[:model_name] = v
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
opts.on("-e", "--env ENV", "Execution environment: local or docker (default: local)") do |v|
|
|
28
|
+
options[:environment] = v
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
opts.on("--max-iterations NUM", Integer, "Maximum iterations (default: 30)") do |v|
|
|
32
|
+
options[:max_iterations] = v
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
opts.on("--max-depth NUM", Integer, "Maximum recursion depth (default: 1)") do |v|
|
|
36
|
+
options[:max_depth] = v
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
opts.on("--timeout SECS", Integer, "Iteration execution timeout (default: 60)") do |v|
|
|
40
|
+
options[:iteration_timeout] = v
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
opts.on("--thinking LEVEL", "Thinking level: low|medium|high (default: medium)") do |v|
|
|
44
|
+
options[:thinking_level] = v
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
opts.on("--keep-alive", "Keep docker container alive after run") do
|
|
48
|
+
options[:environment_options][:keep_alive] = true
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
opts.on("--reuse-container-id ID", "Reuse existing docker container by its ID") do |v|
|
|
52
|
+
options[:environment_options][:reuse_container_id] = v
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
opts.on("--allow-network", "Allow docker container to access host networking") do
|
|
56
|
+
options[:environment_options][:allow_network] = true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
opts.on("-v", "--verbose", "Enable verbose debug output") do
|
|
60
|
+
options[:verbose] = true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
opts.on("-h", "--help", "Prints this help") do
|
|
64
|
+
puts opts
|
|
65
|
+
exit
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
parser.parse!
|
|
70
|
+
|
|
71
|
+
prompt = ARGV.join(" ").strip
|
|
72
|
+
if prompt.empty? && !STDIN.tty?
|
|
73
|
+
prompt = STDIN.read.strip
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if prompt.empty?
|
|
77
|
+
puts parser.help
|
|
78
|
+
exit 1
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
api_key = ENV["GEMINI_API_KEY"]
|
|
82
|
+
if api_key.to_s.strip.empty?
|
|
83
|
+
warn "Error: GEMINI_API_KEY is missing in environment."
|
|
84
|
+
exit 1
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class MultiLogger
|
|
88
|
+
def initialize(*loggers)
|
|
89
|
+
@loggers = loggers.compact
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def log(payload)
|
|
93
|
+
@loggers.each { |l| l.log(payload) }
|
|
94
|
+
rescue => e
|
|
95
|
+
warn "Logger error: #{e.message}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class CliStreamer
|
|
100
|
+
def initialize
|
|
101
|
+
@in_chunk = false
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def log(payload)
|
|
105
|
+
case payload[:type]
|
|
106
|
+
when "chunk"
|
|
107
|
+
print payload[:chunk]
|
|
108
|
+
@in_chunk = true
|
|
109
|
+
when "iteration"
|
|
110
|
+
puts "" if @in_chunk
|
|
111
|
+
@in_chunk = false
|
|
112
|
+
data = payload[:data]
|
|
113
|
+
if data && data[:action] == "exec"
|
|
114
|
+
puts "\n\n[Executing Ruby Code] ->"
|
|
115
|
+
puts "```ruby\n#{data[:code]}\n```"
|
|
116
|
+
if data[:execution]
|
|
117
|
+
puts "[Result: #{data[:execution][:ok] ? 'Success' : 'Failed'}]"
|
|
118
|
+
puts data[:execution][:output]
|
|
119
|
+
puts data[:execution][:error] if data[:execution][:error]
|
|
120
|
+
end
|
|
121
|
+
puts "-" * 40
|
|
122
|
+
end
|
|
123
|
+
when "run_error"
|
|
124
|
+
puts "" if @in_chunk
|
|
125
|
+
@in_chunk = false
|
|
126
|
+
puts "\n[Error] #{payload[:error]}"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
cli_streamer = CliStreamer.new
|
|
132
|
+
jsonl_logger = RubyRLM::Logger::JsonlLogger.new(log_dir: options[:log_dir])
|
|
133
|
+
multi_logger = MultiLogger.new(cli_streamer, jsonl_logger)
|
|
134
|
+
|
|
135
|
+
generation_config = {}
|
|
136
|
+
if %w[low medium high].include?(options[:thinking_level].to_s.downcase)
|
|
137
|
+
generation_config[:thinking_config] = { thinkingLevel: options[:thinking_level].to_s.downcase }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
client_kwargs = {
|
|
141
|
+
backend: "gemini",
|
|
142
|
+
api_key: api_key,
|
|
143
|
+
model_name: options[:model_name],
|
|
144
|
+
environment: options[:environment],
|
|
145
|
+
max_iterations: options[:max_iterations],
|
|
146
|
+
max_depth: options[:max_depth],
|
|
147
|
+
iteration_timeout: options[:iteration_timeout],
|
|
148
|
+
generation_config: generation_config,
|
|
149
|
+
verbose: options[:verbose],
|
|
150
|
+
streaming: true,
|
|
151
|
+
logger: multi_logger
|
|
152
|
+
}
|
|
153
|
+
client_kwargs[:environment_options] = options[:environment_options] unless options[:environment_options].empty?
|
|
154
|
+
|
|
155
|
+
begin
|
|
156
|
+
client = RubyRLM::Client.new(**client_kwargs)
|
|
157
|
+
rescue => e
|
|
158
|
+
warn "Failed to initialize client: #{e.message}"
|
|
159
|
+
exit 1
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
puts "=> Starting query..."
|
|
163
|
+
|
|
164
|
+
result = client.completion(prompt: prompt)
|
|
165
|
+
|
|
166
|
+
puts "\n\n=== Final Result ==="
|
|
167
|
+
puts result.response
|
|
168
|
+
puts "\n[Usage Summary: #{result.usage_summary.total_tokens} tokens | Cached: #{result.usage_summary.cached_content_tokens} | Cost: $#{'%.4f' % result.usage_summary.compute_total_usd_cost}]"
|