elelem 0.11.0 → 0.12.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72a4cd636b39b6f30cc0520c3445a3b8748625fd5ca52f15b05d27349a0701b4
4
- data.tar.gz: 12c55aaf15f42e7607fb60ed78500b44655ba6eaab857601ec58073378587866
3
+ metadata.gz: d4696869850fc38fba45a65a38aca74f8663fe9b8562deb7a987cbc71058221a
4
+ data.tar.gz: 8ec379df1537243659cfee997c02fde40912be780ace54b14bb0ff181f5fad64
5
5
  SHA512:
6
- metadata.gz: '018856c824b90cbbe658c3b117f683737f14c34f493ba3629281cd7318c11d039585b829e201a9a8bf591619747d2d6c7f1f3568f53a79c40222632a706303ac'
7
- data.tar.gz: ab3132d4613f560dfb97e7bcfa406e5f49ac66f5e642ec85f77c1a25d030825f1a7d9f6401d54dd6eb3f767ec69506b8f114e3a269324fb5fd72f0ca97827a0d
6
+ metadata.gz: 52e225e4b206091bcd1838cce432ee2391c648fa4f995a722dfefc2820985bb405e5e3574a972a8e31be260695eb368f3f6f3dcfa32f8cbeeb76b5fab6b1a2f3
7
+ data.tar.gz: c508bfedce1daf081011046ca77a13872af1bb133d161edfb944d7e4019e92222b15dc9aeb4f98376328e099a62e5ec89f2b67d2f595ed6ea6a58281a0a44589
data/lib/elelem/agent.rb CHANGED
@@ -2,94 +2,100 @@
2
2
 
3
3
  module Elelem
4
4
  class Agent
5
- attr_reader :conversation, :toolbox, :input, :output, :commands
5
+ attr_reader :conversation, :toolbox, :output
6
6
  attr_accessor :provider
7
7
 
8
- def initialize(provider, toolbox: Toolbox.new, input: NullInput.new, output: NullOutput.new, system_prompt: nil, commands: nil)
8
+ def initialize(provider, toolbox: Toolbox.new, output: NullOutput.new, system_prompt: nil)
9
9
  @provider = provider
10
10
  @toolbox = toolbox
11
- @commands = commands || Commands.new
12
11
  @output = output
13
- @input = input
14
- @conversation = Conversation.new
15
- @system_prompt = SystemPrompt.new(system_prompt)
16
- end
17
-
18
- def repl
19
- output.say "elelem v#{VERSION}"
20
- loop do
21
- line = input.ask("> ")
22
- break if line.nil?
23
- next if line.empty?
24
- line.start_with?("/") ? command(line) : turn(line)
25
- end
12
+ @conversation = Transcript.new(system_prompt: system_prompt)
26
13
  end
27
14
 
28
15
  def context
29
- @conversation.to_a(system_prompt: @system_prompt.render)
16
+ @conversation.to_a
30
17
  end
31
18
 
32
19
  def turn(prompt)
33
20
  @conversation.add(role: "user", content: prompt)
34
- ctx = []
35
- content = nil
21
+ content = run_until_done([])
22
+ @conversation.add(role: "assistant", content: content)
23
+ content
24
+ end
36
25
 
26
+ private
27
+
28
+ def run_until_done(ctx)
37
29
  loop do
38
- output.waiting
39
- content, tool_calls = fetch_response(ctx)
40
- output.say(content, as: :markdown)
41
- break if tool_calls.empty?
42
-
43
- ctx << { role: "assistant", content: content, tool_calls: tool_calls }.compact
44
- tool_calls.each do |tool_call|
45
- ctx << { role: "tool", tool_call_id: tool_call[:id], content: process(tool_call).to_json }
46
- end
30
+ content, tool_calls = fetch_and_say(ctx)
31
+ return content if tool_calls.empty?
32
+
33
+ ctx.concat(tool_turn(content, tool_calls))
47
34
  end
