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