elelem 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ Elelem.configure do |config|
4
+ config.setup(:provider) do |session|
5
+ session.commands.register("provider", description: "Switch provider: /provider NAME [MODEL]", completions: lambda {
6
+ Elelem::Config.names
7
+ }) do |args|
8
+ name, model = args.to_s.split
9
+ next session.output.say(" -> available: #{Elelem::Config.names.join(", ")}") if name.nil?
10
+
11
+ # --model carries over only to the provider it was given for.
12
+ options = if model then { model: }
13
+ elsif name == session.provider_name then session.provider_options
14
+ else {}
15
+ end
16
+ session.provider = Elelem::Config.build_provider(name, **options)
17
+ session.provider_name = name
18
+ session.provider_options = options
19
+ session.output.say " -> switched to #{name}"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ Elelem.configure do |config|
4
+ config.setup(:read) do |agent|
5
+ agent.toolbox.add("read",
6
+ description: "Read file",
7
+ params: { path: { type: "string" } },
8
+ required: ["path"],
9
+ aliases: ["open"]) do |a|
10
+ path = Pathname.new(a["path"]).expand_path
11
+ next { error: "not found" } unless path.exist?
12
+
13
+ content = path.binread
14
+ next { error: "binary file (#{content.bytesize} bytes)" } if content.include?("\0")
15
+
16
+ content = content.force_encoding(Encoding::UTF_8).scrub
17
+
18
+ { content: Elelem::Builtins.truncate(content), path: a["path"] }
19
+ end
20
+
21
+ agent.toolbox.after("read") do |_, result|
22
+ if result[:error]
23
+ agent.output.say " ! #{result[:error]}"
24
+ else
25
+ agent.output.display_file(result[:path], fallback: result[:content])
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ Elelem.configure do |config|
4
+ config.setup(:reload) do |agent|
5
+ agent.commands.register("reload", description: "Reload plugin code from disk") do
6
+ Elelem::Config.reload(agent)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The default output. A gem that registers its own output first keeps it.
4
+ Elelem.configure do |config|
5
+ config.output { Elelem::Builtins::Terminal.new } unless config.factories.key?(:output)
6
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ Elelem.configure do |config|
4
+ config.setup(:tools) do |agent|
5
+ describe = lambda do |tool|
6
+ parameters = tool.to_h.dig(:function, :parameters)
7
+ lines = ["## #{tool.name}", "", tool.description, "", "### Parameters", ""]
8
+ parameters[:properties].each do |param, spec|
9
+ req = parameters[:required].include?(param.to_s) ? ", required" : ""
10
+ desc = spec[:description] ? " - #{spec[:description]}" : ""
11
+ lines << "- `#{param}` (#{spec[:type]}#{req})#{desc}"
12
+ end
13
+ lines << "" << "*aliases: #{tool.aliases.join(", ")}*" if tool.aliases.any?
14
+ lines.join("\n")
15
+ end
16
+
17
+ completions = -> { agent.toolbox.tools.keys }
18
+ agent.commands.register("tools", description: "List available tools", completions: completions) do |arg|
19
+ tool = agent.toolbox.tools[arg] if arg && !arg.empty?
20
+ if tool
21
+ agent.output.say describe.call(tool), as: :markdown
22
+ elsif arg && !arg.empty?
23
+ agent.output.say "Unknown tool: #{arg}"
24
+ else
25
+ # values copies the list, so a plugin adding tools from a thread cannot break this loop.
26
+ rows = agent.toolbox.tools.values.map { |t| "| #{t.name} | #{t.description.lines.first.chomp} |" }
27
+ agent.output.say "| Tool | Description |\n|------|-------------|\n#{rows.join("\n")}", as: :markdown
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ Elelem.configure do |config|
4
+ config.setup(:write) do |agent|
5
+ agent.toolbox.add("write",
6
+ description: "Write file",
7
+ params: { path: { type: "string" }, content: { type: "string" } },
8
+ required: %w[path content],
9
+ aliases: ["write<|channel|>"]) do |a|
10
+ path = Pathname.new(a["path"]).expand_path
11
+ FileUtils.mkdir_p(path.dirname)
12
+ { bytes: path.write(a["content"]), path: a["path"] }
13
+ end
14
+
15
+ agent.toolbox.before("write") do |args|
16
+ path = Pathname.new(args["path"]).expand_path
17
+ next unless path.exist? && agent.input.interactive?
18
+
19
+ Tempfile.create(["elelem", File.extname(path)]) do |t|
20
+ t.write(args["content"])
21
+ t.flush
22
+ diff, = Open3.capture2("diff", "--color=always", "-u", path.to_s, t.path)
23
+ agent.output.print(diff)
24
+ end
25
+ rescue Errno::ENOENT
26
+ nil # no diff installed
27
+ end
28
+
29
+ agent.toolbox.after("write") do |_, result|
30
+ agent.output.display_file(result[:path], fallback: " -> #{result[:path]}")
31
+ # The model sees the checks in the write's own result.
32
+ if agent.toolbox.tools.key?("verify")
33
+ result[:verify] = agent.toolbox.run("verify", { "path" => result[:path] }).to_h
34
+ end
35
+ end
36
+ end
37
+ end
@@ -3,6 +3,7 @@
3
3
  module Elelem
