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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a728784fd3d7720bb7fd96bf2cd22a8d53e59eaed01c9b4240a2cb09023e261
4
- data.tar.gz: 78ddbe87cf964ca94039f560ddfcb743a14757e551af0847363b0fa82228e4a7
3
+ metadata.gz: a1c3bc1771d81f732f941e27dfc393ef56b6f6c42d6c448385202f065dfc5811
4
+ data.tar.gz: 8950e15664e538beffc54d5e0e92da76bc8390e7ddd20bc923914ad14d3613e3
5
5
  SHA512:
6
- metadata.gz: c0306c99637469cff9e51a9b1b50424e646f38dd601dcb5b36d8eee417e2f280488b3e6c78747c242d78d8213fd6a9e004d3cd7065b692d7dc63a5900bf20e16
7
- data.tar.gz: 68302a81b5062f54415a89e49513e2186afd0fb6d129dfa1111b33565ade932cbae9e657218d66b37cf2fd58b78103456da0dcc7c52c26948016134117cc4e42
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
- llm = Regent::LLM.new("gpt-4o")
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
- llm: llm,
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
- result = agent.execute("What's the weather like in Tokyo?") # => "It is currently 72°F and sunny in Tokyo."
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, llm:, tools: [], **options)
9
+ def initialize(context, model:, tools: [], engine: Regent::Engine::React, **options)
10
10
  super()
11
11
 
12
12
  @context = context
13
- @llm = llm
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, :llm, :tools
20
+ attr_reader :context, :sessions, :model, :tools
20
21
 
21
- def execute(task)
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
- react.reason(task)
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 react
51
- Regent::Engine::React.new(context, llm, tools, session, @max_iterations)
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, options[:model])
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 warn_and_exit(name, model)
67
- warn "\n\e[33mIn order to use \e[33;1m#{model}\e[0m\e[33m model you need to install \e[33;1m#{name}\e[0m\e[33m gem. Please add \e[33;1mgem \"#{name}\"\e[0m\e[33m to your Gemfile.\e[0m"
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 - is always present after an Action.
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 | London
24
+ Action: {"tool": "weather_tool", "args": ["London"]}
25
25
  PAUSE
26
26
 
27
27
  You will have a response form a user with Observation:
@@ -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
- initialize_session(task)
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
- max_iterations.times do |i|
27
- content = get_llm_response
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
- tool, argument = parse_action(content)
25
+ tool_name, arguments = parse_tool_signature(content)
26
+ tool = find_tool(tool_name)
33
27
  return unless tool
34
-
35
- process_tool_execution(tool, argument)
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 success_answer(content)
86
- session.exec(Span::Type::ANSWER, top_level: true,type: :success, message: content, duration: session.duration.round(2)) { content }
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
- action = content.split(SEQUENCES[:action])[1]&.strip
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
- [tool_name, argument]
117
- rescue
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
@@ -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: options[:model],
16
- stop_sequences: args[:stop] ? args[:stop] : nil,
15
+ model: model,
16
+ temperature: args[:temperature] || 0.0,
17
+ stop_sequences: args[:stop] || [],
17
18
  max_tokens: MAX_TOKENS
18
19
  })
19
- format_response(response)
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
@@ -2,55 +2,36 @@
2
2
 
3
3
  module Regent
4
4
  class LLM
5
- class Response
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 invoke(messages, **args)
35
- provider.chat(messages: format_messages(messages), **args)
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 :options, :dependency
24
+ attr_reader :model, :api_key, :options
41
25
 
42
- def format_response(response)
43
- Response.new(
44
- content: response.chat_completion,
45
- model: options[:model],
46
- usage: Usage.new(input_tokens: response.prompt_tokens, output_tokens: response.completion_tokens)
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."
@@ -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({ contents: format_messages(messages) })
12
- format_response(response)
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: 'generative-language-api', api_key: api_key },
20
- options: { model: 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
@@ -10,10 +10,17 @@ module Regent
10
10
  def invoke(messages, **args)
11
11
  response = client.chat(parameters: {
12
12
  messages: messages,
13
- model: options[:model],
14
- stop: args[:stop]
13
+ model: model,
14
+ temperature: args[:temperature] || 0.0,
15
+ stop: args[:stop] || []
15
16
  })
16
- format_response(response)
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("[")}#{green(":spinner")}#{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
- rescue StandardError => e
46
+
47
+ rescue StandardError, ToolError => e
47
48
  logger.error(label: type, message: e.message, **arguments)
48
49
  raise
49
50
  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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Regent
4
- VERSION = "0.3.1"
4
+ VERSION = "0.3.2"
5
5
  end
data/lib/regent.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'securerandom'
4
+ require 'faraday'
5
+ require 'json'
4
6
  require 'pastel'
5
7
  require 'tty-spinner'
6
8
  require 'zeitwerk'
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.1
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: 2024-12-29 00:00:00.000000000 Z
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