35
+ end
48
36
 
49
- @conversation.add(role: "assistant", content: content)
50
- content
37
+ def fetch_and_say(ctx)
38
+ output.waiting
39
+ content, tool_calls = fetch_response(ctx)
40
+ output.say(content, as: :markdown)
41
+ [content, tool_calls]
51
42
  end
52
43
 
53
- private
44
+ def tool_turn(content, tool_calls)
45
+ results = tool_calls.map { |tool_call| tool_result(tool_call) }
46
+ [{ role: "assistant", content: content, tool_calls: tool_calls }.compact, *results]
47
+ end
54
48
 
55
- def command(line)
56
- parts = line.delete_prefix("/").split(" ", 2)
57
- name, args = parts[0], parts[1]
58
- commands.run(name, args) || output.say(commands.names.join(" "))
59
- rescue => e
60
- Elelem.logger.warn("agent: #{e.message}")
61
- output.say(e.message, as: :error)
49
+ def tool_result(tool_call)
50
+ { role: "tool", tool_call_id: tool_call[:id], content: process(tool_call).to_json }
62
51
  end
63
52
 
64
53
  def process(tool_call)
65
- name, args = tool_call[:name], tool_call[:arguments]
54
+ name = tool_call[:name]
55
+ args = tool_call[:arguments]
66
56
  output.doing(toolbox.tool_for(name).name, args)
67
57
  Elelem.logger.debug("agent: #{name}(#{args.inspect})")
68
- result = toolbox.run(name.to_s, args)
58
+ log_result(name, toolbox.run(name.to_s, args))
59
+ end
60
+
61
+ def log_result(name, result)
69
62
  Elelem.logger.debug("agent: #{name} -> #{result.inspect}")
70
63
  result
71
64
  end
72
65
 
73
66
  def fetch_response(ctx)
67
+ collect_response(ctx)
68
+ rescue StandardError, ScriptError => e
69
+ fetch_error(e)
70
+ end
71
+
72
+ def collect_response(ctx)
74
73
  content = String.new
75
74
  tool_calls = []
75
+ provider.fetch(messages(ctx), toolbox.to_a) { |event| handle_event(event, content, tool_calls) }
76
+ [content, tool_calls]
77
+ end
78
+
79
+ def messages(ctx)
80
+ @conversation.to_a(ctx)
81
+ end
76
82
 
77
- provider.fetch(@conversation.to_a(system_prompt: @system_prompt.render) + ctx, toolbox.to_a) do |event|
78
- case event[:type]
79
- when "saying"
80
- content << event[:text].to_s
81
- when "thinking"
82
- output.thinking(event[:text])
83
- when "doing"
84
- tool_calls << { id: event[:id], name: event[:name], arguments: event[:arguments] }
85
- end
83
+ def fetch_error(error)
84
+ warn_and_say(error)
85
+ ["Error: #{error.message}", []]
86
+ end
87
+
88
+ def handle_event(event, content, tool_calls)
89
+ case event[:type]
90
+ when "saying" then content << event[:text].to_s
91
+ when "thinking" then output.thinking(event[:text])
92
+ when "doing" then tool_calls << { id: event[:id], name: event[:name], arguments: event[:arguments] }
86
93
  end
94
+ end
87
95
 
88
- [content, tool_calls]
89
- rescue => e
90
- Elelem.logger.warn("agent: #{e.message}")
91
- output.say(e.message, as: :error)
92
- ["Error: #{e.message}", []]
96
+ def warn_and_say(error)
97
+ Elelem.logger.warn("agent: #{error.message}")
98
+ output.say(error.message, as: :error)
93
99
  end
94
100
  end
95
101
  end
data/lib/elelem/cli.rb CHANGED
@@ -8,6 +8,15 @@ require "reline"
8
8
  module Elelem
9
9
  class CLI