4
4
  class Plugins
5
5
  LOAD_PATHS = [
6
+ File.expand_path("plugins", __dir__),
6
7
  "~/.agents/plugins",
7
8
  ".agents/plugins"
8
9
  ].freeze
@@ -15,10 +16,14 @@ module Elelem
15
16
  def load!(force: false)
16
17
  return if @loaded && !force
17
18
 
18
- @load_paths.each { |path| load_dir(File.expand_path(path)) }
19
+ dirs.each { |dir| load_dir(dir) }
19
20
  @loaded = true
20
21
  end
21
22
 
23
+ def dirs
24
+ @load_paths.map { |path| File.expand_path(path) }
25
+ end
26
+
22
27
  private
23
28
 
24
29
  def load_dir(dir)
@@ -30,7 +35,7 @@ module Elelem
30
35
  def load_plugin(file)
31
36
  load(file)
32
37
  rescue StandardError, ScriptError => e
33
- warn "elelem: failed to load plugin #{file}: #{e.message}"
38
+ $stderr.puts "elelem: failed to load plugin #{file}: #{e.message}"
34
39
  end
35
40
  end
36
41
  end
@@ -30,8 +30,13 @@ module Elelem
30
30
  self
31
31
  end
32
32
 
33
- def clear_setups!
34
- @setups.clear
33
+ # Drops setups defined in files under dirs. Gem setups are kept, since
34
+ # require will not run them again.
35
+ def clear_setups_from!(dirs)
36
+ @setups.reject! do |_, block|
37
+ file = block.source_location&.first.to_s
38
+ dirs.any? { |dir| file.start_with?("#{dir}/") }
39
+ end
35
40
  end
36
41
  end
37
42
  end
@@ -12,9 +12,9 @@ module Elelem
12
12
  self
13
13
  end
14
14
 
15
- def build_provider(name)
15
+ def build_provider(name, **)
16
16
  @plugins.load!
17
- @registration.providers.fetch(name.to_s) { require_gem(name) }.call
17
+ @registration.providers.fetch(name.to_s) { require_gem(name) }.call(**)
18
18
  end
19
19
 
20
20
  def apply(session)
@@ -23,7 +23,7 @@ module Elelem
23
23
  end
24
24
 
25
25
  def reload(session)
26
- @registration.clear_setups!
26
+ @registration.clear_setups_from!(@plugins.dirs)
27
27
  session.toolbox.clear!
28
28
  session.commands.clear!
29
29
  @plugins.load!(force: true)
@@ -34,10 +34,13 @@ module Elelem
34
34
  @registration.providers.keys
35
35
  end
36
36
 
37
- def build_session(provider, toolbox: Toolbox.new, input: nil, output: nil)
37
+ def build_session(provider, toolbox: Toolbox.new, input: nil, output: nil, provider_options: {},
38
+ template: nil, system_prompt: nil)
38
39
  commands = Commands.new
39
- agent = Agent.new(build_provider(provider), toolbox: toolbox, output: output || build_output)
40
- session = Session.new(agent, input: input || build_input(commands: commands), commands: commands)
40
+ agent = Agent.new(build_provider(provider, **provider_options), toolbox: toolbox, output: output || build_output,
41
+ template: template, system_prompt: system_prompt)
42
+ session = Session.new(agent, input: input || build_input(commands: commands), commands: commands,
43
+ provider_name: provider, provider_options: provider_options)
41
44
  apply(session)
