Commit f8c4ca7
Changed files (7)
exe
exe/elelem
@@ -3,7 +3,7 @@
require "elelem"
-Signal.trap('INT') do
+Signal.trap("INT") do
exit(1)
end
lib/elelem/agent.rb
@@ -13,17 +13,18 @@ module Elelem
def repl
loop do
print "\n> "
- user = STDIN.gets&.chomp
- break if user.nil? || user.empty? || user == 'exit'
+ user = $stdin.gets&.chomp
+ break if user.nil? || user.empty? || user == "exit"
+
process_input(user)
- puts("\u001b[32mDone!\u001b[0m")
+ puts "\u001b[32mDone!\u001b[0m"
end
end
private
def process_input(text)
- conversation.add(role: 'user', content: text)
+ conversation.add(role: "user", content: text)
done = false
loop do
@@ -31,18 +32,25 @@ module Elelem
debug_print(chunk)
response = JSON.parse(chunk)
- done = response['done']
- message = response['message'] || {}
-
- if message['thinking']
- print("\u001b[90m#{message['thinking']}\u001b[0m")
- elsif message['tool_calls']&.any?
- message['tool_calls'].each do |t|
- conversation.add(role: 'tool', content: tools.execute(t))
+ done = response["done"]
+ message = response["message"] || {}
+
+ if message["thinking"]
+ print message["thinking"]
+ $stdout.flush
+ elsif message["tool_calls"]&.any?
+ puts
+ message["tool_calls"].each do |t|
+ command = extract_command_from_tool_call(t)
+ puts "Running: #{command}"
+ tool_output = tools.execute(t)
+ puts tool_output
+ conversation.add(role: "tool", content: tool_output)
end
done = false
- elsif message['content'].to_s.strip
- print message['content'].to_s.strip
+ elsif message["content"].to_s.strip
+ print message["content"]
+ $stdout.flush
else
raise chunk.inspect
end
@@ -50,24 +58,26 @@ module Elelem
break if done
end
+
+ puts
end
def call_api(messages)
body = {
- messages: messages,
- model: configuration.model,
- stream: true,
- keep_alive: '5m',
- options: { temperature: 0.1 },
- tools: tools.to_h
+ messages: messages,
+ model: configuration.model,
+ stream: true,
+ keep_alive: "5m",
+ options: { temperature: 0.1 },
+ tools: tools.to_h
}
json_body = body.to_json
debug_print(json_body)
req = Net::HTTP::Post.new(configuration.uri)
- req['Content-Type'] = 'application/json'
+ req["Content-Type"] = "application/json"
req.body = json_body
- req['Authorization'] = "Bearer #{configuration.token}" if configuration.token
+ req["Authorization"] = "Bearer #{configuration.token}" if configuration.token
configuration.http.request(req) do |response|
raise response.inspect unless response.code == "200"
@@ -83,5 +93,19 @@ module Elelem
def debug_print(body = nil)
configuration.logger.debug(body) if configuration.debug && body
end
+
+ def extract_command_from_tool_call(tool_call)
+ function_name = tool_call.dig("function", "name")
+ args = tool_call.dig("function", "arguments")
+
+ case function_name
+ when "execute_command"
+ args["command"]
+ when "ask_user"
+ "ask user: #{args["question"]}"
+ else
+ function_name
+ end
+ end
end
end
lib/elelem/application.rb
@@ -2,21 +2,24 @@
module Elelem
class Application < Thor
- desc 'chat', 'Start the REPL'
- method_option :help, aliases: '-h', type: :boolean, desc: 'Display usage information'
- method_option :host, aliases: '--host', type: :string, desc: 'Ollama host', default: ENV.fetch('OLLAMA_HOST', 'localhost:11434')
- method_option :model, aliases: '--model', type: :string, desc: 'Ollama model', default: ENV.fetch('OLLAMA_MODEL', 'gpt-oss')
- method_option :token, aliases: '--token', type: :string, desc: 'Ollama token', default: ENV.fetch('OLLAMA_API_KEY', nil)
- method_option :debug, aliases: '--debug', type: :boolean, desc: 'Debug mode', default: false
+ desc "chat", "Start the REPL"
+ method_option :help, aliases: "-h", type: :boolean, desc: "Display usage information"
+ method_option :host, aliases: "--host", type: :string, desc: "Ollama host",
+ default: ENV.fetch("OLLAMA_HOST", "localhost:11434")
+ method_option :model, aliases: "--model", type: :string, desc: "Ollama model",
+ default: ENV.fetch("OLLAMA_MODEL", "gpt-oss")
+ method_option :token, aliases: "--token", type: :string, desc: "Ollama token",
+ default: ENV.fetch("OLLAMA_API_KEY", nil)
+ method_option :debug, aliases: "--debug", type: :boolean, desc: "Debug mode", default: false
def chat(*)
if options[:help]
- invoke :help, ['chat']
+ invoke :help, ["chat"]
else
configuration = Configuration.new(
host: options[:host],
model: options[:model],
token: options[:token],
- debug: options[:debug],
+ debug: options[:debug]
)
say "Ollama Agent (#{configuration.model})", :green
say "Tools:\n #{configuration.tools.banner}", :green
@@ -26,7 +29,7 @@ module Elelem
end
end
- desc 'version', 'spandx version'
+ desc "version", "spandx version"
def version
puts "v#{Spandx::VERSION}"
end
lib/elelem/configuration.rb
@@ -19,10 +19,8 @@ module Elelem
end
def logger
- @logger ||= begin
- Logger.new(debug ? $stderr : "/dev/null").tap do |logger|
- logger.formatter = ->(_, _, _, msg) { msg }
- end
+ @logger ||= Logger.new(debug ? $stderr : "/dev/null").tap do |logger|
+ logger.formatter = ->(_, _, _, msg) { msg }
end
end
@@ -41,7 +39,7 @@ module Elelem
private
def scheme
- host.match?(/\A(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?\z/) ? 'http' : 'https'
+ host.match?(/\A(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?\z/) ? "http" : "https"
end
end
end
lib/elelem/conversation.rb
@@ -4,14 +4,14 @@ module Elelem
class Conversation
SYSTEM_MESSAGE = <<~SYS
You are ChatGPT, a helpful assistant with reasoning capabilities.
- Current date: #{Time.now.strftime('%Y-%m-%d')}.
+ Current date: #{Time.now.strftime("%Y-%m-%d")}.
System info: `uname -a` output: #{`uname -a`.strip}
Reasoning: high
SYS
- ROLES = ['system', 'user', 'tool'].freeze
+ ROLES = %w[system user tool].freeze
- def initialize(items = [{ role: 'system', content: SYSTEM_MESSAGE }])
+ def initialize(items = [{ role: "system", content: SYSTEM_MESSAGE }])
@items = items
end
@@ -20,7 +20,7 @@ module Elelem
end
# :TODO truncate conversation history
- def add(role: 'user', content: '')
+ def add(role: "user", content: "")
raise "unknown role: #{role}" unless ROLES.include?(role)
@items << { role: role, content: content }
lib/elelem/tools.rb
@@ -4,36 +4,36 @@ module Elelem
class Tools
DEFAULT_TOOLS = [
{
- type: 'function',
+ type: "function",
function: {
- name: 'execute_command',
- description: 'Execute a shell command.',
+ name: "execute_command",
+ description: "Execute a shell command.",
parameters: {
- type: 'object',
- properties: { command: { type: 'string' } },
- required: ['command']
+ type: "object",
+ properties: { command: { type: "string" } },
+ required: ["command"]
}
},
- handler: -> (args) {
- stdout, stderr, _status = Open3.capture3('/bin/sh', '-c', args['command'])
+ handler: lambda { |args|
+ stdout, stderr, _status = Open3.capture3("/bin/sh", "-c", args["command"])
stdout + stderr
}
},
{
- type: 'function',
+ type: "function",
function: {
- name: 'ask_user',
- description: 'Ask the user to answer a question.',
+ name: "ask_user",
+ description: "Ask the user to answer a question.",
parameters: {
- type: 'object',
- properties: { question: { type: 'string' } },
- required: ['question']
+ type: "object",
+ properties: { question: { type: "string" } },
+ required: ["question"]
}
},
- handler: ->(args) {
- puts("\u001b[35m#{args['question']}\u001b[0m")
+ handler: lambda { |args|
+ puts("\u001b[35m#{args["question"]}\u001b[0m")
print "> "
- STDIN.gets&.chomp
+ $stdin.gets&.chomp
}
}
]
@@ -52,8 +52,8 @@ module Elelem
end
def execute(tool_call)
- name = tool_call.dig('function', 'name')
- args = tool_call.dig('function', 'arguments')
+ name = tool_call.dig("function", "name")
+ args = tool_call.dig("function", "arguments")
tool = @tools.find do |tool|
tool.dig(:function, :name) == name
elelem.gemspec
@@ -33,10 +33,10 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
- spec.add_dependency 'json'
- spec.add_dependency 'logger'
- spec.add_dependency 'net-http'
- spec.add_dependency 'open3'
- spec.add_dependency 'thor'
- spec.add_dependency 'uri'
+ spec.add_dependency "json"
+ spec.add_dependency "logger"
+ spec.add_dependency "net-http"
+ spec.add_dependency "open3"
+ spec.add_dependency "thor"
+ spec.add_dependency "uri"
end