Commit 13e3365
Changed files (6)
lib/elelem/agent.rb
@@ -2,12 +2,17 @@
module Elelem
class Agent
- attr_reader :conversation, :client, :toolbox
+ PROVIDERS = %w[ollama anthropic openai vertex-ai].freeze
+ ANTHROPIC_MODELS = %w[claude-sonnet-4-20250514 claude-opus-4-20250514 claude-haiku-3-5-20241022].freeze
+ VERTEX_MODELS = %w[claude-sonnet-4@20250514 claude-opus-4-5@20251101].freeze
- def initialize(client, toolbox)
+ attr_reader :conversation, :client, :toolbox, :provider
+
+ def initialize(provider, model, toolbox)
@conversation = Conversation.new
- @client = client
+ @provider = provider
@toolbox = toolbox
+ @client = build_client(provider, model)
end
def repl
@@ -31,6 +36,7 @@ module Elelem
mode = Set[:read, :execute]
puts " → Mode: verify (read + execute)"
when "/mode"
+ puts " Provider: #{provider}/#{client.model}"
puts " Mode: #{mode.to_a.inspect}"
puts " Tools: #{toolbox.tools_for(mode).map { |t| t.dig(:function, :name) }}"
when "/exit" then exit
@@ -38,6 +44,34 @@ module Elelem
conversation.clear
puts " → Conversation cleared"
when "/context" then puts conversation.dump(mode)
+ when "/provider"
+ CLI::UI::Prompt.ask("Provider?") do |handler|
+ PROVIDERS.each do |name|
+ handler.option(name) do |selected_provider|
+ models = models_for(selected_provider)
+ if models.empty?
+ puts " ✗ No models available for #{selected_provider}"
+ else
+ CLI::UI::Prompt.ask("Model?") do |h|
+ models.each do |model|
+ h.option(model) { |m| switch_client(selected_provider, m) }
+ end
+ end
+ end
+ end
+ end
+ end
+ when "/model"
+ models = models_for(provider)
+ if models.empty?
+ puts " ✗ No models available for #{provider}"
+ else
+ CLI::UI::Prompt.ask("Model?") do |handler|
+ models.each do |model|
+ handler.option(model) { |m| switch_model(m) }
+ end
+ end
+ end
else
puts help_banner
end
@@ -58,6 +92,8 @@ module Elelem
def help_banner
<<~HELP
/mode auto build plan verify
+ /provider
+ /model
/clear
/context
/exit
@@ -65,6 +101,53 @@ module Elelem
HELP
end
+ def build_client(provider_name, model = nil)
+ model_opts = model ? { model: model } : {}
+
+ case provider_name
+ when "ollama" then Net::Llm::Ollama.new(**model_opts)
+ when "anthropic" then Net::Llm::Anthropic.new(**model_opts)
+ when "openai" then Net::Llm::OpenAI.new(**model_opts)
+ when "vertex-ai" then Net::Llm::VertexAI.new(**model_opts)
+ else
+ raise Error, "Unknown provider: #{provider_name}"
+ end
+ end
+
+ def models_for(provider_name)
+ case provider_name
+ when "ollama"
+ client_for_models = provider_name == provider ? client : build_client(provider_name)
+ client_for_models.tags["models"]&.map { |m| m["name"] } || []
+ when "openai"
+ client_for_models = provider_name == provider ? client : build_client(provider_name)
+ client_for_models.models["data"]&.map { |m| m["id"] } || []
+ when "anthropic"
+ ANTHROPIC_MODELS
+ when "vertex-ai"
+ VERTEX_MODELS
+ else
+ []
+ end
+ rescue KeyError => e
+ puts " ⚠ Missing credentials: #{e.message}"
+ []
+ rescue => e
+ puts " ⚠ Could not fetch models: #{e.message}"
+ []
+ end
+
+ def switch_client(new_provider, model)
+ @provider = new_provider
+ @client = build_client(new_provider, model)
+ puts " → Switched to #{new_provider}/#{client.model}"
+ end
+
+ def switch_model(model)
+ @client = build_client(provider, model)
+ puts " → Switched to #{provider}/#{client.model}"
+ end
+
def format_tool_call_result(result)
return if result.nil?
return result["stdout"] if result["stdout"]
@@ -97,15 +180,20 @@ module Elelem
tool_calls = []
print "Thinking> "
- client.fetch(messages + turn_context, tools) do |chunk|
- case chunk[:type]
- when :delta
- print chunk[:thinking] if chunk[:thinking]
- content += chunk[:content] if chunk[:content]
- when :complete
- content = chunk[:content] if chunk[:content]
- tool_calls = chunk[:tool_calls] || []
+ begin
+ client.fetch(messages + turn_context, tools) do |chunk|
+ case chunk[:type]
+ when :delta
+ print chunk[:thinking] if chunk[:thinking]
+ content += chunk[:content] if chunk[:content]
+ when :complete
+ content = chunk[:content] if chunk[:content]
+ tool_calls = chunk[:tool_calls] || []
+ end
end
+ rescue => e
+ puts "\n ✗ API Error: #{e.message}"
+ return { role: "assistant", content: "[Error: #{e.message}]" }
end
puts "\nAssistant> #{content}" unless content.to_s.empty?
lib/elelem/application.rb
@@ -15,27 +15,13 @@ module Elelem
type: :string,
desc: "Model name (uses provider default if not specified)"
def chat(*)
- client = build_client
- say "Agent (#{options[:provider]}/#{client.model})", :green
- agent = Agent.new(client, Toolbox.new)
+ provider = options[:provider]
+ model = options[:model]
+ say "Agent (#{provider})", :green
+ agent = Agent.new(provider, model, Toolbox.new)
agent.repl
end
- private
-
- def build_client
- model_opts = options[:model] ? { model: options[:model] } : {}
-
- case options[:provider]
- when "ollama" then Net::Llm::Ollama.new(**model_opts)
- when "anthropic" then Net::Llm::Anthropic.new(**model_opts)
- when "openai" then Net::Llm::OpenAI.new(**model_opts)
- when "vertex-ai" then Net::Llm::VertexAI.new(**model_opts)
- else
- raise Error, "Unknown provider: #{options[:provider]}. Use: #{PROVIDERS.join(', ')}"
- end
- end
-
desc "files", "Generate CXML of the files"
def files
puts '<documents>'
lib/elelem.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require "cli/ui"
require "erb"
require "fileutils"
require "json"
spec/elelem/agent_spec.rb
@@ -1,8 +1,15 @@
# frozen_string_literal: true
RSpec.describe Elelem::Agent do
- let(:mock_client) { double("client") }
- let(:agent) { described_class.new(mock_client, Elelem::Toolbox.new) }
+ let(:mock_client) { double("client", model: "test-model") }
+ let(:agent) do
+ agent = described_class.allocate
+ agent.instance_variable_set(:@conversation, Elelem::Conversation.new)
+ agent.instance_variable_set(:@provider, "ollama")
+ agent.instance_variable_set(:@toolbox, Elelem::Toolbox.new)
+ agent.instance_variable_set(:@client, mock_client)
+ agent
+ end
describe "#initialize" do
it "creates a new conversation" do
elelem.gemspec
@@ -38,6 +38,7 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
+ spec.add_dependency "cli-ui", "~> 2.0"
spec.add_dependency "erb", "~> 6.0"
spec.add_dependency "fileutils", "~> 1.0"
spec.add_dependency "json", "~> 2.0"
Gemfile.lock
@@ -2,6 +2,7 @@ PATH
remote: .
specs:
elelem (0.5.0)
+ cli-ui (~> 2.0)
erb (~> 6.0)
fileutils (~> 1.0)
json (~> 2.0)
@@ -22,6 +23,7 @@ GEM
public_suffix (>= 2.0.2, < 7.0)
base64 (0.3.0)
bigdecimal (3.2.2)
+ cli-ui (2.7.0)
date (3.4.1)
diff-lcs (1.6.2)
erb (6.0.1)