42
45
  session
43
46
  end
@@ -57,7 +60,9 @@ module Elelem
57
60
  def require_gem(name)
58
61
  require "elelem/#{name}"
59
62
  @registration.providers.fetch(name.to_s) { unregistered_provider(name) }
60
- rescue LoadError
63
+ rescue LoadError => e
64
+ raise unless e.path == "elelem/#{name}"
65
+
61
66
  raise "unknown provider: #{name.inspect}. Available: #{names.join(", ")}. Run: gem install elelem-#{name}"
62
67
  end
63
68
 
data/lib/elelem/repl.rb CHANGED
@@ -15,20 +15,20 @@ module Elelem
15
15
 
16
16
  private
17
17
 
18
+ # Only lines typed at the prompt go in history, not answers to a tool.
19
+ # A line is a command only when its first word names one, so a path such
20
+ # as /etc/hosts reaches the model. One failed turn does not end the session.
18
21
  def dispatch_line(line)
19
- line.start_with?("/") ? command(line) : @session.turn(line)
22
+ @session.input.remember(line) if @session.input.respond_to?(:remember)
23
+ command(line) || @session.turn(line)
24
+ rescue StandardError, ScriptError => e
25
+ Elelem.logger.debug("repl: #{e.message}")
26
+ @session.output.say(e.message, as: :error)
20
27
  end
21
28
 
22
29
  def command(line)
23
30
  name, args = line.delete_prefix("/").split(" ", 2)
24
- @session.commands.run(name, args) || @session.output.say(@session.commands.names.join(" "))
25
- rescue StandardError, ScriptError => e
26
- warn_and_say(e)
27
- end
28
-
29
- def warn_and_say(error)
30
- Elelem.logger.warn("repl: #{error.message}")
31
- @session.output.say(error.message, as: :error)
31
+ line.start_with?("/") && @session.commands.run(name, args)
32
32
  end
33
33
  end
34
34
  end
data/lib/elelem/result.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
-
5
3
  module Elelem
6
4
  class Result
7
5
  extend Forwardable
@@ -21,7 +19,5 @@ module Elelem
21
19
  end
22
20
 
23
21
  def ok? = !!@payload[:ok]
24
- def error? = !!@payload[:error]
25
- def error = @payload[:error]
26
22
  end
27
23
  end
@@ -5,13 +5,17 @@ module Elelem
5
5
  extend Forwardable
6
6
 
7
7
  attr_reader :agent, :input, :commands
8
+ # The provider the session was built with, and its options (such as --model).
9
+ attr_accessor :provider_name, :provider_options
8
10
 
9
11
  def_delegators :@agent, :provider, :provider=, :toolbox, :output, :conversation, :context, :turn
10
12
 
11
- def initialize(agent, input: NullInput.new, commands: Commands.new)
13
+ def initialize(agent, input: NullInput.new, commands: Commands.new, provider_name: nil, provider_options: {})
12
14
  @agent = agent
13
15
  @input = input
14
16
  @commands = commands
17
+ @provider_name = provider_name&.to_s
18
+ @provider_options = provider_options
15
19
  end
16
20
  end
17
21
  end
@@ -1,15 +1,76 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Elelem
4
+ # No model. Understands a tool name and its arguments, and the few plain
5
+ # requests the docs show. Reads only the first line, so piped text never
6
+ # becomes arguments.
4
7
  class StubProvider
5
- def fetch(messages, tools = [], &block)
6
- block.call(type: "thinking", text: "considering the request")
7
- return block.call(type: "saying", text: "done.") if messages.last[:role] == "tool"
8
+ REQUESTS = [
9
+ [/\Ashow me (\S+?)[.?!]?\z/i, "read", ->(m) { { "path" => m[1] } }],
10
+ [%r{\Acreate (\S*[./]\S*) with (.+)\z}i, "write", ->(m) { { "path" => m[1], "content" => m[2] } }],
11
+ [/\Awhat files are here\??\z/i, "execute", ->(_) { { "command" => "ls" } }],
12
+ [/\Arun (?:the )?tests?\z/i, "execute",
13
+ ->(_) { { "command" => File.executable?("bin/test") ? "bin/test" : "rake" } }],
14
+ [/\A(?:run|execute) `?(.+?)`?\z/i, "execute", ->(m) { { "command" => m[1] } }]
15
+ ].freeze
8
16
 
