Commit c33aa90
Changed files (11)
lib
elelem
spec
elelem
lib/elelem/plugins/anthropic.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+Elelem::Providers.register(:anthropic) do
+ Elelem::Net::Claude.anthropic(
+ model: ENV.fetch("ANTHROPIC_MODEL", "claude-opus-4-5-20250514"),
+ api_key: ENV.fetch("ANTHROPIC_API_KEY")
+ )
+end
lib/elelem/plugins/ollama.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+Elelem::Providers.register(:ollama) do
+ Elelem::Net::Ollama.new(
+ model: ENV.fetch("OLLAMA_MODEL", "gpt-oss:latest"),
+ host: ENV.fetch("OLLAMA_HOST", "localhost:11434")
+ )
+end
lib/elelem/plugins/openai.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+Elelem::Providers.register(:openai) do
+ Elelem::Net::OpenAI.new(
+ model: ENV.fetch("OPENAI_MODEL", "gpt-4o"),
+ api_key: ENV.fetch("OPENAI_API_KEY")
+ )
+end
lib/elelem/plugins/provider.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:provider) do |agent|
+ agent.commands.register("provider", description: "Switch provider", completions: -> { Elelem::Providers.names }) do |name|
+ if name.nil? || name.empty?
+ agent.terminal.say " → available: #{Elelem::Providers.names.join(", ")}"
+ else
+ agent.client = Elelem::Providers.build(name)
+ agent.terminal.say " → switched to #{name}"
+ end
+ end
+end
lib/elelem/plugins/vertex.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+Elelem::Providers.register(:vertex) do
+ Elelem::Net::Claude.vertex(
+ model: ENV.fetch("VERTEX_MODEL", "claude-opus-4-5@20251101"),
+ project: ENV.fetch("GOOGLE_CLOUD_PROJECT"),
+ region: ENV.fetch("GOOGLE_CLOUD_REGION", "us-east5")
+ )
+end
lib/elelem/agent.rb
@@ -2,8 +2,8 @@
module Elelem
class Agent
- attr_reader :conversation, :client, :toolbox, :terminal, :commands, :system_prompt
- attr_writer :terminal, :toolbox, :commands
+ attr_reader :conversation, :system_prompt
+ attr_accessor :client, :toolbox, :terminal, :commands
def initialize(client, toolbox: Toolbox.new, terminal: nil, system_prompt: nil, commands: nil)
@client = client
lib/elelem/plugins.rb
@@ -9,17 +9,22 @@ module Elelem
].freeze
def self.setup!(agent)
- load_plugins
+ load!
+ run!(agent)
+ end
+
+ def self.run!(agent)
registry.each_value { |plugin| plugin.call(agent) }
end
def self.reload!(agent)
+ Providers.registry.clear
registry.clear
- load_plugins
- registry.each_value { |plugin| plugin.call(agent) }
+ load!
+ run!(agent)
end
- def self.load_plugins
+ def self.load!
LOAD_PATHS.each do |path|
dir = File.expand_path(path)
next unless File.directory?(dir)
lib/elelem/providers.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Elelem
+ module Providers
+ def self.register(name, &factory)
+ registry[name.to_s] = factory
+ end
+
+ def self.build(name)
+ Plugins.load! if registry.empty?
+ registry.fetch(name.to_s).call
+ end
+
+ def self.names
+ registry.keys
+ end
+
+ def self.registry
+ @registry ||= {}
+ end
+ end
+end
lib/elelem.rb
@@ -26,6 +26,7 @@ require_relative "elelem/mcp"
require_relative "elelem/net"
require_relative "elelem/permissions"
require_relative "elelem/plugins"
+require_relative "elelem/providers"
require_relative "elelem/system_prompt"
require_relative "elelem/terminal"
require_relative "elelem/tool"
@@ -47,14 +48,16 @@ module Elelem
end
end
- def self.start(client, toolbox: Toolbox.new)
+ def self.start(provider: "ollama", toolbox: Toolbox.new)
+ client = Providers.build(provider)
agent = Agent.new(client, toolbox: toolbox)
Plugins.setup!(agent)
agent.terminal = Terminal.new(commands: agent.commands)
agent.repl
end
- def self.ask(client, prompt, toolbox: Toolbox.new)
+ def self.ask(prompt, provider: "ollama", toolbox: Toolbox.new)
+ client = Providers.build(provider)
agent = Agent.new(client, toolbox: toolbox, terminal: Terminal.new(quiet: true))
Plugins.setup!(agent)
agent.turn(prompt)
@@ -62,23 +65,8 @@ module Elelem
end
class CLI
- MODELS = {
- "ollama" => "gpt-oss:latest",
- "anthropic" => "claude-opus-4-5-20250514",
- "vertex" => "claude-opus-4-5@20251101",
- "openai" => "gpt-4o"
- }.freeze
-
- PROVIDERS = {
- "ollama" => ->(model) { Elelem::Net::Ollama.new(model: model, host: ENV.fetch("OLLAMA_HOST", "localhost:11434")) },
- "anthropic" => ->(model) { Elelem::Net::Claude.anthropic(model: model, api_key: ENV.fetch("ANTHROPIC_API_KEY")) },
- "vertex" => ->(model) { Elelem::Net::Claude.vertex(model: model, project: ENV.fetch("GOOGLE_CLOUD_PROJECT"), region: ENV.fetch("GOOGLE_CLOUD_REGION", "us-east5")) },
- "openai" => ->(model) { Elelem::Net::OpenAI.new(model: model, api_key: ENV.fetch("OPENAI_API_KEY")) }
- }.freeze
-
def initialize(args)
@provider = "ollama"
- @model = nil
@args = parse(args)
end
@@ -101,7 +89,6 @@ module Elelem
o.separator " help Show this help"
o.separator "\nOptions:"
o.on("-p", "--provider NAME", "ollama, anthropic, vertex, openai") { |p| @provider = p }
- o.on("-m", "--model NAME", "Override default model") { |m| @model = m }
o.on("-h", "--help") { puts o; exit }
end
@parser.parse!(args)
@@ -111,20 +98,15 @@ module Elelem
puts @parser
end
- def client
- model = @model || MODELS.fetch(@provider)
- PROVIDERS.fetch(@provider).call(model)
- end
-
def chat
- Elelem.start(client)
+ Elelem.start(provider: @provider)
end
def ask
abort "Usage: elelem ask <prompt>" if @args.empty?
prompt = @args.join(" ")
prompt = "#{prompt}\n\n```\n#{$stdin.read}\n```" if $stdin.stat.pipe?
- Elelem::Terminal.new.markdown Elelem.ask(client, prompt)
+ Elelem::Terminal.new.markdown Elelem.ask(prompt, provider: @provider)
end
def files
spec/elelem/providers_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+RSpec.describe Elelem::Providers do
+ before do
+ described_class.registry.clear
+ end
+
+ after do
+ described_class.registry.clear
+ end
+
+ describe ".register" do
+ it "registers a provider factory" do
+ client = double("client")
+ described_class.register(:test) { client }
+
+ expect(described_class.build("test")).to eq(client)
+ end
+
+ it "accepts symbol or string names" do
+ client = double("client")
+ described_class.register("string_provider") { client }
+
+ expect(described_class.build(:string_provider)).to eq(client)
+ end
+ end
+
+ describe ".build" do
+ it "calls the factory and returns the client" do
+ call_count = 0
+ described_class.register(:counter) { call_count += 1 }
+
+ described_class.build("counter")
+ described_class.build("counter")
+
+ expect(call_count).to eq(2)
+ end
+
+ it "raises KeyError for unknown provider" do
+ expect { described_class.build("unknown") }.to raise_error(KeyError)
+ end
+ end
+
+ describe ".names" do
+ it "returns registered provider names" do
+ described_class.register(:alpha) { }
+ described_class.register(:beta) { }
+
+ expect(described_class.names).to contain_exactly("alpha", "beta")
+ end
+ end
+end
Gemfile.lock
@@ -45,16 +45,20 @@ GEM
regexp_parser (~> 2.0)
simpleidn (~> 0.2)
logger (1.7.0)
- net-hippie (1.4.0)
+ monitor (0.2.0)
+ net-hippie (1.5.1)
base64 (~> 0.1)
json (~> 2.0)
logger (~> 1.0)
- net-http (~> 0.6)
- openssl (~> 3.0)
+ monitor (~> 0.1)
+ net-http (~> 0.1)
+ openssl (~> 4.0)
+ resolv (~> 0.1)
+ timeout (~> 0.1)
net-http (0.9.1)
uri (>= 0.11.1)
open3 (0.2.1)
- openssl (3.3.2)
+ openssl (4.0.0)
optparse (0.8.1)
pathname (0.4.0)
pp (0.6.3)
@@ -71,6 +75,7 @@ GEM
regexp_parser (2.11.3)
reline (0.6.3)
io-console (~> 0.5)
+ resolv (0.7.1)
rspec (3.13.2)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
@@ -89,6 +94,7 @@ GEM
simpleidn (0.2.3)
stringio (3.2.0)
tempfile (0.3.1)
+ timeout (0.6.0)
tsort (0.2.0)
uri (1.1.1)
webrick (1.9.2)