regent 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +51 -11
- data/lib/regent/agent.rb +53 -0
- data/lib/regent/concerns/durationable.rb +12 -0
- data/lib/regent/concerns/identifiable.rb +24 -0
- data/lib/regent/engine/react/prompt_template.rb +36 -0
- data/lib/regent/engine/react.rb +122 -0
- data/lib/regent/logger.rb +63 -0
- data/lib/regent/session.rb +86 -0
- data/lib/regent/span.rb +92 -0
- data/lib/regent/tool.rb +20 -0
- data/lib/regent/toolchain.rb +19 -0
- data/lib/regent/version.rb +1 -1
- data/lib/regent.rb +7 -1
- metadata +83 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2a6ab1feb1aafc91509881b72df5b6e7212a9588345ae39576afee47bfb1c614
|
4
|
+
data.tar.gz: 8a4275e5a82653c736539ef8c5650e1a7e11ac1c231be7561470fafd9943e54c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5dbaa9f8c0ed4d6ded4e164436062c2a919c2fe42a81da97d993785f6298963a34241cdf01900fb7d3a24eee1bd2f23844cab1764c1e1507d455793a4e85a60b
|
7
|
+
data.tar.gz: ad7d51c9fbd2c6751974d4cf8591f59c673b3ddf420cf6ce22d7fd60e4f62d1b7e028cc5647c3ac13867bf4f5a4c3ea0a275c9f34c1c9bd13ec62c09a3cc9854
|
data/README.md
CHANGED
@@ -1,24 +1,64 @@
|
|
1
|
+
|
2
|
+
![regent_light](https://github.com/user-attachments/assets/62564dac-b8d7-4dc0-9b63-64c6841b5872)
|
3
|
+
|
1
4
|
# Regent
|
5
|
+
**Regent** is library for building AI agents with Ruby.
|
6
|
+
|
7
|
+
> [!WARNING]
|
8
|
+
> 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.
|
2
9
|
|
3
|
-
|
10
|
+
## Showcase
|
11
|
+
A basic Regnt Agent extended with a `price_tool` that allows for retrieving cryptocurrency prices from coingecko.com.
|
4
12
|
|
5
|
-
|
13
|
+
![screencast 2024-12-25 21-53-47](https://github.com/user-attachments/assets/4e65b731-bbd7-4732-b157-b705d35a7824)
|
6
14
|
|
7
|
-
## Installation
|
8
15
|
|
9
|
-
|
16
|
+
## Install
|
10
17
|
|
11
|
-
|
18
|
+
```bash
|
19
|
+
gem install regent
|
20
|
+
```
|
12
21
|
|
13
|
-
|
22
|
+
or add regent to the Gemfile:
|
14
23
|
|
15
|
-
|
24
|
+
```ruby
|
25
|
+
gem 'regent'
|
26
|
+
```
|
16
27
|
|
17
|
-
|
28
|
+
and run
|
29
|
+
|
30
|
+
```bash
|
31
|
+
bundle install
|
32
|
+
```
|
18
33
|
|
19
34
|
## Usage
|
35
|
+
In order to operate an agent needs access to LLM (large language model). Regent relies on the [Langchainrb](https://github.com/patterns-ai-core/langchainrb) library to interact with LLMs. Let's create an instance of OapnAI LLM:
|
36
|
+
```ruby
|
37
|
+
llm = Langchain::LLM::OpenAI(api_key: ENV["OPENAI_KEY"])
|
38
|
+
```
|
39
|
+
|
40
|
+
Agents are effective when they have tools that enable them to get new information:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
class WeatherTool < Regent::Tool
|
44
|
+
def call(location)
|
45
|
+
# implementation of a call to weather API
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
weather_tool = WeatherTool.new(name: "weather_tool", description: "Get the weather in a given location")
|
50
|
+
```
|
51
|
+
|
52
|
+
Next, let's instantiate an agent passing LLM and a set of tools:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
agent = Regent::Agent.new(llm: llm, tools: [weather_tool])
|
56
|
+
```
|
20
57
|
|
21
|
-
|
58
|
+
Simply run an execute function, passing your query as an argument
|
59
|
+
``` ruby
|
60
|
+
agent.execute("What is the weather in London today?")
|
61
|
+
```
|
22
62
|
|
23
63
|
## Development
|
24
64
|
|
@@ -28,8 +68,8 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
28
68
|
|
29
69
|
## Contributing
|
30
70
|
|
31
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
71
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/alchaplinsky/regent. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/alchaplinsky/regent/blob/main/CODE_OF_CONDUCT.md).
|
32
72
|
|
33
73
|
## Code of Conduct
|
34
74
|
|
35
|
-
Everyone interacting in the Regent project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/
|
75
|
+
Everyone interacting in the Regent project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/alchaplinsky/regent/blob/main/CODE_OF_CONDUCT.md).
|
data/lib/regent/agent.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Regent
|
4
|
+
class Agent
|
5
|
+
include Concerns::Identifiable
|
6
|
+
|
7
|
+
DEFAULT_MAX_ITERATIONS = 10
|
8
|
+
|
9
|
+
def initialize(llm:, tools: [], **options)
|
10
|
+
super()
|
11
|
+
|
12
|
+
@llm = llm
|
13
|
+
@sessions = []
|
14
|
+
@tools = tools.is_a?(Toolchain) ? tools : Toolchain.new(Array(tools))
|
15
|
+
@max_iterations = options[:max_iterations] || DEFAULT_MAX_ITERATIONS
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :sessions, :llm, :tools
|
19
|
+
|
20
|
+
def execute(task)
|
21
|
+
raise ArgumentError, "Task cannot be empty" if task.to_s.strip.empty?
|
22
|
+
|
23
|
+
start_session
|
24
|
+
react.reason(task)
|
25
|
+
ensure
|
26
|
+
complete_session
|
27
|
+
end
|
28
|
+
|
29
|
+
def running?
|
30
|
+
session&.active? || false
|
31
|
+
end
|
32
|
+
|
33
|
+
def session
|
34
|
+
@sessions.last
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def start_session
|
40
|
+
complete_session
|
41
|
+
@sessions << Session.new
|
42
|
+
session.start
|
43
|
+
end
|
44
|
+
|
45
|
+
def complete_session
|
46
|
+
session&.complete if running?
|
47
|
+
end
|
48
|
+
|
49
|
+
def react
|
50
|
+
Regent::Engine::React.new(llm, tools, session, @max_iterations)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Regent
|
4
|
+
module Concerns
|
5
|
+
module Identifiable
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
attr_reader :id
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def generate_id
|
15
|
+
@id = SecureRandom.uuid
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(*)
|
19
|
+
generate_id
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Regent
|
4
|
+
module Engine
|
5
|
+
class React
|
6
|
+
module PromptTemplate
|
7
|
+
def self.system_prompt(tool_names)
|
8
|
+
<<~PROMPT
|
9
|
+
You are assisstant reasoning step-by-step to solve complex problems.
|
10
|
+
Your reasoning process happens in a loop of Though, Action, Observation.
|
11
|
+
Thought - a description of your thoughts about the question.
|
12
|
+
Action - pick a an action from available tools. If there are no tools that can help return an Answer saying you are not able to help..
|
13
|
+
Observation - is the result of running a tool.
|
14
|
+
|
15
|
+
## Available tools:
|
16
|
+
#{tool_names}
|
17
|
+
|
18
|
+
## Example session
|
19
|
+
Question: What is the weather in London today?
|
20
|
+
Thought: I need to get the wether in London
|
21
|
+
Action: weather_tool | "London"
|
22
|
+
PAUSE
|
23
|
+
|
24
|
+
You will have a response with Observation:
|
25
|
+
Observation: It is 32 degress and Sunny
|
26
|
+
|
27
|
+
... (this Thought/Action/Observation can repeat N times)
|
28
|
+
|
29
|
+
Thought: I know the final answer
|
30
|
+
Answer: It is 32 degress and Sunny in London
|
31
|
+
PROMPT
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Regent
|
4
|
+
module Engine
|
5
|
+
class React
|
6
|
+
SEQUENCES = {
|
7
|
+
answer: "Answer:",
|
8
|
+
action: "Action:",
|
9
|
+
observation: "Observation:",
|
10
|
+
stop: "PAUSE"
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
def initialize(llm, toolchain, session, max_iterations)
|
14
|
+
@llm = llm
|
15
|
+
@toolchain = toolchain
|
16
|
+
@session = session
|
17
|
+
@max_iterations = max_iterations
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :llm, :toolchain, :session, :max_iterations
|
21
|
+
|
22
|
+
def reason(task)
|
23
|
+
initialize_session(task)
|
24
|
+
|
25
|
+
max_iterations.times do |i|
|
26
|
+
content = get_llm_response
|
27
|
+
session.add_message({role: :assistant, content: content })
|
28
|
+
return extract_answer(content) if answer_present?(content)
|
29
|
+
|
30
|
+
if action_present?(content)
|
31
|
+
tool, argument = parse_action(content)
|
32
|
+
return unless tool
|
33
|
+
|
34
|
+
process_tool_execution(tool, argument)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
error_answer("Max iterations reached without finding an answer.")
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def initialize_session(task)
|
44
|
+
session.add_message({role: :system, content: Regent::Engine::React::PromptTemplate.system_prompt(toolchain.to_s)})
|
45
|
+
session.add_message({role: :user, content: task})
|
46
|
+
session.exec(Span::Type::INPUT, message: task) { task }
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_llm_response
|
50
|
+
session.exec(Span::Type::LLM_CALL, type: llm.defaults[:chat_model], message: session.messages.last[:content]) do
|
51
|
+
result = llm.chat(messages: session.messages, params: { stop: [SEQUENCES[:stop]] })
|
52
|
+
|
53
|
+
# Relying on Langchain Response interface to get token counts and chat completion
|
54
|
+
session.current_span.set_meta("#{result.prompt_tokens} → #{result.completion_tokens} tokens")
|
55
|
+
result.chat_completion
|
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
|
+
def answer_present?(content)
|
78
|
+
content.include?(SEQUENCES[:answer])
|
79
|
+
end
|
80
|
+
|
81
|
+
def action_present?(content)
|
82
|
+
content.include?(SEQUENCES[:action])
|
83
|
+
end
|
84
|
+
|
85
|
+
def success_answer(content)
|
86
|
+
session.exec(Span::Type::ANSWER, type: :success, message: content, duration: session.duration.round(2)) { content }
|
87
|
+
end
|
88
|
+
|
89
|
+
def error_answer(content)
|
90
|
+
session.exec(Span::Type::ANSWER, 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]
|
103
|
+
end
|
104
|
+
|
105
|
+
def parse_tool_signature(content)
|
106
|
+
action = content.split(SEQUENCES[:action])[1]&.strip
|
107
|
+
return [nil, nil] unless action
|
108
|
+
|
109
|
+
parts = action.split('|', 2).map(&:strip)
|
110
|
+
tool_name = parts[0]
|
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?
|
115
|
+
|
116
|
+
[tool_name, argument]
|
117
|
+
rescue
|
118
|
+
[nil, nil]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Regent
|
4
|
+
class Logger
|
5
|
+
COLORS = %i[dim green yellow red blue cyan clear].freeze
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@pastel = Pastel.new
|
9
|
+
@spinner = build_spinner(spinner_symbol)
|
10
|
+
@nested_spinner = build_spinner("#{dim(" ├──")}#{spinner_symbol}")
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :spinner, :nested_spinner
|
14
|
+
|
15
|
+
def info(label:, message:, duration: nil, type: nil, meta: nil, top_level: false)
|
16
|
+
current_spinner = top_level ? spinner : nested_spinner
|
17
|
+
|
18
|
+
current_spinner.update(title: format_message(label, message, duration, type, meta))
|
19
|
+
current_spinner
|
20
|
+
end
|
21
|
+
|
22
|
+
def start(**args)
|
23
|
+
info(**args).auto_spin
|
24
|
+
end
|
25
|
+
|
26
|
+
def success(**args)
|
27
|
+
info(**args).success
|
28
|
+
end
|
29
|
+
|
30
|
+
def error(**args)
|
31
|
+
info(**args).error
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def format_message(label, message, duration, type, meta)
|
37
|
+
parts = []
|
38
|
+
parts << "#{dim("[")}#{cyan(label)}"
|
39
|
+
parts << "#{dim(" ❯")} #{yellow(type)}" if type
|
40
|
+
parts << dim("]")
|
41
|
+
parts << dim("[#{meta}]") if meta
|
42
|
+
parts << dim("[#{duration.round(2)}s]") if duration
|
43
|
+
parts << dim(":")
|
44
|
+
parts << clear(" #{message}")
|
45
|
+
|
46
|
+
parts.join
|
47
|
+
end
|
48
|
+
|
49
|
+
def spinner_symbol
|
50
|
+
"#{dim("[")}#{green(":spinner")}#{dim("]")}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def build_spinner(spinner_format)
|
54
|
+
TTY::Spinner.new("#{spinner_format} :title", format: :dots)
|
55
|
+
end
|
56
|
+
|
57
|
+
COLORS.each do |color|
|
58
|
+
define_method(color) do |message|
|
59
|
+
@pastel.send(color, message)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Regent
|
4
|
+
class Session
|
5
|
+
include Concerns::Identifiable
|
6
|
+
include Concerns::Durationable
|
7
|
+
|
8
|
+
class SessionError < StandardError; end
|
9
|
+
class InactiveSessionError < SessionError; end
|
10
|
+
class AlreadyStartedError < SessionError; end
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
super()
|
14
|
+
|
15
|
+
@spans = []
|
16
|
+
@messages = []
|
17
|
+
@start_time = nil
|
18
|
+
@end_time = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :id, :spans, :messages, :start_time, :end_time
|
22
|
+
|
23
|
+
# Starts the session
|
24
|
+
# @raise [AlreadyStartedError] if session is already started
|
25
|
+
# @return [void]
|
26
|
+
def start
|
27
|
+
raise AlreadyStartedError, "Session already started" if @start_time
|
28
|
+
|
29
|
+
@start_time = Time.now.freeze
|
30
|
+
end
|
31
|
+
|
32
|
+
# Executes a new span in the session
|
33
|
+
# @param type [Symbol, String] The type of span
|
34
|
+
# @param options [Hash] Options for the span
|
35
|
+
# @raise [InactiveSessionError] if session is not active
|
36
|
+
# @return [String] The output of the span
|
37
|
+
def exec(type, options = {}, &block)
|
38
|
+
raise InactiveSessionError, "Cannot execute span in inactive session" unless active?
|
39
|
+
|
40
|
+
@spans << Span.new(type: type, arguments: options)
|
41
|
+
current_span.run(&block)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Replays the session
|
45
|
+
# @return [String] The result of the session
|
46
|
+
def replay
|
47
|
+
spans.each { |span| span.replay }
|
48
|
+
result
|
49
|
+
end
|
50
|
+
|
51
|
+
# Completes the session and returns the result
|
52
|
+
# @return [Object] The result of the last span
|
53
|
+
# @raise [InactiveSessionError] if session is not active
|
54
|
+
# @return [String] The result of the last span
|
55
|
+
def complete
|
56
|
+
raise InactiveSessionError, "Cannot complete inactive session" unless active?
|
57
|
+
|
58
|
+
@end_time = Time.now.freeze
|
59
|
+
result
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [Span, nil] The current span or nil if no spans exist
|
63
|
+
def current_span
|
64
|
+
@spans.last
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [String, nil] The output of the current span or nil if no spans exist
|
68
|
+
def result
|
69
|
+
current_span&.output
|
70
|
+
end
|
71
|
+
|
72
|
+
# @return [Boolean] Whether the session is currently active
|
73
|
+
def active?
|
74
|
+
start_time && end_time.nil?
|
75
|
+
end
|
76
|
+
|
77
|
+
# Adds a message to the session
|
78
|
+
# @param message [String] The message to add
|
79
|
+
# @raise [ArgumentError] if message is nil or empty
|
80
|
+
def add_message(message)
|
81
|
+
raise ArgumentError, "Message cannot be nil or empty" if message.nil? || message.empty?
|
82
|
+
|
83
|
+
@messages << message
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/regent/span.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Regent
|
4
|
+
class Span
|
5
|
+
include Concerns::Identifiable
|
6
|
+
include Concerns::Durationable
|
7
|
+
|
8
|
+
module Type
|
9
|
+
INPUT = 'INPUT'.freeze
|
10
|
+
LLM_CALL = 'LLM'.freeze
|
11
|
+
TOOL_EXECUTION = 'TOOL'.freeze
|
12
|
+
MEMORY_ACCESS = 'MEMO'.freeze
|
13
|
+
ANSWER = 'ANSWER'.freeze
|
14
|
+
|
15
|
+
def self.all
|
16
|
+
constants.map { |c| const_get(c) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.valid?(type)
|
20
|
+
all.include?(type)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param type [String] The type of span (must be one of Type.all)
|
25
|
+
# @param arguments [Hash] Arguments for the span
|
26
|
+
# @param logger [Logger] Logger instance
|
27
|
+
def initialize(type:, arguments:, logger: Logger.new)
|
28
|
+
super()
|
29
|
+
|
30
|
+
validate_type!(type)
|
31
|
+
|
32
|
+
@logger = logger
|
33
|
+
@type = type
|
34
|
+
@arguments = arguments
|
35
|
+
@meta = nil
|
36
|
+
@output = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :name, :arguments, :output, :type, :start_time, :end_time
|
40
|
+
|
41
|
+
# @raise [ArgumentError] if block is not given
|
42
|
+
# @return [String] The output of the span
|
43
|
+
def run
|
44
|
+
@output = log_operation do
|
45
|
+
yield
|
46
|
+
rescue StandardError => e
|
47
|
+
logger.error(label: type, message: e.message, **arguments)
|
48
|
+
raise
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [String] The output of the span
|
53
|
+
def replay
|
54
|
+
log_operation(live: false) { @output }
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Boolean] Whether the span is currently running
|
58
|
+
def running?
|
59
|
+
@start_time && @end_time.nil?
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [Boolean] Whether the span is completed
|
63
|
+
def completed?
|
64
|
+
@start_time && @end_time
|
65
|
+
end
|
66
|
+
|
67
|
+
# @param value [String] The meta value to set
|
68
|
+
def set_meta(value)
|
69
|
+
@meta = value.freeze
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
attr_reader :logger, :meta
|
75
|
+
|
76
|
+
def validate_type!(type)
|
77
|
+
raise InvalidSpanType, "Invalid span type: #{type}" unless Type.valid?(type)
|
78
|
+
end
|
79
|
+
|
80
|
+
def log_operation(live: true, &block)
|
81
|
+
@start_time = live ? Time.now.freeze : @start_time
|
82
|
+
logger.start(label: type, **arguments)
|
83
|
+
|
84
|
+
result = yield
|
85
|
+
|
86
|
+
@end_time = live ? Time.now.freeze : @end_time
|
87
|
+
logger.success(label: type, **({ duration: duration.round(2), meta: meta }.merge(arguments)))
|
88
|
+
|
89
|
+
result
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/regent/tool.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Regent
|
4
|
+
class Tool
|
5
|
+
def initialize(name:, description:)
|
6
|
+
@name = name
|
7
|
+
@description = description
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :name, :description
|
11
|
+
|
12
|
+
def call(argument)
|
13
|
+
raise NotImplementedError, "Tool #{name} has not implemented the execute method"
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
"#{name} - #{description}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Regent
|
4
|
+
class Toolchain
|
5
|
+
def initialize(tools)
|
6
|
+
@tools = tools
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :tools
|
10
|
+
|
11
|
+
def find(name)
|
12
|
+
tools.find { |tool| tool.name.downcase == name.downcase }
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
tools.map(&:to_s).join("\n")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/regent/version.rb
CHANGED
data/lib/regent.rb
CHANGED
@@ -1,8 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require 'securerandom'
|
4
|
+
require 'pastel'
|
5
|
+
require 'tty-spinner'
|
6
|
+
require 'zeitwerk'
|
4
7
|
|
5
8
|
module Regent
|
6
9
|
class Error < StandardError; end
|
7
10
|
# Your code goes here...
|
11
|
+
|
12
|
+
loader = Zeitwerk::Loader.for_gem
|
13
|
+
loader.setup
|
8
14
|
end
|
metadata
CHANGED
@@ -1,15 +1,85 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: regent
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alex Chaplinsky
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
12
|
-
dependencies:
|
11
|
+
date: 2024-12-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: zeitwerk
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.7'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: ruby-openai
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 7.3.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 7.3.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: langchainrb
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.19.2
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.19.2
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: tty-spinner
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.9.3
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.9.3
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pastel
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.8.0
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.8.0
|
13
83
|
description: Write a longer description or delete this line.
|
14
84
|
email:
|
15
85
|
- alchaplinsky@gmail.com
|
@@ -24,6 +94,16 @@ files:
|
|
24
94
|
- README.md
|
25
95
|
- Rakefile
|
26
96
|
- lib/regent.rb
|
97
|
+
- lib/regent/agent.rb
|
98
|
+
- lib/regent/concerns/durationable.rb
|
99
|
+
- lib/regent/concerns/identifiable.rb
|
100
|
+
- lib/regent/engine/react.rb
|
101
|
+
- lib/regent/engine/react/prompt_template.rb
|
102
|
+
- lib/regent/logger.rb
|
103
|
+
- lib/regent/session.rb
|
104
|
+
- lib/regent/span.rb
|
105
|
+
- lib/regent/tool.rb
|
106
|
+
- lib/regent/toolchain.rb
|
27
107
|
- lib/regent/version.rb
|
28
108
|
- sig/regent.rbs
|
29
109
|
homepage: https://github.com/alchaplinsky/regent
|