9
- block.call(type: "saying", text: "let me check.")
10
- return unless (tool = tools.first)
17
+ HELP = <<~TEXT
18
+ I need a model to understand that. Without one, type a tool and its
19
+ arguments (/tools lists them), or: show me README.md, create notes.txt
20
+ with hello, what files are here, run the tests.
21
+ For a model: gem install elelem-llama, then elelem -p llama
22
+ TEXT
11
23
 
12
- block.call(type: "doing", id: "stub-1", name: tool[:function][:name], arguments: {})
24
+ def fetch(messages, tools = [], &)
25
+ return yield(type: "saying", text: summary(messages)) if messages.last[:role] == "tool"
26
+
27
+ name, args = understand(messages.last[:content], tools)
28
+ name ? act(name, args, messages.length, &) : yield(type: "saying", text: HELP)
29
+ end
30
+
31
+ private
32
+
33
+ def act(name, args, turn)
34
+ yield(type: "thinking", text: "I'll call #{name}.")
35
+ yield(type: "doing", id: "stub-#{turn}", name: name, arguments: args)
36
+ end
37
+
38
+ def understand(content, tools)
39
+ text = content.to_s.lines.first.to_s.strip
40
+ request(text, tools.map { |t| t.dig(:function, :name) }) || direct(text, tools)
41
+ end
42
+
43
+ def request(text, names)
44
+ REQUESTS.each do |pattern, name, build|
45
+ match = pattern.match(text)
46
+ return [name, build.call(match)] if match && names.include?(name)
47
+ end
48
+ nil
49
+ end
50
+
51
+ def direct(text, tools)
52
+ name, rest = text.split(/\s+/, 2)
53
+ tool = tools.find { |t| t.dig(:function, :name) == name }
54
+ args = arguments(tool, rest.to_s) if tool
55
+ [name, args] if args && file_like?(name, args)
56
+ end
57
+
58
+ def arguments(tool, rest)
59
+ required = tool.dig(:function, :parameters, :required) || []
60
+ required.zip(rest.split(/\s+/, required.size)).to_h.compact.transform_keys(&:to_s)
61
+ end
62
+
63
+ # A model would not write to "a" when asked to "write a commit message".
64
+ def file_like?(name, args)
65
+ name != "write" || args["path"].to_s.match?(%r{[./]})
66
+ end
67
+
68
+ def summary(messages)
69
+ name = messages.rfind { |m| m[:tool_calls] }&.dig(:tool_calls, 0, :name)
70
+ result = JSON.parse(messages.last[:content].to_s).except("ok")
71
+ return "#{name} failed: #{result["error"]}." if result["error"]
72
+
73
+ result.empty? ? "#{name} is done." : result.map { |k, v| "#{k}: #{v}" }.join("\n")
13
74
  end
14
75
  end
15
76
  end
@@ -5,7 +5,7 @@ module Elelem
5
5
  DEFAULT = File.read(File.expand_path("prompts/default.erb", __dir__)).freeze
6
6
  USER_AGENTS_MD = Pathname.new(Dir.home).join(".agents/AGENTS.md").freeze
7
7
 
8
- attr_accessor :template
8
+ attr_reader :template
9
9
 
10
10
  def initialize(template = nil)
11
11
  @template = template || DEFAULT
data/lib/elelem/tool.rb CHANGED
@@ -4,31 +4,18 @@ module Elelem
4
4
  class Tool
5
5
  attr_reader :name, :description, :aliases
6
6
 
7
- def initialize(name, description:, schema: Schema.new, aliases: [], &handler)
7
+ # params and required are JSON Schema; every call is checked against them.
8
+ def initialize(name, description:, params: {}, required: [], aliases: [], &handler)
8
9
  @name = name
9
10
  @description = description
10
- @schema = schema
11
11
  @aliases = aliases.freeze
12
12
  @handler = handler
13
+ @schema = { type: "object", properties: params.freeze, required: required.freeze }.freeze
13
14
  end
14
15
 