10
10
  COMMANDS = %w[chat ask files help].freeze
11
+ USAGE = <<~TEXT
12
+ \nCommands:
13
+ chat Interactive REPL (default)
14
+ ask <prompt> One-shot query (reads stdin if piped)
15
+ files Output files as XML (no options)
16
+ help Show this help
17
+ \nOptions:
18
+ TEXT
19
+ private_constant :USAGE
11
20
 
12
21
  def initialize(args, provider: "stub")
13
22
  @provider = provider
@@ -23,54 +32,85 @@ module Elelem
23
32
  private
24
33
 
25
34
  def parse(args)
26
- @parser = OptionParser.new do |o|
27
- o.banner = "Usage: #{File.basename($PROGRAM_NAME)} [command] [options] [args]"
28
- o.separator "\nCommands:"
29
- o.separator " chat Interactive REPL (default)"
30
- o.separator " ask <prompt> One-shot query (reads stdin if piped)"
31
- o.separator " files Output files as XML (no options)"
32
- o.separator " help Show this help"
33
- o.separator "\nOptions:"
34
- o.on("-p", "--provider NAME", "Provider to use (default: #{@provider})") { |p| @provider = p }
35
- o.on("-h", "--help", "Show this help") { puts o; exit }
36
- end
35
+ @parser = OptionParser.new { |o| describe_options(o) }
37
36
  @parser.parse!(args)
38
37
  end
39
38
 
39
+ def describe_options(parser)
40
+ describe_usage(parser)
41
+ parser.on("-p", "--provider NAME", "Provider to use (default: #{@provider})") { |p| @provider = p }
42
+ parser.on("-h", "--help", "Show this help") { print_help_and_exit(parser) }
43
+ end
44
+
45
+ def print_help_and_exit(parser)
46
+ puts parser
47
+ exit
48
+ end
49
+
50
+ def describe_usage(parser)
51
+ parser.banner = "Usage: #{File.basename($PROGRAM_NAME)} [command] [options] [args]"
52
+ parser.separator USAGE
53
+ end
54
+
40
55
  def help
41
56
  puts @parser
42
57
  end
43
58
 
44
59
  def chat
45
60
  Elelem.start(provider: @provider)
46
- rescue => e
47
- Elelem.logger.warn("cli: #{e.message}\n#{e.backtrace.join("\n")}")
48
- abort "elelem: #{e.message}"
61
+ rescue StandardError, ScriptError => e
62
+ warn_and_abort(e)
49
63
  end
50
64
 
51
65
  def ask
52
66
  abort "Usage: elelem-chat ask <prompt>" if @args.empty?
67
+
68
+ reply = Elelem.ask(prompt_for_ask, provider: @provider, output: output_for_ask)
69
+ print_reply(reply, piped: piped?)
70
+ rescue StandardError, ScriptError => e
71
+ warn_and_abort(e)
72
+ end
73
+
74
+ def piped?
75
+ !$stdout.tty?
76
+ end
77
+
78
+ def output_for_ask
79
+ Elelem::Output.new(stream: piped? ? $stderr : $stdout)
80
+ end
81
+
82
+ def prompt_for_ask
53
83
  prompt = @args.join(" ")
54
- prompt = "#{prompt}\n\n```\n#{$stdin.read}\n```" if $stdin.stat.pipe?
55
- piped = !$stdout.tty?
56
- output = Elelem::Output.new(stream: piped ? $stderr : $stdout)
57
- reply = Elelem.ask(prompt, provider: @provider, output: output)
84
+ $stdin.stat.pipe? ? "#{prompt}\n\n```\n#{$stdin.read}\n```" : prompt
85
+ end
86
+
87
+ def print_reply(reply, piped:)
58
88
  abort "elelem: no reply" if reply.to_s.strip.empty?
59
89
  Elelem::Output.new.say(reply, as: :markdown) if piped
