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 +4 -4
- data/lib/elelem/agent.rb +60 -54
- data/lib/elelem/cli.rb +68 -28
- data/lib/elelem/commands.rb +1 -1
- data/lib/elelem/config.rb +3 -3
- data/lib/elelem/conversation.rb +1 -0
- data/lib/elelem/input.rb +7 -9
- data/lib/elelem/output.rb +8 -10
- data/lib/elelem/plugins.rb +14 -10
- data/lib/elelem/registry.rb +20 -21
- data/lib/elelem/repl.rb +34 -0
- data/lib/elelem/result.rb +1 -0
- data/lib/elelem/schema.rb +20 -0
- data/lib/elelem/session.rb +17 -0
- data/lib/elelem/system_prompt.rb +9 -0
- data/lib/elelem/tool.rb +7 -13
- data/lib/elelem/toolbox.rb +30 -12
- data/lib/elelem/transcript.rb +26 -0
- data/lib/elelem/version.rb +1 -1
- data/lib/elelem.rb +7 -3
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d4696869850fc38fba45a65a38aca74f8663fe9b8562deb7a987cbc71058221a
|
|
4
|
+
data.tar.gz: 8ec379df1537243659cfee997c02fde40912be780ace54b14bb0ff181f5fad64
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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, :
|
|
5
|
+
attr_reader :conversation, :toolbox, :output
|
|
6
6
|
attr_accessor :provider
|
|
7
7
|
|
|
8
|
-
def initialize(provider, toolbox: Toolbox.new,
|
|
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
|
-
@
|
|
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
|
|
16
|
+
@conversation.to_a
|
|
30
17
|
end
|
|
31
18
|
|
|
32
19
|
def turn(prompt)
|
|
33
20
|
@conversation.add(role: "user", content: prompt)
|
|
34
|
-
|
|
35
|
-
content
|
|
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
|
-
|
|
39
|
-
content
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
|
56
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/elelem/commands.rb
CHANGED
|
@@ -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
|
|
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, :
|
|
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(&
|
|
15
|
-
Config.default.configure(&
|
|
14
|
+
def self.configure(&)
|
|
15
|
+
Config.default.configure(&)
|
|
16
16
|
end
|
|
17
17
|
end
|
data/lib/elelem/conversation.rb
CHANGED
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
|
|
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
|
-
|
|
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)
|
data/lib/elelem/plugins.rb
CHANGED
|
@@ -15,18 +15,22 @@ module Elelem
|
|
|
15
15
|
def load!(force: false)
|
|
16
16
|
return if @loaded && !force
|
|
17
17
|
|
|
18
|
-
@load_paths.each
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
@load_paths.each { |path| load_dir(File.expand_path(path)) }
|
|
19
|
+
@loaded = true
|
|
20
|
+
end
|
|
21
21
|
|
|
22
|
-
|
|
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
|
-
|
|
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
|
data/lib/elelem/registry.rb
CHANGED
|
@@ -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(
|
|
20
|
+
def apply(session)
|
|
21
21
|
@plugins.load!
|
|
22
|
-
@registration.setups.each_value { |block| block.call(
|
|
22
|
+
@registration.setups.each_value { |block| block.call(session) }
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def reload(
|
|
25
|
+
def reload(session)
|
|
26
26
|
@registration.clear_setups!
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
session.toolbox.clear!
|
|
28
|
+
session.commands.clear!
|
|
29
29
|
@plugins.load!(force: true)
|
|
30
|
-
apply(
|
|
30
|
+
apply(session)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def names
|
|
34
34
|
@registration.providers.keys
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
def
|
|
37
|
+
def build_session(provider, toolbox: Toolbox.new, input: nil, output: nil)
|
|
38
38
|
commands = Commands.new
|
|
39
|
-
agent = Agent.new(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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(**
|
|
47
|
+
def build_input(**)
|
|
53
48
|
@plugins.load!
|
|
54
|
-
(@registration.factories[:input] || ->(**o) { Input.new(**o) }).call(**
|
|
49
|
+
(@registration.factories[:input] || ->(**o) { Input.new(**o) }).call(**)
|
|
55
50
|
end
|
|
56
51
|
|
|
57
|
-
def build_output(**
|
|
52
|
+
def build_output(**)
|
|
58
53
|
@plugins.load!
|
|
59
|
-
(@registration.factories[:output] || ->(**o) { Output.new(**o) }).call(**
|
|
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) {
|
|
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
|
data/lib/elelem/repl.rb
ADDED
|
@@ -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
|
@@ -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
|
data/lib/elelem/system_prompt.rb
CHANGED
|
@@ -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, :
|
|
5
|
+
attr_reader :name, :description, :aliases
|
|
6
6
|
|
|
7
|
-
def initialize(name, description:,
|
|
7
|
+
def initialize(name, description:, schema: Schema.new, aliases: [], &handler)
|
|
8
8
|
@name = name
|
|
9
9
|
@description = description
|
|
10
|
-
@
|
|
11
|
-
@required = required.freeze
|
|
10
|
+
@schema = schema
|
|
12
11
|
@aliases = aliases.freeze
|
|
13
|
-
@
|
|
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
|
-
@
|
|
16
|
+
@handler.call(args)
|
|
20
17
|
end
|
|
21
18
|
|
|
22
19
|
def validate(args)
|
|
23
|
-
@schema.validate(args
|
|
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: @
|
|
29
|
+
parameters: @schema.to_h
|
|
35
30
|
}
|
|
36
31
|
}
|
|
37
32
|
end
|
|
38
|
-
|
|
39
33
|
end
|
|
40
34
|
end
|
data/lib/elelem/toolbox.rb
CHANGED
|
@@ -17,8 +17,9 @@ module Elelem
|
|
|
17
17
|
@hooks[:after].clear
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def add(name, description:, params: {}, required: [], aliases: [], &
|
|
21
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
data/lib/elelem/version.rb
CHANGED
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|