gguf-provider
  1# frozen_string_literal: true
  2
  3require "base64"
  4require "date"
  5require "digest"
  6require "erb"
  7require "fileutils"
  8require "json"
  9require "json_schemer"
 10require "logger"
 11require "net/hippie"
 12require "open3"
 13require "optparse"
 14require "pathname"
 15require "reline"
 16require "securerandom"
 17require "shellwords"
 18require "stringio"
 19require "tempfile"
 20require "uri"
 21require "webrick"
 22
 23require_relative "elelem/agent"
 24require_relative "elelem/commands"
 25require_relative "elelem/conversation"
 26require_relative "elelem/mcp"
 27require_relative "elelem/net"
 28require_relative "elelem/permissions"
 29require_relative "elelem/plugins"
 30require_relative "elelem/providers"
 31require_relative "elelem/server"
 32require_relative "elelem/system_prompt"
 33require_relative "elelem/terminal"
 34require_relative "elelem/tool"
 35require_relative "elelem/toolbox"
 36require_relative "elelem/version"
 37require_relative "elelem/web_terminal"
 38
 39module Elelem
 40  def self.logger
 41    @logger ||= begin
 42      FileUtils.mkdir_p("./elelem")
 43      Logger.new("./elelem/current.log").tap do |log|
 44        log.level = Logger.const_get(ENV.fetch("LOG_LEVEL", "warn").upcase)
 45      end
 46    end
 47  end
 48
 49  OUTPUT_LIMIT = 10_000
 50
 51  def self.sh(cmd, args: [], cwd: Dir.pwd, env: {}, timeout: nil)
 52    output = StringIO.new
 53    options = { chdir: cwd }
 54    options[:pgroup] = true if timeout
 55
 56    Open3.popen2e(env, cmd, *args, **options) do |stdin, out, wait_thr|
 57      stdin.close
 58      timed_out = false
 59      watchdog = timeout && Thread.new do
 60        sleep(timeout)
 61        timed_out = true
 62        terminate(wait_thr.pid)
 63      end
 64
 65      out.each_line do |line|
 66        yield line if block_given?
 67        output.write(line)
 68      end
 69      timed_out ? watchdog&.join : watchdog&.kill
 70
 71      status = wait_thr.value
 72      note = timed_out ? "\n[command timed out after #{timeout}s]" : ""
 73      { exit_status: status.exitstatus || (timed_out ? 124 : 1), content: truncate(output.string) + note }
 74    end
 75  end
 76
 77  def self.truncate(content, limit: OUTPUT_LIMIT)
 78    return content if content.length <= limit
 79
 80    half = limit / 2
 81    elided = content.length - limit
 82    "#{content[0, half]}\n[#{elided} characters elided]\n#{content[-half, half]}"
 83  end
 84
 85  def self.terminate(pid)
 86    Process.kill("TERM", -pid)
 87    sleep 0.5
 88    Process.kill("KILL", -pid)
 89  rescue Errno::ESRCH
 90    nil
 91  end
 92
 93  def self.command_timeout
 94    value = ENV["ELELEM_CMD_TIMEOUT"]
 95    value && !value.empty? ? Integer(value) : nil
 96  end
 97
 98  def self.start(provider: "ollama", toolbox: Toolbox.new)
 99    client = Providers.build(provider)
100    agent = Agent.new(client, toolbox: toolbox)
101    Plugins.setup!(agent)
102    agent.terminal = Terminal.new(commands: agent.commands)
103    agent.repl
104  end
105
106  def self.serve(provider: "ollama", port: 4567, toolbox: Toolbox.new)
107    client = Providers.build(provider)
108    agent = Agent.new(client, toolbox: toolbox, terminal: WebTerminal.new)
109    Plugins.setup!(agent)
110    Server.new(agent, port: port).start
111  end
112
113  def self.ask(prompt, provider: "ollama", toolbox: Toolbox.new)
114    client = Providers.build(provider)
115    agent = Agent.new(client, toolbox: toolbox, terminal: Terminal.new(quiet: true))
116    Plugins.setup!(agent)
117    agent.turn(prompt)
118    agent.conversation.last[:content]
119  end
120end