60
- rescue => e
61
- Elelem.logger.warn("cli: #{e.message}\n#{e.backtrace.join("\n")}")
62
- abort "elelem: #{e.message}"
90
+ end
91
+
92
+ def warn_and_abort(error)
93
+ Elelem.logger.warn("cli: #{error.message}\n#{error.backtrace.join("\n")}")
94
+ abort "elelem: #{error.message}"
63
95
  end
64
96
 
65
97
  def files
66
- files = $stdin.stat.pipe? ? $stdin.readlines : `git ls-files`.lines
67
98
  puts "<documents>"
68
- files.each_with_index do |line, i|
69
- path = line.strip
70
- next if path.empty? || !File.file?(path)
71
- puts %Q{<document index="#{i + 1}"><source>#{path}</source><document_content><![CDATA[#{File.read(path)}]]></document_content></document>}
72
- end
99
+ candidate_files.each_with_index { |line, i| print_document(line, i) }
73
100
  puts "</documents>"
74
- end
101
+ end
102
+
103
+ def candidate_files
104
+ $stdin.stat.pipe? ? $stdin.readlines : `git ls-files`.lines
105
+ end
106
+
107
+ def print_document(line, index)
108
+ path = line.strip
109
+ return if path.empty? || !File.file?(path)
110
+
111
+ content = File.read(path)
112
+ puts %(<document index="#{index + 1}"><source>#{path}</source>) +
113
+ %(<document_content><![CDATA[#{content}]]></document_content></document>)
114
+ end
75
115
  end
76
116
  end
@@ -18,7 +18,7 @@ module Elelem
18
18
 
19
19
  def completions_for(name, partial = "")
20
20
  cmd = command_for(name)
21
- return [] unless cmd && cmd.completions
21
+ return [] unless cmd&.completions
22
22
 
23
23
  options = cmd.completions.respond_to?(:call) ? cmd.completions.call : cmd.completions
24
24
  options.select { |o| o.start_with?(partial) }
data/lib/elelem/config.rb CHANGED
@@ -4,14 +4,14 @@ module Elelem
4
4
  module Config
5
5
  extend SingleForwardable
6
6
 
7
- def_single_delegators :default, :build_provider, :build_agent, :apply, :reload, :names
7
+ def_single_delegators :default, :build_provider, :build_session, :apply, :reload, :names
8
8
 
9
9
  def self.default
10
10
  @default ||= Registry.new
11
11
  end
12
12
  end
13
13
 
14
- def self.configure(&block)
15
- Config.default.configure(&block)
14
+ def self.configure(&)
15
+ Config.default.configure(&)
16
16
  end
17
17
  end
@@ -10,6 +10,7 @@ module Elelem
10
10
 
11
11
  def add(role:, content:, **extra)
12
12
  raise ArgumentError, "invalid role: #{role}" unless ROLES.include?(role)
13
+
13
14
  @messages << { role: role, content: content, **extra }.compact
14
15
  end
15
16
 
data/lib/elelem/input.rb CHANGED
@@ -22,19 +22,17 @@ module Elelem
22
22
 
23
23
  def complete(target, preposing)
24
24
  line = "#{preposing}#{target}"
25
-
26
- if line.start_with?("/") && !preposing.include?(" ")
27
- return command_names.select { |c| c.start_with?(line) }
28
- end
29
-
30
- if preposing.start_with?("/") && preposing.include?(" ")
31
- cmd_name = preposing.delete_prefix("/").split(" ", 2).first
32
- return complete_command_args(cmd_name, target)
33
- end
25
+ return command_names.select { |c| c.start_with?(line) } if line.start_with?("/") && !preposing.include?(" ")
26
+ return complete_command_args_for(preposing, target) if preposing.start_with?("/") && preposing.include?(" ")
34
27
 
35
28
  complete_files(target)
36
29
  end
37
30
 
31
+ def complete_command_args_for(preposing, target)
32
+ cmd_name = preposing.delete_prefix("/").split(" ", 2).first
33
+ complete_command_args(cmd_name, target)
34
+ end
35
+
38
36
  def command_names
39
37
  @commands.respond_to?(:names) ? @commands.names : @commands
40
38
  end
data/lib/elelem/output.rb CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  module Elelem
4
4
  class Output
5
+ RENDERERS = {
6
+ error: ->(text) { "error: #{text}" },
7
+ markdown: ->(text) { "```\n#{text}\n```\n" }
8
+ }.freeze
9
+ private_constant :RENDERERS
10
+
5
11
  def initialize(stream: $stdout, **)
6
12
  @stream = stream
7
13
  end
@@ -26,8 +32,7 @@ module Elelem
26
32
  say "#{state} \e[36m#{name}\e[0m(#{args})"
27
33
  end
28
34
 
29
- def waiting
30
- end
35
+ def waiting; end
31
36
 
32
37
  def display_file(path, fallback: nil)
33
38
  say(fallback || path)
@@ -36,14 +41,7 @@ module Elelem
36
41
  private
37
42
 
38
43
  def render(text, as)
39
- case as
40
- when :error
41
- return "error: #{text}"
42
- when :markdown
43
- "```\n" + text + "\n```\n"
44
- else
45
- text
46
- end
44
+ RENDERERS[as]&.call(text) || text
47
45
  end
48
46
 
49
47
  def write(text)
@@ -15,18 +15,22 @@ module Elelem
15
15
  def load!(force: false)
16
16
  return if @loaded && !force
17
17
 
18
- @load_paths.each do |path|
19
- dir = File.expand_path(path)
20
- next unless File.directory?(dir)
18
+ @load_paths.each { |path| load_dir(File.expand_path(path)) }
19
+ @loaded = true
20
+ end
21
21
 
22
- Dir["#{dir}/*.rb"].sort.each do |file|
23
- load(file)
24
- rescue => e
25
- warn "elelem: failed to load plugin #{file}: #{e.message}"
26
- end
27
- end
22
+ private
28
23
 
29
- @loaded = true
24
+ def load_dir(dir)
25
+ return unless File.directory?(dir)
26
+
27
+ Dir["#{dir}/*.rb"].each { |file| load_plugin(file) }
28
+ end
29
+
30
+ def load_plugin(file)
31
+ load(file)
32
+ rescue StandardError, ScriptError => e
33
+ warn "elelem: failed to load plugin #{file}: #{e.message}"
30
34
  end
31
35
  end
32
36
  end
@@ -17,53 +17,52 @@ module Elelem
17
17
  @registration.providers.fetch(name.to_s) { require_gem(name) }.call
18
18
  end
19
19
 
20
- def apply(agent)
20
+ def apply(session)
21
21
  @plugins.load!
22
- @registration.setups.each_value { |block| block.call(agent) }
22
+ @registration.setups.each_value { |block| block.call(session) }
23
23
  end
24
24
 
25
- def reload(agent)
25
+ def reload(session)
26
26
  @registration.clear_setups!
27
- agent.toolbox.clear!
28
- agent.commands.clear!
27
+ session.toolbox.clear!
28
+ session.commands.clear!
29
29
  @plugins.load!(force: true)
30
- apply(agent)
30
+ apply(session)
31
31
  end
32
32
 
33
33
  def names
34
34
  @registration.providers.keys
35
35
  end
36
36
 
37
- def build_agent(provider, toolbox: Toolbox.new, input: nil, output: nil)
37
+ def build_session(provider, toolbox: Toolbox.new, input: nil, output: nil)
38
38
  commands = Commands.new
39
- agent = Agent.new(
40
- build_provider(provider),
41
- toolbox: toolbox,
42
- commands: commands,
43
- input: input || build_input(commands: commands),
44
- output: output || build_output,
45
- )
46
- apply(agent)
47
- agent
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)
41
+ apply(session)
42
+ session
48
43
  end
49
44
 
50
45
  private
51
46
 
52
- def build_input(**opts)
47
+ def build_input(**)
53
48
  @plugins.load!
54
- (@registration.factories[:input] || ->(**o) { Input.new(**o) }).call(**opts)
49
+ (@registration.factories[:input] || ->(**o) { Input.new(**o) }).call(**)
55
50
  end
56
51
 
57
- def build_output(**opts)
52
+ def build_output(**)
58
53
  @plugins.load!
59
- (@registration.factories[:output] || ->(**o) { Output.new(**o) }).call(**opts)
54
+ (@registration.factories[:output] || ->(**o) { Output.new(**o) }).call(**)
60
55
  end
61
56
 
62
57
  def require_gem(name)
63
58
  require "elelem/#{name}"
64
- @registration.providers.fetch(name.to_s) { raise "elelem-#{name} did not register a provider named #{name.inspect}" }
59
+ @registration.providers.fetch(name.to_s) { unregistered_provider(name) }
65
60
  rescue LoadError
66
61
  raise "unknown provider: #{name.inspect}. Available: #{names.join(", ")}. Run: gem install elelem-#{name}"
67
62
  end
63
+
64
+ def unregistered_provider(name)
65
+ raise "elelem-#{name} did not register a provider named #{name.inspect}"
66
+ end
68
67
  end
69
68
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ class Repl
5
+ def initialize(session)
6
+ @session = session
7
+ end
8
+
9
+ def run
10
+ @session.output.say "elelem v#{VERSION}"
11
+ while (line = @session.input.ask("> "))
12
+ dispatch_line(line) unless line.empty?
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def dispatch_line(line)
19
+ line.start_with?("/") ? command(line) : @session.turn(line)
20
+ end
21
+
22
+ def command(line)
23
+ 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)
32
+ end
33
+ end
34
+ end
data/lib/elelem/result.rb CHANGED
@@ -5,6 +5,7 @@ require "forwardable"
5
5
  module Elelem
6
6
  class Result
7
7
  extend Forwardable
8
+
8
9
  def_delegators :@payload, :[], :each, :to_json, :to_h, :key?, :merge
9
10
 
10
11
  def self.success(payload)
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ class Schema
5
+ def initialize(params: {}, required: [])
6
+ @params = params.freeze
7
+ @required = required.freeze
8
+ @schema_hash = { type: "object", properties: @params, required: @required }.freeze
9
+ @json_schemer = JSONSchemer.schema(@schema_hash)
10
+ end
11
+
12
+ def to_h
13
+ @schema_hash
14
+ end
15
+
16
+ def validate(args)
17
+ @json_schemer.validate(args || {}).map { |error| error["error"] }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ class Session
5
+ extend Forwardable
6
+
7
+ attr_reader :agent, :input, :commands
8
+
9
+ def_delegators :@agent, :provider, :provider=, :toolbox, :output, :conversation, :context, :turn
10
+
11
+ def initialize(agent, input: NullInput.new, commands: Commands.new)
12
+ @agent = agent
13
+ @input = input
14
+ @commands = commands
15
+ end
16
+ end
17
+ end
@@ -3,6 +3,7 @@
3
3
  module Elelem
4
4
  class SystemPrompt
5
5
  DEFAULT = File.read(File.expand_path("prompts/default.erb", __dir__)).freeze
6
+ USER_AGENTS_MD = Pathname.new(Dir.home).join(".agents/AGENTS.md").freeze
6
7
 
7
8
  attr_accessor :template
8
9
 
@@ -17,6 +18,14 @@ module Elelem
17
18
  private
18
19
 
19
20
  def agents_md
21
+ [global_agents_md, project_agents_md].compact.then { |parts| parts.empty? ? nil : parts.join("\n\n") }
22
+ end
23
+
24
+ def global_agents_md
25
+ USER_AGENTS_MD.read if USER_AGENTS_MD.exist?
26
+ end
27
+
28
+ def project_agents_md
20
29
  Pathname.pwd.ascend.each do |dir|
21
30
  file = dir / "AGENTS.md"
22
31
  return file.read if file.exist?
data/lib/elelem/tool.rb CHANGED
@@ -2,27 +2,22 @@
2
2
 
3
3
  module Elelem
4
4
  class Tool
5
- attr_reader :name, :description, :params, :required, :aliases
5
+ attr_reader :name, :description, :aliases
6
6
 
7
- def initialize(name, description:, params: {}, required: [], aliases: [], &fn)
7
+ def initialize(name, description:, schema: Schema.new, aliases: [], &handler)
8
8
  @name = name
9
9
  @description = description
10
- @params = params.freeze
11
- @required = required.freeze
10
+ @schema = schema
12
11
  @aliases = aliases.freeze
13
- @fn = fn
14
- @schema_hash = { type: "object", properties: @params, required: @required }.freeze
15
- @schema = JSONSchemer.schema(@schema_hash)
12
+ @handler = handler
16
13
  end
17
14
 
18
15
  def call(args)
19
- @fn.call(args)
16
+ @handler.call(args)
20
17
  end
21
18
 
22
19
  def validate(args)
23
- @schema.validate(args || {}).map do |error|
24
- error["error"]
25
- end
20
+ @schema.validate(args)
26
21
  end
27
22
 
28
23
  def to_h
@@ -31,10 +26,9 @@ module Elelem
31
26
  function: {
32
27
  name: name,
33
28
  description: description,
34
- parameters: @schema_hash
29
+ parameters: @schema.to_h
35
30
  }
36
31
  }
37
32
  end
38
-
39
33
  end
40
34
  end
@@ -17,8 +17,9 @@ module Elelem
17
17
  @hooks[:after].clear
18
18
  end
19
19
 
20
- def add(name, description:, params: {}, required: [], aliases: [], &fn)
21
- tool = Tool.new(name, description: description, params: params, required: required, aliases: aliases, &fn)
20
+ 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)
22
23
  @tools[name] = tool
23
24
  tool.aliases.each { |a| @aliases[a] = name }
24
25
  end
@@ -37,14 +38,9 @@ module Elelem
37
38
 
38
39
  def run(name, args)
39
40
  tool = tool_for(name)
40
- errors = tool.validate(args)
41
- return Result.failure(error: errors.join(", ")) if errors.any?
42
-
43
- result = dispatch(tool, args)
44
- result[:error] ? Result.failure(result) : Result.success(result)
45
- rescue => e
46
- Elelem.logger.warn("toolbox: #{e.message}\n#{e.backtrace.join("\n")}")
47
- Result.failure(error: e.message, name: name, args: args)
41
+ invalid(tool, args) || to_result(dispatch(tool, args))
42
+ rescue StandardError, ScriptError => e
43
+ failure(name, args, e)
48
44
  end
49
45
 
50
46
  def to_a
@@ -53,13 +49,35 @@ module Elelem
53
49
 
54
50
  private
55
51
 
52
+ def invalid(tool, args)
53
+ errors = tool.validate(args)
54
+ Result.failure(error: errors.join(", ")) if errors.any?
55
+ end
56
+
57
+ 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)
64
+ end
65
+
56
66
  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)