15
- def call(args)
16
- @handler.call(args)
17
- end
18
-
19
- def validate(args)
20
- @schema.validate(args)
21
- end
22
-
23
- def to_h
24
- {
25
- type: "function",
26
- function: {
27
- name: name,
28
- description: description,
29
- parameters: @schema.to_h
30
- }
31
- }
32
- end
16
+ def call(args) = @handler.call(args)
17
+ def validate(args) = (@validator ||= JSONSchemer.schema(@schema)).validate(args || {}).map { |e| e["error"] }
18
+ def inspect = "#<#{self.class} #{name}>"
19
+ def to_h = { type: "function", function: { name: name, description: description, parameters: @schema } }
33
20
  end
34
21
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Elelem
4
4
  class Toolbox
5
- attr_reader :tools, :hooks, :aliases
5
+ attr_reader :tools
6
6
 
7
7
  def initialize
8
8
  @tools = {}
@@ -11,17 +11,12 @@ module Elelem
11
11
  end
12
12
 
13
13
  def clear!
14
- @tools.clear
15
- @aliases.clear
16
- @hooks[:before].clear
17
- @hooks[:after].clear
14
+ [@tools, @aliases, @hooks[:before], @hooks[:after]].each(&:clear)
18
15
  end
19
16
 
20
17
  def add(name, description:, params: {}, required: [], aliases: [], &handler)
21
- schema = Schema.new(params: params, required: required)
22
- tool = Tool.new(name, description: description, schema: schema, aliases: aliases, &handler)
23
- @tools[name] = tool
24
- tool.aliases.each { |a| @aliases[a] = name }
18
+ aliases.each { |a| @aliases[a] = name }
19
+ @tools[name] = Tool.new(name, description:, params:, required:, aliases:, &handler)
25
20
  end
26
21
 
27
22
  def before(tool_name = :*, &block)
@@ -32,15 +27,17 @@ module Elelem
32
27
  @hooks[:after][tool_name] << block
33
28
  end
34
29
 
30
+ # The tool by name, else by alias, so a real tool named like an alias wins.
35
31
  def tool_for(name)
36
- tools.fetch(@aliases.fetch(name, name)) { NullTool.new(name, available: tools.keys) }
32
+ tools[name] || tools[@aliases[name]]
37
33
  end
38
34
 
39
35
  def run(name, args)
40
36
  tool = tool_for(name)
41
- invalid(tool, args) || to_result(dispatch(tool, args))
37
+ invalid(name, tool, args) || to_result(dispatch(tool, args))
42
38
  rescue StandardError, ScriptError => e
43
- failure(name, args, e)
39
+ Elelem.logger.debug("toolbox: #{e.message}\n#{e.backtrace.join("\n")}")
40
+ Result.failure(error: e.message, name: name, args: args)
44
41
  end
45
42
 
46
43
  def to_a
@@ -49,35 +46,25 @@ module Elelem
49
46
 
50
47
  private
51
48
 
52
- def invalid(tool, args)
53
- errors = tool.validate(args)
49
+ def invalid(name, tool, args)
50
+ errors = tool ? tool.validate(args) : ["unknown tool: #{name.inspect}. Available: #{tools.keys.join(", ")}."]
54
51
  Result.failure(error: errors.join(", ")) if errors.any?
55
52
  end
56
53
 
54
+ # A tool fails by returning error:, or ok: false (as execute does for a
55
+ # non-zero exit).
57
56
  def to_result(result)
58
- result[:error] ? Result.failure(result) : Result.success(result)
59
- end
60
-
61
- def failure(name, args, error)
62
- Elelem.logger.warn("toolbox: #{error.message}\n#{error.backtrace.join("\n")}")
63
- Result.failure(error: error.message, name: name, args: args)
57
+ result[:error] || result[:ok] == false ? Result.failure(result) : Result.success(result)
64
58
  end
65
59
 
66
60
  def dispatch(tool, args)
67
- run_before_hooks(tool, args)
68
- result = tool.call(args)
69
- run_after_hooks(tool, args, result)
70
- result
71
- end
72
-
73
- def run_before_hooks(tool, args)
74
- @hooks[:before][:*].each { |h| h.call(args, tool_name: tool.name) }
75
- @hooks[:before][tool.name].each { |h| h.call(args) }
61
+ run_hooks(:before, tool, args)
62
+ tool.call(args).tap { |result| run_hooks(:after, tool, args, result) }
76
63
  end
77
64
 
