regent 0.3.1 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +175 -20
- data/lib/regent/agent.rb +26 -8
- data/lib/regent/concerns/dependable.rb +5 -4
- data/lib/regent/concerns/toolable.rb +22 -0
- data/lib/regent/engine/base.rb +64 -0
- data/lib/regent/engine/react/prompt_template.rb +2 -2
- data/lib/regent/engine/react.rb +18 -81
- data/lib/regent/llm/anthropic.rb +17 -17
- data/lib/regent/llm/base.rb +13 -32
- data/lib/regent/llm/gemini.rb +21 -15
- data/lib/regent/llm/ollama.rb +57 -0
- data/lib/regent/llm/open_ai.rb +10 -14
- data/lib/regent/llm/open_router.rb +35 -0
- data/lib/regent/llm.rb +35 -5
- data/lib/regent/logger.rb +9 -3
- data/lib/regent/span.rb +7 -2
- data/lib/regent/tool.rb +8 -0
- data/lib/regent/toolchain.rb +15 -0
- data/lib/regent/version.rb +1 -1
- data/lib/regent.rb +2 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b507ccf9494ec028a77c396a80acd23bb686b4e1e15ba6b993430b479f495aeb
|
4
|
+
data.tar.gz: 8a00bcfaddae77f89b19ec72a9112c82e07086a2fee2713fc71fac5cbf0581b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e1249458a5fa9e035a9bc9c6dfb7102ec9efcdd6f1fbb71fe8638194e6181256ae09df6f7ec42ccb781bf641cdcae13c84c2104ec33e8543e5480868fe03bd77
|
7
|
+
data.tar.gz: be2357b3af64f96d69573bbc913c3322a11aa40434f84aba314e64ef066c78821ead732becb2f455c5f93251907f724ca7cd48109706108c66d3d57a9837b5b4
|
data/README.md
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
<div align="center">
|
4
4
|
|
5
5
|
# Regent
|
6
|
+
|
6
7
|
[](https://badge.fury.io/rb/regent)
|
7
8
|
[](https://github.com/alchaplinsky/regent/actions/workflows/main.yml)
|
8
9
|
[](https://opensource.org/licenses/MIT)
|
@@ -11,8 +12,10 @@
|
|
11
12
|
|
12
13
|
**Regent** is a small and elegant Ruby framework for building AI agents that can think, reason, and take actions through tools. It provides a clean, intuitive interface for creating agents that can solve complex problems by breaking them down into logical steps.
|
13
14
|
|
14
|
-
> [!
|
15
|
+
> [!NOTE]
|
15
16
|
> Regent is currently an experiment intended to explore patterns for building easily traceable and debuggable AI agents of different architectures. It is not yet intended to be used in production and is currently in development.
|
17
|
+
>
|
18
|
+
> Read more about Regent in a Medium article: [Building AI Agent from scratch with Ruby](https://medium.com/towards-artificial-intelligence/building-ai-agent-from-scratch-with-ruby-c6260dad45b7)
|
16
19
|
|
17
20
|
## Key Features
|
18
21
|
|
@@ -29,7 +32,7 @@
|
|
29
32
|
|
30
33
|
A basic Regnt Agent extended with a `price_tool` that allows for retrieving cryptocurrency prices from coingecko.com.
|
31
34
|
|
32
|
-

|
33
36
|
|
34
37
|
## Quick Start
|
35
38
|
|
@@ -51,39 +54,191 @@ bundle install
|
|
51
54
|
|
52
55
|
## Usage
|
53
56
|
|
54
|
-
|
57
|
+
### Quick Example
|
58
|
+
|
59
|
+
Create your first weather agent:
|
55
60
|
|
56
61
|
```ruby
|
57
|
-
#
|
58
|
-
|
62
|
+
# Define agent class
|
63
|
+
class WeatherAgent < Regent::Agent
|
64
|
+
tool(:weather_tool, "Get current weather for a location")
|
59
65
|
|
60
|
-
|
61
|
-
class WeatherTool < Regent::Tool
|
62
|
-
def call(location)
|
63
|
-
# Implement weather lookup logic
|
66
|
+
def weather_tool(location)
|
64
67
|
"Currently 72°F and sunny in #{location}"
|
65
68
|
end
|
66
69
|
end
|
67
70
|
|
68
|
-
#
|
69
|
-
agent =
|
70
|
-
"You are a helpful weather assistant",
|
71
|
-
llm: llm,
|
72
|
-
tools: [WeatherTool.new(
|
73
|
-
name: "weather_tool",
|
74
|
-
description: "Get current weather for a location"
|
75
|
-
)]
|
76
|
-
)
|
71
|
+
# Instantiate an agent
|
72
|
+
agent = WeatherAgent.new("You are a helpful weather assistant", model: "gpt-4o")
|
77
73
|
|
78
74
|
# Execute a query
|
79
|
-
|
75
|
+
agent.run("What's the weather like in Tokyo?") # => "It is currently 72°F and sunny in Tokyo."
|
76
|
+
```
|
77
|
+
|
78
|
+
### LLMs
|
79
|
+
Regent provides an interface for invoking an LLM through an instance of `Regent::LLM` class. Even though Agent initializer allows you to pass a modal name as a string, sometimes it is useful to create a model instance if you want to tune model params before passing it to the agent. Or if you need to invoke a model directly without passing it to an Agent you can do that by creating an instance of LLM class:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
model = Regent::LLM.new("gemini-1.5-flash")
|
83
|
+
# or with options
|
84
|
+
model = Regent::LLM.new("gemini-1.5-flash", temperature: 0.5) # supports options that are supported by the model
|
85
|
+
```
|
86
|
+
|
87
|
+
#### API keys
|
88
|
+
By default, **Regent** will try to fetch API keys for corresponding models from environment variables. Make sure that the following ENV variables are set depending on your model choice:
|
89
|
+
|
90
|
+
| Model series | ENV variable name |
|
91
|
+
|--------------|---------------------|
|
92
|
+
| `gpt-` | `OPENAI_API_KEY` |
|
93
|
+
| `gemini-` | `GEMINI_API_KEY` |
|
94
|
+
| `claude-` | `ANTHROPIC_API_KEY` |
|
95
|
+
|
96
|
+
But you can also pass an `api_key` option to the` Regent::LLM` constructor should you need to override this behavior:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
model = Regent::LLM.new("gemini-1.5-flash", api_key: "AIza...")
|
100
|
+
```
|
101
|
+
|
102
|
+
> [!NOTE]
|
103
|
+
> Currently **Regent** supports only `gpt-`, `gemini-` and `claude-` models series and local **ollama** models. But you can build, your custom model classes that conform to the Regent's interface and pass those instances to the Agent.
|
104
|
+
|
105
|
+
#### Calling LLM
|
106
|
+
Once your model is instantiated you can call the `invoke` method:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
model.invoke("Hello!")
|
110
|
+
```
|
111
|
+
|
112
|
+
Alternatively, you can pass message history to the `invoke` method. Messages need to follow OpenAI's message format (eg. `{role: "user", content: "..."}`)
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
model.invoke([
|
116
|
+
{role: "system", content: "You are a helpful assistant"},
|
117
|
+
{role: "user", content: "Hello!"}
|
118
|
+
])
|
80
119
|
```
|
81
120
|
|
121
|
+
This method returns an instance of the `Regent::LLM::Result` class, giving access to the content or error and token usage stats.
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
result = model.invoke("Hello!")
|
125
|
+
|
126
|
+
result.content # => Hello there! How can I help you today?
|
127
|
+
result.input_tokens # => 2
|
128
|
+
result.output_tokens # => 11
|
129
|
+
result.error # => nil
|
130
|
+
```
|
131
|
+
|
132
|
+
### Tools
|
133
|
+
|
134
|
+
There are multiple ways how you can give agents tools for performing actions and retrieving additional information. First of all you can define a **function tool** directly on the agent class:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
class MyAgent < Regent::Agent
|
138
|
+
# define the tool by giving a unique name and description
|
139
|
+
tool :search_web, "Search for information on the web"
|
140
|
+
|
141
|
+
def search_web(query)
|
142
|
+
# Implement tool logic within the method with the same name
|
143
|
+
end
|
144
|
+
end
|
145
|
+
```
|
146
|
+
|
147
|
+
For more complex tools we can define a dedicated class with a `call` method that will get called. And then pass an instance of this tool to an agent:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
class SearchTool < Regent::Tool
|
151
|
+
def call(query)
|
152
|
+
# Implement tool logic
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
agent = Regent::Agent.new("Find information and answer any question", {
|
157
|
+
model: "gpt-4o",
|
158
|
+
tools: [SearchTool.new]
|
159
|
+
})
|
160
|
+
|
161
|
+
```
|
162
|
+
|
163
|
+
### Agent
|
164
|
+
|
165
|
+
**Agent** class is the core of the library. To crate an agent, you can use `Regent::Agent` class directly if you don't need to add any business logic. Or you can create your own class inheriting from `Regent::Agent`. To instantiate an agent you need to pass a **purpose** of an agent and a model it should use.
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
agent = Regent::Agent.new("You are a helpful assistant", model: "gpt-4o-mini")
|
169
|
+
```
|
170
|
+
|
171
|
+
Additionally, you can pass a list of Tools to extend the agent's capabilities. Those should be instances of classes that inherit from `Regent::Tool` class:
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
class SearchTool < Regent::Tool
|
175
|
+
def call
|
176
|
+
# make a call to search API
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
class CalculatorTool < Regent::Tool
|
181
|
+
def call
|
182
|
+
# perform calculations
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
tools = [SearchTool.new, CalculatorTool.new]
|
187
|
+
|
188
|
+
agent = Regent::Agent.new("You are a helpful assistant", model: "gpt-4o-mini", tools: tools)
|
189
|
+
```
|
190
|
+
|
191
|
+
Each agent run creates a **session** that contains every operation that is performed by the agent while working on a task. Sessions can be replayed and drilled down into while debugging.
|
192
|
+
```ruby
|
193
|
+
agent.sessions # => Returns all sessions performed by the agent
|
194
|
+
agent.session # => Returns last session performed by the agent
|
195
|
+
agent.session.result # => Returns result of latest agent run
|
196
|
+
```
|
197
|
+
|
198
|
+
While running agent logs all session spans (all operations) to the console with all sorts of useful information, that helps to understand what the agent was doing and why it took a certain path.
|
199
|
+
```ruby
|
200
|
+
weather_agent.run("What is the weather in San Francisco?")
|
201
|
+
```
|
202
|
+
|
203
|
+
Outputs:
|
204
|
+
```console
|
205
|
+
[✔] [INPUT][0.0s]: What is the weather in San Francisco?
|
206
|
+
├──[✔] [LLM ❯ gpt-4o-mini][242 → 30 tokens][0.02s]: What is the weather in San Francisco?
|
207
|
+
├──[✔] [TOOL ❯ get_weather][0.0s]: ["San Francisco"] → The weather in San Francisco is 70 degrees and sunny.
|
208
|
+
├──[✔] [LLM ❯ gpt-4o-mini][294 → 26 tokens][0.01s]: Observation: The weather in San Francisco is 70 degrees and sunny.
|
209
|
+
[✔] [ANSWER ❯ success][0.03s]: It is 70 degrees and sunny in San Francisco.
|
210
|
+
```
|
211
|
+
|
212
|
+
### Engine
|
213
|
+
By default, Regent uses ReAct agent architecture. You can see the [details of its implementation](https://github.com/alchaplinsky/regent/blob/main/lib/regent/engine/react.rb). However, Agent constructor accepts an `engine` option that allows you to swap agent engine when instantiating an Agent. This way you can implement your own agent architecture that can be plugged in and user within Regent framework.
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
agent = CustomAgent.new("You are a self-correcting assistant", model: "gpt-4o", engine: CustomEngine)
|
217
|
+
```
|
218
|
+
|
219
|
+
In order to implement your own engine you need to define a class that inherits from `Regent::Engine::Base` class and implements `reason` method:
|
220
|
+
|
221
|
+
```ruby
|
222
|
+
class CustomEngine < Regent::Engine::Base
|
223
|
+
def reason(task)
|
224
|
+
# Your implementation of an Agent lifecycle
|
225
|
+
end
|
226
|
+
end
|
227
|
+
```
|
228
|
+
|
229
|
+
Note that Base class already handles `max_iteration` check, so you won't end up in an infinite loop. Also, it allows you to use `llm_call_response` and `tool_call_response` methods for agent reasoning as well as `success_answer` and `error_answer` for the final result.
|
230
|
+
|
231
|
+
For any other operation that happens in your agent architecture that you want to track separately call it within the `session.exec` block. See examples in `Regent::Engine::Base` class.
|
232
|
+
|
233
|
+
|
234
|
+
---
|
82
235
|
## Why Regent?
|
236
|
+
|
83
237
|
- **Transparent Decision Making**: Watch your agent's thought process as it reasons through problems
|
84
238
|
- **Flexible Architecture**: Easy to extend with custom tools and adapt to different use cases
|
85
|
-
- **Production Ready**: Built with tracing, error handling, and clean abstractions
|
86
239
|
- **Ruby-First Design**: Takes advantage of Ruby's elegant syntax and conventions
|
240
|
+
- **Transparent Execution**: Built with tracing, error handling, and clean abstractions
|
241
|
+
|
87
242
|
|
88
243
|
## Development
|
89
244
|
|
data/lib/regent/agent.rb
CHANGED
@@ -3,26 +3,28 @@
|
|
3
3
|
module Regent
|
4
4
|
class Agent
|
5
5
|
include Concerns::Identifiable
|
6
|
+
include Concerns::Toolable
|
6
7
|
|
7
8
|
DEFAULT_MAX_ITERATIONS = 10
|
8
9
|
|
9
|
-
def initialize(context,
|
10
|
+
def initialize(context, model:, tools: [], engine: Regent::Engine::React, **options)
|
10
11
|
super()
|
11
12
|
|
12
13
|
@context = context
|
13
|
-
@
|
14
|
+
@model = model.is_a?(String) ? Regent::LLM.new(model) : model
|
15
|
+
@engine = engine
|
14
16
|
@sessions = []
|
15
|
-
@tools =
|
17
|
+
@tools = build_toolchain(tools)
|
16
18
|
@max_iterations = options[:max_iterations] || DEFAULT_MAX_ITERATIONS
|
17
19
|
end
|
18
20
|
|
19
|
-
attr_reader :context, :sessions, :
|
21
|
+
attr_reader :context, :sessions, :model, :tools, :inline_tools
|
20
22
|
|
21
|
-
def
|
23
|
+
def run(task)
|
22
24
|
raise ArgumentError, "Task cannot be empty" if task.to_s.strip.empty?
|
23
25
|
|
24
26
|
start_session
|
25
|
-
|
27
|
+
reason(task)
|
26
28
|
ensure
|
27
29
|
complete_session
|
28
30
|
end
|
@@ -37,6 +39,10 @@ module Regent
|
|
37
39
|
|
38
40
|
private
|
39
41
|
|
42
|
+
def reason(task)
|
43
|
+
engine.reason(task)
|
44
|
+
end
|
45
|
+
|
40
46
|
def start_session
|
41
47
|
complete_session
|
42
48
|
@sessions << Session.new
|
@@ -47,8 +53,20 @@ module Regent
|
|
47
53
|
session&.complete if running?
|
48
54
|
end
|
49
55
|
|
50
|
-
def
|
51
|
-
|
56
|
+
def build_toolchain(tools)
|
57
|
+
context = self
|
58
|
+
|
59
|
+
toolchain = Toolchain.new(Array(tools))
|
60
|
+
|
61
|
+
self.class.function_tools.each do |entry|
|
62
|
+
toolchain.add(entry, context)
|
63
|
+
end
|
64
|
+
|
65
|
+
toolchain
|
66
|
+
end
|
67
|
+
|
68
|
+
def engine
|
69
|
+
@engine.new(context, model, tools, session, @max_iterations)
|
52
70
|
end
|
53
71
|
end
|
54
72
|
end
|
@@ -25,7 +25,7 @@ module Regent
|
|
25
25
|
|
26
26
|
super()
|
27
27
|
rescue Gem::LoadError
|
28
|
-
warn_and_exit(dependency,
|
28
|
+
Regent::Logger.warn_and_exit dependency_warning(dependency, model)
|
29
29
|
end
|
30
30
|
|
31
31
|
def require_dynamic(*names)
|
@@ -34,6 +34,8 @@ module Regent
|
|
34
34
|
|
35
35
|
private
|
36
36
|
|
37
|
+
attr_reader :dependency
|
38
|
+
|
37
39
|
def load_dependency(name)
|
38
40
|
gem(name)
|
39
41
|
|
@@ -63,9 +65,8 @@ module Regent
|
|
63
65
|
Bundler.load.dependencies
|
64
66
|
end
|
65
67
|
|
66
|
-
def
|
67
|
-
|
68
|
-
exit 1
|
68
|
+
def dependency_warning(dependency, model)
|
69
|
+
"\n\e[33mIn order to use \e[33;1m#{model}\e[0m\e[33m model you need to install \e[33;1m#{dependency}\e[0m\e[33m gem. Please add \e[33;1mgem \"#{dependency}\"\e[0m\e[33m to your Gemfile.\e[0m"
|
69
70
|
end
|
70
71
|
end
|
71
72
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Regent
|
4
|
+
module Concerns
|
5
|
+
module Toolable
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
class << self
|
9
|
+
def tool(name, description)
|
10
|
+
@function_tools ||= []
|
11
|
+
@function_tools << { name: name, description: description }
|
12
|
+
end
|
13
|
+
|
14
|
+
def function_tools
|
15
|
+
@function_tools || []
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Regent
|
4
|
+
module Engine
|
5
|
+
class Base
|
6
|
+
def initialize(context, llm, toolchain, session, max_iterations)
|
7
|
+
@context = context
|
8
|
+
@llm = llm
|
9
|
+
@toolchain = toolchain
|
10
|
+
@session = session
|
11
|
+
@max_iterations = max_iterations
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :context, :llm, :toolchain, :session, :max_iterations
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# Run reasoning block within this method to ensure that it
|
19
|
+
# will not run more than max_iterations times.
|
20
|
+
def with_max_iterations
|
21
|
+
max_iterations.times do
|
22
|
+
yield
|
23
|
+
end
|
24
|
+
|
25
|
+
error_answer("Max iterations reached without finding an answer.")
|
26
|
+
end
|
27
|
+
|
28
|
+
# Make a call to LLM and return the response.
|
29
|
+
def llm_call_response(args)
|
30
|
+
session.exec(Span::Type::LLM_CALL, type: llm.model, message: session.messages.last[:content]) do
|
31
|
+
result = llm.invoke(session.messages, **args)
|
32
|
+
|
33
|
+
session.current_span.set_meta("#{result.input_tokens} → #{result.output_tokens} tokens")
|
34
|
+
result.content
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Make a call to a tool and return the response.
|
39
|
+
def tool_call_response(tool, arguments)
|
40
|
+
session.exec(Span::Type::TOOL_EXECUTION, { type: tool.name, message: arguments }) do
|
41
|
+
tool.execute(*arguments)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Find a tool in the toolchain by name and return it.
|
46
|
+
def find_tool(tool_name)
|
47
|
+
tool = toolchain.find(tool_name)
|
48
|
+
return tool if tool
|
49
|
+
|
50
|
+
session.exec(Span::Type::ANSWER, type: :failure, message: "No matching tool found for: #{tool_name}")
|
51
|
+
end
|
52
|
+
|
53
|
+
# Complete a session with a success answer
|
54
|
+
def success_answer(content)
|
55
|
+
session.exec(Span::Type::ANSWER, top_level: true, type: :success, message: content, duration: session.duration.round(2)) { content }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Complete a session with an error answer
|
59
|
+
def error_answer(content)
|
60
|
+
session.exec(Span::Type::ANSWER, top_level: true, type: :failure, message: content, duration: session.duration.round(2)) { content }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -13,7 +13,7 @@ module Regent
|
|
13
13
|
Thought - a description of your thoughts about the question.
|
14
14
|
Action - pick a an action from available tools if required. If there are no tools that can help return an Answer saying you are not able to help.
|
15
15
|
Observation - is the result of running a tool.
|
16
|
-
PAUSE -
|
16
|
+
PAUSE - a stop sequence that will always be present after an Action.
|
17
17
|
|
18
18
|
## Available tools:
|
19
19
|
#{tool_list}
|
@@ -21,7 +21,7 @@ module Regent
|
|
21
21
|
## Example session
|
22
22
|
Question: What is the weather in London today?
|
23
23
|
Thought: I need to get current weather in London
|
24
|
-
Action: weather_tool
|
24
|
+
Action: {"tool": "weather_tool", "args": ["London"]}
|
25
25
|
PAUSE
|
26
26
|
|
27
27
|
You will have a response form a user with Observation:
|
data/lib/regent/engine/react.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Regent
|
4
4
|
module Engine
|
5
|
-
class React
|
5
|
+
class React < Base
|
6
6
|
SEQUENCES = {
|
7
7
|
answer: "Answer:",
|
8
8
|
action: "Action:",
|
@@ -10,70 +10,29 @@ module Regent
|
|
10
10
|
stop: "PAUSE"
|
11
11
|
}.freeze
|
12
12
|
|
13
|
-
def initialize(context, llm, toolchain, session, max_iterations)
|
14
|
-
@context = context
|
15
|
-
@llm = llm
|
16
|
-
@toolchain = toolchain
|
17
|
-
@session = session
|
18
|
-
@max_iterations = max_iterations
|
19
|
-
end
|
20
|
-
|
21
|
-
attr_reader :context, :llm, :toolchain, :session, :max_iterations
|
22
|
-
|
23
13
|
def reason(task)
|
24
|
-
|
14
|
+
session.exec(Span::Type::INPUT, top_level: true, message: task) { task }
|
15
|
+
session.add_message({role: :system, content: Regent::Engine::React::PromptTemplate.system_prompt(context, toolchain.to_s)})
|
16
|
+
session.add_message({role: :user, content: task})
|
25
17
|
|
26
|
-
|
27
|
-
content =
|
18
|
+
with_max_iterations do
|
19
|
+
content = llm_call_response(stop: [SEQUENCES[:stop]])
|
28
20
|
session.add_message({role: :assistant, content: content })
|
21
|
+
|
29
22
|
return extract_answer(content) if answer_present?(content)
|
30
23
|
|
31
24
|
if action_present?(content)
|
32
|
-
|
25
|
+
tool_name, arguments = parse_tool_signature(content)
|
26
|
+
tool = find_tool(tool_name)
|
33
27
|
return unless tool
|
34
|
-
|
35
|
-
|
28
|
+
result = tool_call_response(tool, arguments)
|
29
|
+
session.add_message({ role: :user, content: "#{SEQUENCES[:observation]} #{result}" })
|
36
30
|
end
|
37
31
|
end
|
38
|
-
|
39
|
-
error_answer("Max iterations reached without finding an answer.")
|
40
32
|
end
|
41
33
|
|
42
34
|
private
|
43
35
|
|
44
|
-
def initialize_session(task)
|
45
|
-
session.add_message({role: :system, content: Regent::Engine::React::PromptTemplate.system_prompt(context, toolchain.to_s)})
|
46
|
-
session.add_message({role: :user, content: task})
|
47
|
-
session.exec(Span::Type::INPUT, top_level: true, message: task) { task }
|
48
|
-
end
|
49
|
-
|
50
|
-
def get_llm_response
|
51
|
-
session.exec(Span::Type::LLM_CALL, type: llm.model, message: session.messages.last[:content]) do
|
52
|
-
result = llm.invoke(session.messages, stop: [SEQUENCES[:stop]])
|
53
|
-
|
54
|
-
session.current_span.set_meta("#{result.usage.input_tokens} → #{result.usage.output_tokens} tokens")
|
55
|
-
result.content
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def extract_answer(content)
|
60
|
-
answer = content.split(SEQUENCES[:answer])[1]&.strip
|
61
|
-
success_answer(answer)
|
62
|
-
end
|
63
|
-
|
64
|
-
def parse_action(content)
|
65
|
-
sanitized_content = content.gsub(SEQUENCES[:stop], "")
|
66
|
-
lookup_tool(sanitized_content)
|
67
|
-
end
|
68
|
-
|
69
|
-
def process_tool_execution(tool, argument)
|
70
|
-
result = session.exec(Span::Type::TOOL_EXECUTION, { type: tool.name, message: argument }) do
|
71
|
-
tool.call(argument)
|
72
|
-
end
|
73
|
-
|
74
|
-
session.add_message({ role: :user, content: "#{SEQUENCES[:observation]} #{result}" })
|
75
|
-
end
|
76
|
-
|
77
36
|
def answer_present?(content)
|
78
37
|
content.include?(SEQUENCES[:answer])
|
79
38
|
end
|
@@ -82,39 +41,17 @@ module Regent
|
|
82
41
|
content.include?(SEQUENCES[:action])
|
83
42
|
end
|
84
43
|
|
85
|
-
def
|
86
|
-
|
87
|
-
end
|
88
|
-
|
89
|
-
def error_answer(content)
|
90
|
-
session.exec(Span::Type::ANSWER, top_level: true, type: :failure, message: content, duration: session.duration.round(2)) { content }
|
91
|
-
end
|
92
|
-
|
93
|
-
def lookup_tool(content)
|
94
|
-
tool_name, argument = parse_tool_signature(content)
|
95
|
-
tool = toolchain.find(tool_name)
|
96
|
-
|
97
|
-
unless tool
|
98
|
-
session.exec(Span::Type::ANSWER, type: :failure, message: "No matching tool found for: #{tool_name}")
|
99
|
-
return [nil, nil]
|
100
|
-
end
|
101
|
-
|
102
|
-
[tool, argument]
|
44
|
+
def extract_answer(content)
|
45
|
+
success_answer content.split(SEQUENCES[:answer])[1]&.strip
|
103
46
|
end
|
104
47
|
|
105
48
|
def parse_tool_signature(content)
|
106
|
-
|
107
|
-
return [nil, nil] unless action
|
108
|
-
|
109
|
-
parts = action.split('|').map(&:strip)
|
110
|
-
tool_name = parts[0].gsub(/["`']/, '')
|
111
|
-
argument = parts[1].gsub(/["`']/, '')
|
112
|
-
|
113
|
-
# Handle cases where argument is nil, empty, or only whitespace
|
114
|
-
argument = nil if argument.nil? || argument.empty?
|
49
|
+
return [nil, nil] unless match = content.match(/Action:.*?\{.*"tool".*\}/m)
|
115
50
|
|
116
|
-
|
117
|
-
|
51
|
+
# Extract just the JSON part using a second regex
|
52
|
+
json = JSON.parse(match[0].match(/\{.*\}/m)[0])
|
53
|
+
[json["tool"], json["args"] || []]
|
54
|
+
rescue JSON::ParserError
|
118
55
|
[nil, nil]
|
119
56
|
end
|
120
57
|
end
|
data/lib/regent/llm/anthropic.rb
CHANGED
@@ -9,14 +9,25 @@ module Regent
|
|
9
9
|
depends_on "anthropic"
|
10
10
|
|
11
11
|
def invoke(messages, **args)
|
12
|
-
|
12
|
+
parameters = {
|
13
13
|
messages: format_messages(messages),
|
14
|
-
|
15
|
-
|
16
|
-
stop_sequences: args[:stop]
|
14
|
+
model: model,
|
15
|
+
temperature: args[:temperature] || 0.0,
|
16
|
+
stop_sequences: args[:stop] || [],
|
17
17
|
max_tokens: MAX_TOKENS
|
18
|
-
}
|
19
|
-
|
18
|
+
}
|
19
|
+
if system_instruction = system_instruction(messages)
|
20
|
+
parameters[:system] = system_instruction
|
21
|
+
end
|
22
|
+
|
23
|
+
response = client.messages(parameters:)
|
24
|
+
|
25
|
+
result(
|
26
|
+
model: model,
|
27
|
+
content: response.dig("content", 0, "text"),
|
28
|
+
input_tokens: response.dig("usage", "input_tokens"),
|
29
|
+
output_tokens: response.dig("usage", "output_tokens")
|
30
|
+
)
|
20
31
|
end
|
21
32
|
|
22
33
|
private
|
@@ -32,17 +43,6 @@ module Regent
|
|
32
43
|
def format_messages(messages)
|
33
44
|
messages.reject { |message| message[:role].to_s == "system" }
|
34
45
|
end
|
35
|
-
|
36
|
-
def format_response(response)
|
37
|
-
Response.new(
|
38
|
-
content: response.dig("content", 0, "text"),
|
39
|
-
model: options[:model],
|
40
|
-
usage: Usage.new(
|
41
|
-
input_tokens: response.dig("usage", "input_tokens"),
|
42
|
-
output_tokens: response.dig("usage", "output_tokens")
|
43
|
-
)
|
44
|
-
)
|
45
|
-
end
|
46
46
|
end
|
47
47
|
end
|
48
48
|
end
|
data/lib/regent/llm/base.rb
CHANGED
@@ -2,55 +2,36 @@
|
|
2
2
|
|
3
3
|
module Regent
|
4
4
|
class LLM
|
5
|
-
|
6
|
-
def initialize(content:, usage:, model:)
|
7
|
-
@content = content
|
8
|
-
@usage = usage
|
9
|
-
@model = model
|
10
|
-
end
|
11
|
-
|
12
|
-
attr_reader :content, :usage, :model
|
13
|
-
end
|
14
|
-
|
15
|
-
class Usage
|
16
|
-
def initialize(input_tokens:, output_tokens:)
|
17
|
-
@input_tokens = input_tokens
|
18
|
-
@output_tokens = output_tokens
|
19
|
-
end
|
20
|
-
|
21
|
-
attr_reader :input_tokens, :output_tokens
|
22
|
-
end
|
5
|
+
Result = Struct.new(:model, :content, :input_tokens, :output_tokens, keyword_init: true)
|
23
6
|
|
24
7
|
class Base
|
25
8
|
include Concerns::Dependable
|
26
9
|
|
27
|
-
def initialize(**options)
|
10
|
+
def initialize(model:, api_key: nil, **options)
|
11
|
+
@model = model
|
12
|
+
@api_key = api_key || api_key_from_env
|
28
13
|
@options = options
|
29
|
-
api_key.nil?
|
30
14
|
|
31
15
|
super()
|
32
16
|
end
|
33
17
|
|
34
|
-
def
|
35
|
-
|
18
|
+
def parse_error(error)
|
19
|
+
error.response.dig(:body, "error", "message")
|
36
20
|
end
|
37
21
|
|
38
22
|
private
|
39
23
|
|
40
|
-
attr_reader :
|
24
|
+
attr_reader :model, :api_key, :options
|
41
25
|
|
42
|
-
def
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
26
|
+
def result(model:, content:, input_tokens:, output_tokens:)
|
27
|
+
Result.new(
|
28
|
+
model: model,
|
29
|
+
content: content,
|
30
|
+
input_tokens: input_tokens,
|
31
|
+
output_tokens: output_tokens
|
47
32
|
)
|
48
33
|
end
|
49
34
|
|
50
|
-
def api_key
|
51
|
-
@api_key ||= options[:api_key] || api_key_from_env
|
52
|
-
end
|
53
|
-
|
54
35
|
def api_key_from_env
|
55
36
|
ENV.fetch(self.class::ENV_KEY) do
|
56
37
|
raise APIKeyNotFoundError, "API key not found. Make sure to set #{self.class::ENV_KEY} environment variable."
|
data/lib/regent/llm/gemini.rb
CHANGED
@@ -4,20 +4,37 @@ module Regent
|
|
4
4
|
class LLM
|
5
5
|
class Gemini < Base
|
6
6
|
ENV_KEY = "GEMINI_API_KEY"
|
7
|
+
SERVICE = "generative-language-api"
|
7
8
|
|
8
9
|
depends_on "gemini-ai"
|
9
10
|
|
10
11
|
def invoke(messages, **args)
|
11
|
-
response = client.generate_content({
|
12
|
-
|
12
|
+
response = client.generate_content({
|
13
|
+
contents: format_messages(messages),
|
14
|
+
generation_config: {
|
15
|
+
temperature: args[:temperature] || 0.0,
|
16
|
+
stop_sequences: args[:stop] || []
|
17
|
+
}
|
18
|
+
})
|
19
|
+
|
20
|
+
result(
|
21
|
+
model: model,
|
22
|
+
content: response.dig("candidates", 0, "content", "parts", 0, "text").strip,
|
23
|
+
input_tokens: response.dig("usageMetadata", "promptTokenCount"),
|
24
|
+
output_tokens: response.dig("usageMetadata", "candidatesTokenCount")
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse_error(error)
|
29
|
+
JSON.parse(error.response.dig(:body)).dig("error", "message")
|
13
30
|
end
|
14
31
|
|
15
32
|
private
|
16
33
|
|
17
34
|
def client
|
18
35
|
@client ||= ::Gemini.new(
|
19
|
-
credentials: { service:
|
20
|
-
options: { model:
|
36
|
+
credentials: { service: SERVICE, api_key: api_key },
|
37
|
+
options: { model: model }
|
21
38
|
)
|
22
39
|
end
|
23
40
|
|
@@ -26,17 +43,6 @@ module Regent
|
|
26
43
|
{ role: message[:role].to_s == "system" ? "user" : message[:role], parts: [{ text: message[:content] }] }
|
27
44
|
end
|
28
45
|
end
|
29
|
-
|
30
|
-
def format_response(response)
|
31
|
-
Response.new(
|
32
|
-
content: response.dig("candidates", 0, "content", "parts", 0, "text").strip,
|
33
|
-
model: options[:model],
|
34
|
-
usage: Usage.new(
|
35
|
-
input_tokens: response.dig("usageMetadata", "promptTokenCount"),
|
36
|
-
output_tokens: response.dig("usageMetadata", "candidatesTokenCount")
|
37
|
-
)
|
38
|
-
)
|
39
|
-
end
|
40
46
|
end
|
41
47
|
end
|
42
48
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Regent
|
4
|
+
class LLM
|
5
|
+
class Ollama < Base
|
6
|
+
# Default host for Ollama API.
|
7
|
+
DEFAULT_HOST = "http://localhost:11434"
|
8
|
+
|
9
|
+
def initialize(model:, host: nil, **options)
|
10
|
+
@model = model
|
11
|
+
@host = host || DEFAULT_HOST
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :model
|
16
|
+
|
17
|
+
def invoke(messages, **args)
|
18
|
+
response = client.post("/api/chat", {
|
19
|
+
model: model,
|
20
|
+
messages: messages,
|
21
|
+
stream: false
|
22
|
+
})
|
23
|
+
|
24
|
+
if response.status == 200
|
25
|
+
result(
|
26
|
+
model: response.body.dig("model"),
|
27
|
+
content: response.body.dig("message", "content").strip,
|
28
|
+
input_tokens: nil,
|
29
|
+
output_tokens: nil
|
30
|
+
)
|
31
|
+
else
|
32
|
+
raise ApiError, response.body.dig("error")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse_error(error)
|
37
|
+
error.message
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :host
|
43
|
+
|
44
|
+
def client
|
45
|
+
@client ||= Faraday.new(host) do |f|
|
46
|
+
f.request :json
|
47
|
+
f.response :json
|
48
|
+
f.adapter :net_http
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def api_key_from_env
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/regent/llm/open_ai.rb
CHANGED
@@ -10,10 +10,17 @@ module Regent
|
|
10
10
|
def invoke(messages, **args)
|
11
11
|
response = client.chat(parameters: {
|
12
12
|
messages: messages,
|
13
|
-
model:
|
14
|
-
|
13
|
+
model: model,
|
14
|
+
temperature: args[:temperature] || 0.0,
|
15
|
+
stop: args[:stop] || []
|
15
16
|
})
|
16
|
-
|
17
|
+
|
18
|
+
result(
|
19
|
+
model: model,
|
20
|
+
content: response.dig("choices", 0, "message", "content"),
|
21
|
+
input_tokens: response.dig("usage", "prompt_tokens"),
|
22
|
+
output_tokens: response.dig("usage", "completion_tokens")
|
23
|
+
)
|
17
24
|
end
|
18
25
|
|
19
26
|
private
|
@@ -21,17 +28,6 @@ module Regent
|
|
21
28
|
def client
|
22
29
|
@client ||= ::OpenAI::Client.new(access_token: api_key)
|
23
30
|
end
|
24
|
-
|
25
|
-
def format_response(response)
|
26
|
-
Response.new(
|
27
|
-
content: response.dig("choices", 0, "message", "content"),
|
28
|
-
model: options[:model],
|
29
|
-
usage: Usage.new(
|
30
|
-
input_tokens: response.dig("usage", "prompt_tokens"),
|
31
|
-
output_tokens: response.dig("usage", "completion_tokens")
|
32
|
-
)
|
33
|
-
)
|
34
|
-
end
|
35
31
|
end
|
36
32
|
end
|
37
33
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Regent
|
4
|
+
class LLM
|
5
|
+
class OpenRouter < Base
|
6
|
+
ENV_KEY = "OPEN_ROUTER_API_KEY"
|
7
|
+
|
8
|
+
depends_on "open_router"
|
9
|
+
|
10
|
+
def invoke(messages, **args)
|
11
|
+
response = client.complete(
|
12
|
+
messages,
|
13
|
+
model: model,
|
14
|
+
extras: {
|
15
|
+
temperature: args[:temperature] || 0.0,
|
16
|
+
stop: args[:stop] || [],
|
17
|
+
**args
|
18
|
+
}
|
19
|
+
)
|
20
|
+
result(
|
21
|
+
model: model,
|
22
|
+
content: response.dig("choices", 0, "message", "content"),
|
23
|
+
input_tokens: response.dig("usage", "prompt_tokens"),
|
24
|
+
output_tokens: response.dig("usage", "completion_tokens")
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def client
|
31
|
+
@client ||= ::OpenRouter::Client.new access_token: api_key
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/regent/llm.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
module Regent
|
4
4
|
class LLM
|
5
|
+
DEFAULT_RETRY_COUNT = 3
|
5
6
|
PROVIDER_PATTERNS = {
|
6
7
|
OpenAI: /^gpt-/,
|
7
8
|
Gemini: /^gemini-/,
|
@@ -10,28 +11,46 @@ module Regent
|
|
10
11
|
|
11
12
|
class ProviderNotFoundError < StandardError; end
|
12
13
|
class APIKeyNotFoundError < StandardError; end
|
14
|
+
class ApiError < StandardError; end
|
13
15
|
|
14
|
-
def initialize(model, **options)
|
15
|
-
@
|
16
|
+
def initialize(model, strict_mode: true, **options)
|
17
|
+
@strict_mode = strict_mode
|
16
18
|
@options = options
|
17
|
-
|
19
|
+
if model.class.ancestors.include?(Regent::LLM::Base)
|
20
|
+
@model = model.model
|
21
|
+
@provider = model
|
22
|
+
else
|
23
|
+
@model = model
|
24
|
+
@provider = instantiate_provider
|
25
|
+
end
|
18
26
|
end
|
19
27
|
|
20
28
|
attr_reader :model, :options
|
21
29
|
|
22
30
|
def invoke(messages, **args)
|
31
|
+
retries = 0
|
32
|
+
|
33
|
+
messages = [{ role: "user", content: messages }] if messages.is_a?(String)
|
34
|
+
|
23
35
|
provider.invoke(messages, **args)
|
36
|
+
|
37
|
+
rescue Faraday::Error, ApiError => error
|
38
|
+
if error.respond_to?(:retryable?) && error.retryable? && retries < DEFAULT_RETRY_COUNT
|
39
|
+
sleep(exponential_backoff(retries))
|
40
|
+
retry
|
41
|
+
end
|
42
|
+
handle_error(error)
|
24
43
|
end
|
25
44
|
|
26
45
|
private
|
27
46
|
|
28
|
-
attr_reader :provider
|
47
|
+
attr_reader :provider, :strict_mode
|
29
48
|
|
30
49
|
def instantiate_provider
|
31
50
|
provider_class = find_provider_class
|
32
51
|
raise ProviderNotFoundError, "Provider for #{model} is not found" if provider_class.nil?
|
33
52
|
|
34
|
-
|
53
|
+
create_provider(provider_class)
|
35
54
|
end
|
36
55
|
|
37
56
|
def find_provider_class
|
@@ -41,5 +60,16 @@ module Regent
|
|
41
60
|
def create_provider(provider_class)
|
42
61
|
Regent::LLM.const_get(provider_class).new(**options.merge(model: model))
|
43
62
|
end
|
63
|
+
|
64
|
+
def handle_error(error)
|
65
|
+
message = provider.parse_error(error) || error.message
|
66
|
+
raise ApiError, message if strict_mode
|
67
|
+
Result.new(model: model, content: message, input_tokens: nil, output_tokens: nil)
|
68
|
+
end
|
69
|
+
|
70
|
+
def exponential_backoff(retry_count)
|
71
|
+
# Exponential backoff with jitter: 2^n * 100ms + random jitter
|
72
|
+
(2**retry_count * 0.1) + rand(0.1)
|
73
|
+
end
|
44
74
|
end
|
45
75
|
end
|
data/lib/regent/logger.rb
CHANGED
@@ -2,7 +2,14 @@
|
|
2
2
|
|
3
3
|
module Regent
|
4
4
|
class Logger
|
5
|
-
COLORS = %i[dim green yellow red blue cyan clear].freeze
|
5
|
+
COLORS = %i[dim white green yellow red blue cyan clear].freeze
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def warn_and_exit(message)
|
9
|
+
warn message
|
10
|
+
exit 1
|
11
|
+
end
|
12
|
+
end
|
6
13
|
|
7
14
|
def initialize(output: $stdout)
|
8
15
|
@pastel = Pastel.new
|
@@ -14,7 +21,6 @@ module Regent
|
|
14
21
|
|
15
22
|
def info(label:, message:, duration: nil, type: nil, meta: nil, top_level: false)
|
16
23
|
current_spinner = top_level ? spinner : nested_spinner
|
17
|
-
|
18
24
|
current_spinner.update(title: format_message(label, message, duration, type, meta))
|
19
25
|
current_spinner
|
20
26
|
end
|
@@ -47,7 +53,7 @@ module Regent
|
|
47
53
|
end
|
48
54
|
|
49
55
|
def spinner_symbol
|
50
|
-
"#{dim("[")}#{
|
56
|
+
"#{dim("[")}#{white(":spinner")}#{dim("]")}"
|
51
57
|
end
|
52
58
|
|
53
59
|
def build_spinner(spinner_format, output)
|
data/lib/regent/span.rb
CHANGED
@@ -43,7 +43,8 @@ module Regent
|
|
43
43
|
def run
|
44
44
|
@output = log_operation do
|
45
45
|
yield
|
46
|
-
|
46
|
+
|
47
|
+
rescue StandardError, ToolError => e
|
47
48
|
logger.error(label: type, message: e.message, **arguments)
|
48
49
|
raise
|
49
50
|
end
|
@@ -84,9 +85,13 @@ module Regent
|
|
84
85
|
result = yield
|
85
86
|
|
86
87
|
@end_time = live ? Time.now.freeze : @end_time
|
88
|
+
update_message_with_result(result) if type == Type::TOOL_EXECUTION
|
87
89
|
logger.success(label: type, **({ duration: duration.round(2), meta: meta }.merge(arguments)))
|
88
|
-
|
89
90
|
result
|
90
91
|
end
|
92
|
+
|
93
|
+
def update_message_with_result(message)
|
94
|
+
arguments[:message] = "#{arguments[:message]} → #{message}"
|
95
|
+
end
|
91
96
|
end
|
92
97
|
end
|
data/lib/regent/tool.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Regent
|
4
|
+
class ToolError < StandardError; end
|
5
|
+
|
4
6
|
class Tool
|
5
7
|
def initialize(name:, description:)
|
6
8
|
@name = name
|
@@ -13,6 +15,12 @@ module Regent
|
|
13
15
|
raise NotImplementedError, "Tool #{name} has not implemented the execute method"
|
14
16
|
end
|
15
17
|
|
18
|
+
def execute(*arguments)
|
19
|
+
call(*arguments)
|
20
|
+
rescue NotImplementedError, StandardError => e
|
21
|
+
raise ToolError, e.message
|
22
|
+
end
|
23
|
+
|
16
24
|
def to_s
|
17
25
|
"#{name} - #{description}"
|
18
26
|
end
|
data/lib/regent/toolchain.rb
CHANGED
@@ -12,8 +12,23 @@ module Regent
|
|
12
12
|
tools.find { |tool| tool.name.downcase == name.downcase }
|
13
13
|
end
|
14
14
|
|
15
|
+
def add(tool, context)
|
16
|
+
@tools << Regent::Tool.new(name: tool[:name].to_s, description: tool[:description]).instance_eval do
|
17
|
+
raise "A tool method '#{tool[:name]}' is missing in the #{context.class.name}" unless context.respond_to?(tool[:name])
|
18
|
+
|
19
|
+
define_singleton_method(:call){ |*args| context.send(tool[:name], *args) }
|
20
|
+
self
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
15
24
|
def to_s
|
16
25
|
tools.map(&:to_s).join("\n")
|
17
26
|
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def tool_missing_error(tool_name, context_name)
|
31
|
+
"A tool method '#{tool_name}' is missing in the #{context_name}"
|
32
|
+
end
|
18
33
|
end
|
19
34
|
end
|
data/lib/regent/version.rb
CHANGED
data/lib/regent.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: regent
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alex Chaplinsky
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-02-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: zeitwerk
|
@@ -71,13 +71,17 @@ files:
|
|
71
71
|
- lib/regent/concerns/dependable.rb
|
72
72
|
- lib/regent/concerns/durationable.rb
|
73
73
|
- lib/regent/concerns/identifiable.rb
|
74
|
+
- lib/regent/concerns/toolable.rb
|
75
|
+
- lib/regent/engine/base.rb
|
74
76
|
- lib/regent/engine/react.rb
|
75
77
|
- lib/regent/engine/react/prompt_template.rb
|
76
78
|
- lib/regent/llm.rb
|
77
79
|
- lib/regent/llm/anthropic.rb
|
78
80
|
- lib/regent/llm/base.rb
|
79
81
|
- lib/regent/llm/gemini.rb
|
82
|
+
- lib/regent/llm/ollama.rb
|
80
83
|
- lib/regent/llm/open_ai.rb
|
84
|
+
- lib/regent/llm/open_router.rb
|
81
85
|
- lib/regent/logger.rb
|
82
86
|
- lib/regent/session.rb
|
83
87
|
- lib/regent/span.rb
|