57
74
  @hooks[:before][:*].each { |h| h.call(args, tool_name: tool.name) }
58
75
  @hooks[:before][tool.name].each { |h| h.call(args) }
59
- result = tool.call(args)
76
+ end
77
+
78
+ def run_after_hooks(tool, args, result)
60
79
  @hooks[:after][:*].each { |h| h.call(args, result, tool_name: tool.name) }
61
80
  @hooks[:after][tool.name].each { |h| h.call(args, result) }
62
- result
63
81
  end
64
82
  end
65
83
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ class Transcript
5
+ def initialize(system_prompt: nil)
6
+ @conversation = Conversation.new
7
+ @system_prompt = SystemPrompt.new(system_prompt)
8
+ end
9
+
10
+ def add(role:, content:, **extra)
11
+ @conversation.add(role: role, content: content, **extra)
12
+ end
13
+
14
+ def clear!
15
+ @conversation.clear!
16
+ end
17
+
18
+ def last
19
+ @conversation.last
20
+ end
21
+
22
+ def to_a(extra = [])
23
+ @conversation.to_a(system_prompt: @system_prompt.render) + extra
24
+ end
25
+ end
26
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Elelem
4
- VERSION = "0.11.0"
4
+ VERSION = "0.12.0"
5
5
  end
data/lib/elelem.rb CHANGED
@@ -20,26 +20,30 @@ require_relative "elelem/null_output"
20
20
  require_relative "elelem/plugins"