78
- def run_after_hooks(tool, args, result)
79
- @hooks[:after][:*].each { |h| h.call(args, result, tool_name: tool.name) }
80
- @hooks[:after][tool.name].each { |h| h.call(args, result) }
65
+ def run_hooks(type, tool, *)
66
+ @hooks[type][:*].each { |h| h.call(*, tool_name: tool.name) }
67
+ @hooks[type][tool.name].each { |h| h.call(*) }
81
68
  end
82
69
  end
83
70
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Elelem
4
- VERSION = "0.12.0"
4
+ VERSION = "0.13.0"
5
5
  end
data/lib/elelem.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "erb"
4
+ require "fileutils"
4
5
  require "forwardable"
5
6
  require "json"
6
7
  require "json_schemer"
@@ -8,42 +9,47 @@ require "logger"
8
9
  require "open3"
9
10
  require "pathname"
10
11
  require "shellwords"
12
+ require "stringio"
13
+ require "tempfile"
11
14
 
12
- require_relative "elelem/agent"
13
- require_relative "elelem/slash_command"
14
- require_relative "elelem/commands"
15
- require_relative "elelem/conversation"
16
- require_relative "elelem/input"
17
- require_relative "elelem/null_input"
15
+ require_relative "elelem/version"
18
16
  require_relative "elelem/output"
19
17
  require_relative "elelem/null_output"
18
+ require_relative "elelem/input"
19
+ require_relative "elelem/null_input"
20
+ require_relative "elelem/builtins"
21
+ require_relative "elelem/builtins/terminal"
22
+ require_relative "elelem/system_prompt"
23
+ require_relative "elelem/conversation"
24
+ require_relative "elelem/result"
25
+ require_relative "elelem/tool"
26
+ require_relative "elelem/toolbox"
27
+ require_relative "elelem/commands"
28
+ require_relative "elelem/agent"
29
+ require_relative "elelem/session"
30
+ require_relative "elelem/repl"
31
+ require_relative "elelem/stub_provider"
20
32
  require_relative "elelem/plugins"
21
33
  require_relative "elelem/registration"
22
34
  require_relative "elelem/registry"
23
- require_relative "elelem/repl"
24
35
  require_relative "elelem/config"
25
- require_relative "elelem/result"
26
- require_relative "elelem/schema"
27
- require_relative "elelem/session"
28
- require_relative "elelem/stub_provider"
29
- require_relative "elelem/system_prompt"
30
- require_relative "elelem/tool"
31
- require_relative "elelem/transcript"
32
- require_relative "elelem/null_tool"
33
- require_relative "elelem/toolbox"
34
- require_relative "elelem/version"
35
36
 
36
37
  module Elelem
37
- def self.start(provider: "stub", toolbox: Toolbox.new)
38
- Repl.new(build(provider: provider, toolbox: toolbox)).run
38
+ def self.start(provider: "stub", toolbox: Toolbox.new, provider_options: {}, template: nil, system_prompt: nil)
39
+ Repl.new(build(provider: provider, toolbox: toolbox, provider_options: provider_options, template: template,
40
+ system_prompt: system_prompt)).run
39
41
  end
40
42
 
41
- def self.ask(prompt, provider: "stub", toolbox: Toolbox.new, output: NullOutput.new)
42
- build(provider: provider, toolbox: toolbox, input: NullInput.new, output: output).turn(prompt)
43
+ def self.ask(prompt, provider: "stub", toolbox: Toolbox.new, output: NullOutput.new, provider_options: {},
44
+ template: nil, system_prompt: nil)
45
+ build(provider: provider, toolbox: toolbox, input: NullInput.new, output: output,
46
+ provider_options: provider_options, template: template, system_prompt: system_prompt).turn(prompt)
43
47
  end
44
48
 
45
- def self.build(provider: "stub", toolbox: Toolbox.new, input: nil, output: nil)
46
- Config.build_session(provider, toolbox: toolbox, input: input, output: output)
49
+ def self.build(provider: "stub", toolbox: Toolbox.new, input: nil, output: nil, provider_options: {},
50
+ template: nil, system_prompt: nil)
51
+ Config.build_session(provider, toolbox: toolbox, input: input, output: output,
52
+ provider_options: provider_options, template: template, system_prompt: system_prompt)
47
53
  end
48
54
 
49
55
  def self.logger