gemlings 0.3.2 → 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/CHANGELOG.md +11 -0
- data/README.md +39 -1
- data/gemlings.gemspec +1 -1
- data/lib/gemlings/agent.rb +14 -7
- data/lib/gemlings/cli.rb +17 -5
- data/lib/gemlings/code_agent.rb +5 -4
- data/lib/gemlings/ruby_llm.rb +71 -0
- data/lib/gemlings/sandbox.rb +53 -7
- data/lib/gemlings/tool_calling_agent.rb +12 -5
- data/lib/gemlings/ui.rb +8 -0
- data/lib/gemlings/version.rb +1 -1
- data/lib/gemlings.rb +1 -0
- metadata +6 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 626d2d204d6ea651987fd9fa08fa642e94741d6086ee397a20be6eba8f78af82
|
|
4
|
+
data.tar.gz: da993ac05fe07c53e670b5e71b984aa5a7b879a2daed380c6ab4b1febad3ce98
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4ff5eb3cd06d891dfa5c6994822f5d1cf1957366ad59ed14552357114add8acc7b777bd1486dd3ea759f41a35c00bf5306726e6aed1394195bc5dd2364c3dbcf
|
|
7
|
+
data.tar.gz: 0a61d59266d86180ca27d4087f0f8ab33d037c6b681bc4715d7969a7ace021bc464c3b6ebae408c19ff50da863f17af9d8c0a5496f775e1d0bf02536aa9c4d4e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
Streaming output, configurable sandboxing, and RubyLLM interop.
|
|
6
|
+
|
|
7
|
+
- **Streaming output** -- `agent.run("task", stream: true)` prints LLM tokens to the terminal in real-time; CLI flag: `gemlings -S`
|
|
8
|
+
- **Configurable sandbox executors** -- Choose `:fork`, `:thread`, or `:box` via `CodeAgent.new(executor: :box)`; auto-detects the best option per platform
|
|
9
|
+
- **Ruby::Box executor** -- On Ruby 4.0+ with `RUBY_BOX=1`, the `:box` executor adds namespace isolation so agent code can't leak monkey-patches or constants into the host
|
|
10
|
+
- **RubyLLM tool interop** -- `Gemlings.tool_from_ruby_llm(MyTool)` wraps any `RubyLLM::Tool` for use in gemlings agents
|
|
11
|
+
- **RubyLLM agent interop** -- `Gemlings.agent_from_ruby_llm(MyAgent)` wraps a `RubyLLM::Agent` or `Chat` as a managed sub-agent
|
|
12
|
+
- **Test coverage** -- Added specs for agent base class, CLI, prompt templates, and UI (106 -> 172 tests)
|
|
13
|
+
|
|
3
14
|
## 0.3.2
|
|
4
15
|
|
|
5
16
|
Bug fixes and Ollama improvements.
|
data/README.md
CHANGED
|
@@ -90,6 +90,23 @@ tools = Gemlings.tools_from_mcp(command: ["npx", "-y", "@modelcontextprotocol/se
|
|
|
90
90
|
tool = Gemlings.tool_from_mcp(command: ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp"], tool_name: "read_file")
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
### RubyLLM tools
|
|
94
|
+
|
|
95
|
+
Use existing [RubyLLM](https://rubyllm.com) tools directly:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
class SearchTool < RubyLLM::Tool
|
|
99
|
+
description "Search the web"
|
|
100
|
+
param :query, type: :string, desc: "Search query"
|
|
101
|
+
def execute(query:) = "results for #{query}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
agent = Gemlings::CodeAgent.new(
|
|
105
|
+
model: "anthropic/claude-sonnet-4-20250514",
|
|
106
|
+
tools: [Gemlings.tool_from_ruby_llm(SearchTool)]
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
93
110
|
## Multi-agent workflows
|
|
94
111
|
|
|
95
112
|
Nest agents as tools. The manager agent calls sub-agents by name:
|
|
@@ -110,6 +127,21 @@ manager = Gemlings::CodeAgent.new(
|
|
|
110
127
|
manager.run("Find out when Ruby 3.4 was released and summarize the key features")
|
|
111
128
|
```
|
|
112
129
|
|
|
130
|
+
RubyLLM agents work too:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
class Researcher < RubyLLM::Agent
|
|
134
|
+
model "openai/gpt-4o"
|
|
135
|
+
tools SearchTool
|
|
136
|
+
instructions "You are a research assistant."
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
manager = Gemlings::CodeAgent.new(
|
|
140
|
+
model: "anthropic/claude-sonnet-4-20250514",
|
|
141
|
+
agents: [Gemlings.agent_from_ruby_llm(Researcher, name: "researcher", description: "Researches topics")]
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
113
145
|
## Output validation
|
|
114
146
|
|
|
115
147
|
Validate final answers against a JSON Schema:
|
|
@@ -214,6 +246,7 @@ gemlings -m openai/gpt-4o -t web_search "Who won the latest Super Bowl?"
|
|
|
214
246
|
gemlings -a tool_calling -m openai/gpt-4o "What is 6 * 7?"
|
|
215
247
|
gemlings --mcp "npx -y @modelcontextprotocol/server-filesystem /tmp" "List files in /tmp"
|
|
216
248
|
gemlings -i # interactive mode
|
|
249
|
+
gemlings -S "What is 2+2?" # stream tokens to terminal
|
|
217
250
|
```
|
|
218
251
|
|
|
219
252
|
## Configuration
|
|
@@ -231,8 +264,13 @@ gemlings -i # interactive mode
|
|
|
231
264
|
| `final_answer_checks:` | `[]` | Procs `(answer, memory) -> bool` |
|
|
232
265
|
| `callbacks:` | `[]` | Array of `Callback` instances |
|
|
233
266
|
| `step_callbacks:` | `[]` | Procs `(step, agent:) -> void` |
|
|
267
|
+
| `executor:` | auto | CodeAgent sandbox: `:fork`, `:thread`, or `:box` (Ruby 4.0+) |
|
|
268
|
+
|
|
269
|
+
Requires Ruby 3.2+. JRuby 10+ is also supported. On Ruby 4.0+ with `RUBY_BOX=1`, the `:box` executor adds namespace isolation via `Ruby::Box`.
|
|
270
|
+
|
|
271
|
+
## Ecosystem
|
|
234
272
|
|
|
235
|
-
|
|
273
|
+
- [gemlings_browser](https://github.com/parolkar/gemlings_browser) -- Browser automation tools for gemlings agents
|
|
236
274
|
|
|
237
275
|
## License
|
|
238
276
|
|
data/gemlings.gemspec
CHANGED
|
@@ -35,7 +35,7 @@ Gem::Specification.new do |spec|
|
|
|
35
35
|
spec.add_dependency "rouge", "~> 4.0"
|
|
36
36
|
spec.add_dependency "json-schema", "~> 4.0"
|
|
37
37
|
spec.add_dependency "bigdecimal"
|
|
38
|
-
spec.add_dependency "mcp", "
|
|
38
|
+
spec.add_dependency "mcp", ">= 0.9.2"
|
|
39
39
|
spec.add_dependency "ruby_llm", "~> 1.1"
|
|
40
40
|
|
|
41
41
|
spec.add_development_dependency "rake", "~> 13.0"
|
data/lib/gemlings/agent.rb
CHANGED
|
@@ -27,7 +27,7 @@ module Gemlings
|
|
|
27
27
|
@tool_map = @tools.each_with_object({}) { |t, h| h[t.class.tool_name] = t }
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
def run(task, reset: true, return_full_result: false, &on_stream)
|
|
30
|
+
def run(task, reset: true, return_full_result: false, stream: false, &on_stream)
|
|
31
31
|
@interrupt_switch = false
|
|
32
32
|
|
|
33
33
|
if reset || @memory.nil?
|
|
@@ -46,7 +46,7 @@ module Gemlings
|
|
|
46
46
|
maybe_plan(step_number)
|
|
47
47
|
|
|
48
48
|
# Call LLM with timing
|
|
49
|
-
llm_duration, response = timed { generate_response(memory.to_messages, &on_stream) }
|
|
49
|
+
llm_duration, response = timed { generate_response(memory.to_messages, stream: stream, &on_stream) }
|
|
50
50
|
|
|
51
51
|
# Parse response
|
|
52
52
|
thought, action = parse_response(response)
|
|
@@ -159,11 +159,18 @@ module Gemlings
|
|
|
159
159
|
raise NotImplementedError
|
|
160
160
|
end
|
|
161
161
|
|
|
162
|
-
def generate_response(messages, &on_stream)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
162
|
+
def generate_response(messages, stream: false, &on_stream)
|
|
163
|
+
if on_stream
|
|
164
|
+
response = @model.generate(messages, &on_stream)
|
|
165
|
+
elsif stream
|
|
166
|
+
response = @model.generate(messages) { |token| UI.stream_token(token) }
|
|
167
|
+
UI.stream_end
|
|
168
|
+
else
|
|
169
|
+
spin = UI.spinner("Thinking...")
|
|
170
|
+
spin.start
|
|
171
|
+
response = @model.generate(messages)
|
|
172
|
+
spin.stop
|
|
173
|
+
end
|
|
167
174
|
response
|
|
168
175
|
end
|
|
169
176
|
|
data/lib/gemlings/cli.rb
CHANGED
|
@@ -23,9 +23,11 @@ module Gemlings
|
|
|
23
23
|
tools: [],
|
|
24
24
|
mcp: [],
|
|
25
25
|
interactive: false,
|
|
26
|
+
stream: false,
|
|
26
27
|
max_steps: 10,
|
|
27
28
|
planning_interval: nil,
|
|
28
|
-
agent_type: "code"
|
|
29
|
+
agent_type: "code",
|
|
30
|
+
executor: nil
|
|
29
31
|
}
|
|
30
32
|
parse_options!
|
|
31
33
|
end
|
|
@@ -67,6 +69,10 @@ module Gemlings
|
|
|
67
69
|
@options[:interactive] = true
|
|
68
70
|
end
|
|
69
71
|
|
|
72
|
+
opts.on("-S", "--stream", "Stream LLM tokens to the terminal") do
|
|
73
|
+
@options[:stream] = true
|
|
74
|
+
end
|
|
75
|
+
|
|
70
76
|
opts.on("--mcp COMMAND", "MCP server command (repeatable)") do |cmd|
|
|
71
77
|
@options[:mcp] << cmd
|
|
72
78
|
end
|
|
@@ -75,6 +81,10 @@ module Gemlings
|
|
|
75
81
|
@options[:max_steps] = n
|
|
76
82
|
end
|
|
77
83
|
|
|
84
|
+
opts.on("-e", "--executor NAME", "Sandbox executor: fork, thread, box (default: auto)") do |e|
|
|
85
|
+
@options[:executor] = e.to_sym
|
|
86
|
+
end
|
|
87
|
+
|
|
78
88
|
opts.on("-v", "--version", "Show version") do
|
|
79
89
|
puts "gemlings #{VERSION}"
|
|
80
90
|
exit
|
|
@@ -108,18 +118,20 @@ module Gemlings
|
|
|
108
118
|
|
|
109
119
|
agent_class = @options[:agent_type] == "tool_calling" ? ToolCallingAgent : CodeAgent
|
|
110
120
|
|
|
111
|
-
|
|
121
|
+
opts = {
|
|
112
122
|
model: @options[:model],
|
|
113
123
|
tools: tools,
|
|
114
124
|
max_steps: @options[:max_steps],
|
|
115
125
|
planning_interval: @options[:planning_interval]
|
|
116
|
-
|
|
126
|
+
}
|
|
127
|
+
opts[:executor] = @options[:executor] if @options[:executor] && agent_class == CodeAgent
|
|
128
|
+
agent_class.new(**opts)
|
|
117
129
|
end
|
|
118
130
|
|
|
119
131
|
def single_query(query)
|
|
120
132
|
UI.welcome
|
|
121
133
|
agent = build_agent
|
|
122
|
-
agent.run(query)
|
|
134
|
+
agent.run(query, stream: @options[:stream])
|
|
123
135
|
end
|
|
124
136
|
|
|
125
137
|
def interactive_mode
|
|
@@ -135,7 +147,7 @@ module Gemlings
|
|
|
135
147
|
query = $stdin.gets&.strip
|
|
136
148
|
break if query.nil? || query.empty?
|
|
137
149
|
|
|
138
|
-
agent.run(query, reset: first)
|
|
150
|
+
agent.run(query, reset: first, stream: @options[:stream])
|
|
139
151
|
first = false
|
|
140
152
|
puts
|
|
141
153
|
end
|
data/lib/gemlings/code_agent.rb
CHANGED
|
@@ -5,9 +5,9 @@ module Gemlings
|
|
|
5
5
|
CODE_BLOCK_RE = /```ruby\s*\n(.*?)```/m
|
|
6
6
|
|
|
7
7
|
def initialize(model:, tools: [], agents: [], name: nil, description: nil,
|
|
8
|
-
max_steps: 10, timeout: 30,
|
|
9
|
-
|
|
10
|
-
instructions: nil, output_type: nil)
|
|
8
|
+
max_steps: 10, timeout: 30, executor: nil, planning_interval: nil,
|
|
9
|
+
step_callbacks: [], callbacks: [], final_answer_checks: [],
|
|
10
|
+
prompt_templates: nil, instructions: nil, output_type: nil)
|
|
11
11
|
require_relative "tools/list_gems"
|
|
12
12
|
tools = [ListGems] + tools unless tools.any? { |t| t == ListGems || (t.is_a?(Tool) && t.is_a?(ListGems)) }
|
|
13
13
|
super(model: model, tools: tools, agents: agents, name: name, description: description,
|
|
@@ -15,6 +15,7 @@ module Gemlings
|
|
|
15
15
|
callbacks: callbacks, final_answer_checks: final_answer_checks,
|
|
16
16
|
prompt_templates: prompt_templates, instructions: instructions, output_type: output_type)
|
|
17
17
|
@timeout = timeout
|
|
18
|
+
@executor = executor ? Sandbox.resolve_executor(executor) : nil
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
private
|
|
@@ -76,7 +77,7 @@ module Gemlings
|
|
|
76
77
|
end
|
|
77
78
|
|
|
78
79
|
def execute(code)
|
|
79
|
-
sandbox = Sandbox.new(tools: tools, timeout: @timeout)
|
|
80
|
+
sandbox = Sandbox.new(tools: tools, timeout: @timeout, executor: @executor)
|
|
80
81
|
sandbox.execute(code)
|
|
81
82
|
end
|
|
82
83
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemlings
|
|
4
|
+
# Wraps a RubyLLM::Tool (class or instance) as a Gemlings::Tool.
|
|
5
|
+
def self.tool_from_ruby_llm(tool)
|
|
6
|
+
require "ruby_llm"
|
|
7
|
+
instance = tool.is_a?(Class) ? tool.new : tool
|
|
8
|
+
|
|
9
|
+
raise ArgumentError, "Expected a RubyLLM::Tool, got #{instance.class}" unless instance.is_a?(RubyLLM::Tool)
|
|
10
|
+
|
|
11
|
+
tool_name = instance.name
|
|
12
|
+
tool_desc = instance.description || ""
|
|
13
|
+
params = instance.parameters || {}
|
|
14
|
+
|
|
15
|
+
klass = Class.new(Tool) do
|
|
16
|
+
self.tool_name(tool_name)
|
|
17
|
+
description(tool_desc)
|
|
18
|
+
|
|
19
|
+
params.each do |pname, param|
|
|
20
|
+
input pname.to_sym,
|
|
21
|
+
type: (param.type || "string").to_sym,
|
|
22
|
+
description: param.description || "",
|
|
23
|
+
required: param.required != false
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
wrapper = klass.new
|
|
28
|
+
wrapper.define_singleton_method(:call) do |**kwargs|
|
|
29
|
+
instance.call(kwargs)
|
|
30
|
+
end
|
|
31
|
+
wrapper
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Wraps a RubyLLM::Agent (class or instance) as a managed Gemlings agent.
|
|
35
|
+
# The agent can be passed in the `agents:` array of a Gemlings agent.
|
|
36
|
+
def self.agent_from_ruby_llm(agent, name: nil, description: nil)
|
|
37
|
+
require "ruby_llm"
|
|
38
|
+
|
|
39
|
+
chat = if agent.is_a?(Class) && agent < RubyLLM::Agent
|
|
40
|
+
agent.chat
|
|
41
|
+
elsif agent.is_a?(RubyLLM::Agent)
|
|
42
|
+
agent.chat
|
|
43
|
+
elsif agent.respond_to?(:ask)
|
|
44
|
+
agent
|
|
45
|
+
else
|
|
46
|
+
raise ArgumentError, "Expected a RubyLLM::Agent class, instance, or RubyLLM::Chat, got #{agent.class}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
agent_name = name || agent.class.name&.split("::")&.last&.downcase || "ruby_llm_agent"
|
|
50
|
+
agent_desc = description || "A RubyLLM agent"
|
|
51
|
+
|
|
52
|
+
RubyLLMAgentWrapper.new(chat, name: agent_name, description: agent_desc)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Minimal agent-like wrapper around a RubyLLM::Chat so it can be used
|
|
56
|
+
# with Gemlings::ManagedAgentTool.
|
|
57
|
+
class RubyLLMAgentWrapper
|
|
58
|
+
attr_reader :name, :description
|
|
59
|
+
|
|
60
|
+
def initialize(chat, name:, description:)
|
|
61
|
+
@chat = chat
|
|
62
|
+
@name = name
|
|
63
|
+
@description = description
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def run(task)
|
|
67
|
+
response = @chat.ask(task)
|
|
68
|
+
response.content
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/gemlings/sandbox.rb
CHANGED
|
@@ -7,12 +7,17 @@ module Gemlings
|
|
|
7
7
|
DEFAULT_TIMEOUT = 30 # seconds
|
|
8
8
|
|
|
9
9
|
# ---------------------------------------------------------------------------
|
|
10
|
-
# Executor strategy —
|
|
11
|
-
# Adding a new backend (e.g. TruffleRuby) is a new subclass + one constant.
|
|
10
|
+
# Executor strategy — user-selectable, with sensible per-platform defaults.
|
|
12
11
|
# ---------------------------------------------------------------------------
|
|
12
|
+
EXECUTORS = {}
|
|
13
|
+
|
|
13
14
|
class Executor
|
|
14
|
-
def self.
|
|
15
|
-
|
|
15
|
+
def self.inherited(subclass)
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.available?
|
|
20
|
+
true
|
|
16
21
|
end
|
|
17
22
|
|
|
18
23
|
def call(_timeout, &_block)
|
|
@@ -22,6 +27,10 @@ module Gemlings
|
|
|
22
27
|
|
|
23
28
|
# MRI / TruffleRuby: fork gives full process isolation and safe kill.
|
|
24
29
|
class ForkExecutor < Executor
|
|
30
|
+
def self.available?
|
|
31
|
+
Process.respond_to?(:fork)
|
|
32
|
+
end
|
|
33
|
+
|
|
25
34
|
def call(timeout, &block)
|
|
26
35
|
reader, writer = IO.pipe
|
|
27
36
|
|
|
@@ -57,6 +66,24 @@ module Gemlings
|
|
|
57
66
|
data.empty? ? { output: "", result: nil, is_final_answer: false } : Marshal.load(data) # rubocop:disable Security/MarshalLoad
|
|
58
67
|
end
|
|
59
68
|
end
|
|
69
|
+
EXECUTORS[:fork] = ForkExecutor
|
|
70
|
+
|
|
71
|
+
# Ruby 4.0+: Fork with Ruby::Box namespace isolation.
|
|
72
|
+
# Agent code runs in a separate process AND a separate namespace, so
|
|
73
|
+
# monkey-patches, constants, and class variables can't leak into the host.
|
|
74
|
+
class BoxExecutor < ForkExecutor
|
|
75
|
+
def self.available?
|
|
76
|
+
super && defined?(Ruby::Box) && Ruby::Box.enabled?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def call(timeout, &block)
|
|
80
|
+
super(timeout) do
|
|
81
|
+
box = Ruby::Box.new
|
|
82
|
+
block.call(box)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
EXECUTORS[:box] = BoxExecutor
|
|
60
87
|
|
|
61
88
|
# JRuby: fork is unavailable; use a thread with a join-based timeout.
|
|
62
89
|
# STDOUT_MUTEX serializes $stdout redirection so concurrent sandbox calls
|
|
@@ -82,19 +109,38 @@ module Gemlings
|
|
|
82
109
|
error ? { error: error } : result
|
|
83
110
|
end
|
|
84
111
|
end
|
|
112
|
+
EXECUTORS[:thread] = ThreadExecutor
|
|
113
|
+
|
|
114
|
+
def self.default_executor
|
|
115
|
+
if BoxExecutor.available?
|
|
116
|
+
:box
|
|
117
|
+
elsif ForkExecutor.available?
|
|
118
|
+
:fork
|
|
119
|
+
else
|
|
120
|
+
:thread
|
|
121
|
+
end
|
|
122
|
+
end
|
|
85
123
|
|
|
86
|
-
|
|
124
|
+
def self.resolve_executor(name)
|
|
125
|
+
klass = EXECUTORS[name]
|
|
126
|
+
raise ArgumentError, "Unknown executor: #{name.inspect}. Available: #{EXECUTORS.keys.join(", ")}" unless klass
|
|
127
|
+
unless klass.available?
|
|
128
|
+
raise Error, "Executor #{name.inspect} is not available on this platform (#{RUBY_ENGINE} #{RUBY_VERSION})"
|
|
129
|
+
end
|
|
130
|
+
klass.new
|
|
131
|
+
end
|
|
87
132
|
|
|
88
133
|
attr_reader :timeout
|
|
89
134
|
|
|
90
|
-
def initialize(tools:, timeout: DEFAULT_TIMEOUT)
|
|
135
|
+
def initialize(tools:, timeout: DEFAULT_TIMEOUT, executor: nil)
|
|
91
136
|
@tools = tools
|
|
92
137
|
@timeout = timeout
|
|
138
|
+
@executor = executor || self.class.resolve_executor(self.class.default_executor)
|
|
93
139
|
@tool_map = tools.each_with_object({}) { |t, h| h[t.class.tool_name] = t }
|
|
94
140
|
end
|
|
95
141
|
|
|
96
142
|
def execute(code)
|
|
97
|
-
|
|
143
|
+
@executor.call(@timeout) { run_in_child(code) }
|
|
98
144
|
end
|
|
99
145
|
|
|
100
146
|
private
|
|
@@ -15,12 +15,19 @@ module Gemlings
|
|
|
15
15
|
prompt
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def generate_response(messages, &on_stream)
|
|
18
|
+
def generate_response(messages, stream: false, &on_stream)
|
|
19
19
|
tool_schemas = tools.map { |t| t.class.to_schema }
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
if on_stream
|
|
21
|
+
response = @model.generate(messages, tools: tool_schemas, &on_stream)
|
|
22
|
+
elsif stream
|
|
23
|
+
response = @model.generate(messages, tools: tool_schemas) { |token| UI.stream_token(token) }
|
|
24
|
+
UI.stream_end
|
|
25
|
+
else
|
|
26
|
+
spin = UI.spinner("Thinking...")
|
|
27
|
+
spin.start
|
|
28
|
+
response = @model.generate(messages, tools: tool_schemas)
|
|
29
|
+
spin.stop
|
|
30
|
+
end
|
|
24
31
|
response
|
|
25
32
|
end
|
|
26
33
|
|
data/lib/gemlings/ui.rb
CHANGED
|
@@ -208,6 +208,14 @@ module Gemlings
|
|
|
208
208
|
Spinner.new(message)
|
|
209
209
|
end
|
|
210
210
|
|
|
211
|
+
def stream_token(text)
|
|
212
|
+
$stderr.print text
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def stream_end
|
|
216
|
+
$stderr.print "\n"
|
|
217
|
+
end
|
|
218
|
+
|
|
211
219
|
def welcome
|
|
212
220
|
if LIPGLOSS_AVAILABLE
|
|
213
221
|
title = Lipgloss::Style.new
|
data/lib/gemlings/version.rb
CHANGED
data/lib/gemlings.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gemlings
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chris Hasiński
|
|
@@ -83,16 +83,16 @@ dependencies:
|
|
|
83
83
|
name: mcp
|
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
|
85
85
|
requirements:
|
|
86
|
-
- - "
|
|
86
|
+
- - ">="
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
|
-
version:
|
|
88
|
+
version: 0.9.2
|
|
89
89
|
type: :runtime
|
|
90
90
|
prerelease: false
|
|
91
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
92
|
requirements:
|
|
93
|
-
- - "
|
|
93
|
+
- - ">="
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
|
-
version:
|
|
95
|
+
version: 0.9.2
|
|
96
96
|
- !ruby/object:Gem::Dependency
|
|
97
97
|
name: ruby_llm
|
|
98
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -181,6 +181,7 @@ files:
|
|
|
181
181
|
- lib/gemlings/model.rb
|
|
182
182
|
- lib/gemlings/models/ruby_llm_adapter.rb
|
|
183
183
|
- lib/gemlings/prompt.rb
|
|
184
|
+
- lib/gemlings/ruby_llm.rb
|
|
184
185
|
- lib/gemlings/sandbox.rb
|
|
185
186
|
- lib/gemlings/tool.rb
|
|
186
187
|
- lib/gemlings/tool_calling_agent.rb
|