21
21
  require_relative "elelem/registration"
22
22
  require_relative "elelem/registry"
23
+ require_relative "elelem/repl"
23
24
  require_relative "elelem/config"
24
25
  require_relative "elelem/result"
26
+ require_relative "elelem/schema"
27
+ require_relative "elelem/session"
25
28
  require_relative "elelem/stub_provider"
26
29
  require_relative "elelem/system_prompt"
27
30
  require_relative "elelem/tool"
31
+ require_relative "elelem/transcript"
28
32
  require_relative "elelem/null_tool"
29
33
  require_relative "elelem/toolbox"
30
34
  require_relative "elelem/version"
31
35
 
32
36
  module Elelem
33
37
  def self.start(provider: "stub", toolbox: Toolbox.new)
34
- self.build(provider: provider, toolbox: toolbox).repl
38
+ Repl.new(build(provider: provider, toolbox: toolbox)).run
35
39
  end
36
40
 
37
41
  def self.ask(prompt, provider: "stub", toolbox: Toolbox.new, output: NullOutput.new)
38
- self.build(provider: provider, toolbox: toolbox, input: NullInput.new, output: output).turn(prompt)
42
+ build(provider: provider, toolbox: toolbox, input: NullInput.new, output: output).turn(prompt)
39
43
  end
