regent 0.3.1 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +5 -3
- data/lib/regent/agent.rb +12 -7
- data/lib/regent/concerns/dependable.rb +5 -4
- 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 +10 -14
- data/lib/regent/llm/base.rb +13 -32
- data/lib/regent/llm/gemini.rb +21 -15
- data/lib/regent/llm/open_ai.rb +10 -14
- data/lib/regent/llm.rb +24 -2
- data/lib/regent/logger.rb +9 -3
- data/lib/regent/span.rb +2 -1
- data/lib/regent/tool.rb +8 -0
- data/lib/regent/version.rb +1 -1
- data/lib/regent.rb +2 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a1c3bc1771d81f732f941e27dfc393ef56b6f6c42d6c448385202f065dfc5811
|
4
|
+
data.tar.gz: 8950e15664e538beffc54d5e0e92da76bc8390e7ddd20bc923914ad14d3613e3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3d6a447a3df128617b5ede55412752a331f6edcb556039ea09472357b258803889722331408baf1e7b8add8111c5addbf58016a6799b3d2d98d608a88b501e39
|
7
|
+
data.tar.gz: f496a6abdd8646342bb0d2fd34b59893d9770e74d7325d1fdf79d3d0d33751765b3dfd17a2dacf4636fd045edbc0c9661fadb143df24be206a281e6b7b2a356e
|
data/README.md
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
<div align="center">
|
4
4
|
|
5
5
|
# Regent
|
6
|
+
|
6
7
|
[![Gem Version](https://badge.fury.io/rb/regent.svg)](https://badge.fury.io/rb/regent)
|
7
8
|
[![Build](https://github.com/alchaplinsky/regent/actions/workflows/main.yml/badge.svg)](https://github.com/alchaplinsky/regent/actions/workflows/main.yml)
|
8
9
|
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
|
@@ -55,7 +56,7 @@ Create your first agent:
|
|
55
56
|
|
56
57
|
```ruby
|
57
58
|
# Initialize the LLM
|
58
|
-
|
59
|
+
model = Regent::LLM.new("gpt-4o")
|
59
60
|
|
60
61
|
# Create a custom tool
|
61
62
|
class WeatherTool < Regent::Tool
|
@@ -68,7 +69,7 @@ end
|
|
68
69
|
# Create and configure the agent
|
69
70
|
agent = Regent::Agent.new(
|
70
71
|
"You are a helpful weather assistant",
|
71
|
-
|
72
|
+
model: model,
|
72
73
|
tools: [WeatherTool.new(
|
73
74
|
name: "weather_tool",
|
74
75
|
description: "Get current weather for a location"
|
@@ -76,10 +77,11 @@ agent = Regent::Agent.new(
|
|
76
77
|
)
|
77
78
|
|
78
79
|
# Execute a query
|
79
|
-
|
80
|
+
agent.run("What's the weather like in Tokyo?") # => "It is currently 72°F and sunny in Tokyo."
|
80
81
|
```
|
81
82
|
|
82
83
|
## Why Regent?
|
84
|
+
|
83
85
|
- **Transparent Decision Making**: Watch your agent's thought process as it reasons through problems
|
84
86
|
- **Flexible Architecture**: Easy to extend with custom tools and adapt to different use cases
|
85
87
|
- **Production Ready**: Built with tracing, error handling, and clean abstractions
|
data/lib/regent/agent.rb
CHANGED
@@ -6,23 +6,24 @@ module Regent
|
|
6
6
|
|
7
7
|
DEFAULT_MAX_ITERATIONS = 10
|
8
8
|
|
9
|
-
def initialize(context,
|
9
|
+
def initialize(context, model:, tools: [], engine: Regent::Engine::React, **options)
|
10
10
|
super()
|
11
11
|
|
12
12
|
@context = context
|
13
|
-
@
|
13
|
+
@model = model
|
14
|
+
@engine = engine
|
14
15
|
@sessions = []
|
15
16
|
@tools = tools.is_a?(Toolchain) ? tools : Toolchain.new(Array(tools))
|
16
17
|
@max_iterations = options[:max_iterations] || DEFAULT_MAX_ITERATIONS
|
17
18
|
end
|
18
19
|
|
19
|
-
attr_reader :context, :sessions, :
|
20
|
+
attr_reader :context, :sessions, :model, :tools
|
20
21
|
|
21
|
-
def
|
22
|
+
def run(task)
|
22
23
|
raise ArgumentError, "Task cannot be empty" if task.to_s.strip.empty?
|
23
24
|
|
24
25
|
start_session
|
25
|
-
|
26
|
+
reason(task)
|
26
27
|
ensure
|
27
28
|
complete_session
|
28
29
|
end
|
@@ -37,6 +38,10 @@ module Regent
|
|
37
38
|
|
38
39
|
private
|
39
40
|
|
41
|
+
def reason(task)
|
42
|
+
engine.reason(task)
|
43
|
+
end
|
44
|
+
|
40
45
|
def start_session
|
41
46
|
complete_session
|
42
47
|
@sessions << Session.new
|
@@ -47,8 +52,8 @@ module Regent
|
|
47
52
|
session&.complete if running?
|
48
53
|
end
|
49
54
|
|
50
|
-
def
|
51
|
-
|
55
|
+
def engine
|
56
|
+
@engine.new(context, model, tools, session, @max_iterations)
|
52
57
|
end
|
53
58
|
end
|
54
59
|
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,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
@@ -12,11 +12,18 @@ module Regent
|
|
12
12
|
response = client.messages(parameters: {
|
13
13
|
messages: format_messages(messages),
|
14
14
|
system: system_instruction(messages),
|
15
|
-
model:
|
16
|
-
|
15
|
+
model: model,
|
16
|
+
temperature: args[:temperature] || 0.0,
|
17
|
+
stop_sequences: args[:stop] || [],
|
17
18
|
max_tokens: MAX_TOKENS
|
18
19
|
})
|
19
|
-
|
20
|
+
|
21
|
+
result(
|
22
|
+
model: model,
|
23
|
+
content: response.dig("content", 0, "text"),
|
24
|
+
input_tokens: response.dig("usage", "input_tokens"),
|
25
|
+
output_tokens: response.dig("usage", "output_tokens")
|
26
|
+
)
|
20
27
|
end
|
21
28
|
|
22
29
|
private
|
@@ -32,17 +39,6 @@ module Regent
|
|
32
39
|
def format_messages(messages)
|
33
40
|
messages.reject { |message| message[:role].to_s == "system" }
|
34
41
|
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
42
|
end
|
47
43
|
end
|
48
44
|
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
|
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
|
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,9 +11,11 @@ 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)
|
16
|
+
def initialize(model, strict_mode: true, **options)
|
15
17
|
@model = model
|
18
|
+
@strict_mode = strict_mode
|
16
19
|
@options = options
|
17
20
|
instantiate_provider
|
18
21
|
end
|
@@ -20,12 +23,20 @@ module Regent
|
|
20
23
|
attr_reader :model, :options
|
21
24
|
|
22
25
|
def invoke(messages, **args)
|
26
|
+
retries = 0
|
23
27
|
provider.invoke(messages, **args)
|
28
|
+
|
29
|
+
rescue Faraday::Error => error
|
30
|
+
if error.respond_to?(:retryable?) && error.retryable? && retries < DEFAULT_RETRY_COUNT
|
31
|
+
sleep(exponential_backoff(retries))
|
32
|
+
retry
|
33
|
+
end
|
34
|
+
handle_error(error)
|
24
35
|
end
|
25
36
|
|
26
37
|
private
|
27
38
|
|
28
|
-
attr_reader :provider
|
39
|
+
attr_reader :provider, :strict_mode
|
29
40
|
|
30
41
|
def instantiate_provider
|
31
42
|
provider_class = find_provider_class
|
@@ -41,5 +52,16 @@ module Regent
|
|
41
52
|
def create_provider(provider_class)
|
42
53
|
Regent::LLM.const_get(provider_class).new(**options.merge(model: model))
|
43
54
|
end
|
55
|
+
|
56
|
+
def handle_error(error)
|
57
|
+
message = provider.parse_error(error) || error.message
|
58
|
+
raise ApiError, message if strict_mode
|
59
|
+
Result.new(model: model, content: message, input_tokens: nil, output_tokens: nil)
|
60
|
+
end
|
61
|
+
|
62
|
+
def exponential_backoff(retry_count)
|
63
|
+
# Exponential backoff with jitter: 2^n * 100ms + random jitter
|
64
|
+
(2**retry_count * 0.1) + rand(0.1)
|
65
|
+
end
|
44
66
|
end
|
45
67
|
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
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/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.2
|
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-01-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: zeitwerk
|
@@ -71,6 +71,7 @@ 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/engine/base.rb
|
74
75
|
- lib/regent/engine/react.rb
|
75
76
|
- lib/regent/engine/react/prompt_template.rb
|
76
77
|
- lib/regent/llm.rb
|