40
44
 
41
45
  def self.build(provider: "stub", toolbox: Toolbox.new, input: nil, output: nil)
42
- Config.build_agent(provider, toolbox: toolbox, input: input, output: output)
46
+ Config.build_session(provider, toolbox: toolbox, input: input, output: output)
43
47
  end
44
48
 
45
49
  def self.logger
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elelem
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - mo khan
@@ -204,12 +204,16 @@ files:
204
204
  - lib/elelem/prompts/default.erb
205
205
  - lib/elelem/registration.rb
206
206
  - lib/elelem/registry.rb
207
+ - lib/elelem/repl.rb
207
208
  - lib/elelem/result.rb
209
+ - lib/elelem/schema.rb
210
+ - lib/elelem/session.rb
208
211
  - lib/elelem/slash_command.rb
209
212
  - lib/elelem/stub_provider.rb
210
213
  - lib/elelem/system_prompt.rb
211
214
  - lib/elelem/tool.rb
212
215
  - lib/elelem/toolbox.rb
216
+ - lib/elelem/transcript.rb
213
217
  - lib/elelem/version.rb
214
218
  homepage: https://src.mokhan.ca/elelem/elelem
215
219
  licenses:
@@ -218,6 +222,7 @@ metadata:
218
222
  allowed_push_host: https://rubygems.org
219
223
  homepage_uri: https://src.mokhan.ca/elelem/elelem
220
224
  source_code_uri: https://src.mokhan.ca/elelem/elelem
225
+ rubygems_mfa_required: 'true'
221
226
  rdoc_options: []
222
227
  require_paths:
223
228
  - lib