Commit e808660

mo khan <mo@mokhan.ca>
2025-08-08 21:57:31
chore: add debugging and raise error on bad api response
1 parent a31d3c6
lib/elelem/agent.rb
@@ -7,30 +7,11 @@ module Elelem
     def initialize(configuration)
       @configuration = configuration
 
-      @logger = configuration.logger
-      @http = configuration.http
       @conversation = configuration.conversation
       @tools = configuration.tools
     end
 
-    def protocol(host)
-      host.match?(/\A(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?\z/) ? 'http' : 'https'
-    end
-
-    def system_message
-      <<~SYS
-        You are ChatGPT, a helpful assistant with reasoning capabilities.
-        Current date: #{Time.now.strftime('%Y-%m-%d')}.
-        System info: `uname -a` output:
-        #{`uname -a`.strip}
-        Reasoning: high
-      SYS
-    end
-
     def repl
-      puts "Ollama Agent (#{configuration.model})"
-      puts "Tools:\n  #{@tools.banner}"
-
       loop do
         print "\n> "
         user = STDIN.gets&.chomp
@@ -45,9 +26,9 @@ module Elelem
     def process_input(text)
       @conversation.add(role: 'user', content: text)
 
-      # ::TODO state machine
       done = false
       loop do
+        debug_print("Calling API...")
         call_api(@conversation.history) do |chunk|
           debug_print(chunk)
 
@@ -57,10 +38,7 @@ module Elelem
             print("\u001b[90m#{message['thinking']}\u001b[0m")
           elsif message['tool_calls']&.any?
             message['tool_calls'].each do |t|
-              @conversation.add(
-                role: 'tool',
-                content: execute_tool(t.dig('function', 'name'), t.dig('function', 'arguments'))
-              )
+              @conversation.add(role: 'tool', content: @tools.execute(t))
             end
           elsif message['content'].to_s.strip
             print message['content'].to_s.strip
@@ -82,7 +60,7 @@ module Elelem
         stream:     true,
         keep_alive: '5m',
         options:      { temperature: 0.1 },
-        tools:       tools
+        tools:       tools.to_h
       }
       json_body = body.to_json
       debug_print(json_body)
@@ -90,9 +68,12 @@ module Elelem
       req = Net::HTTP::Post.new(configuration.uri)
       req['Content-Type'] = 'application/json'
       req.body = json_body
-      req['Authorization'] = "Bearer #{@token}" if @token
+      req['Authorization'] = "Bearer #{configuration.token}" if configuration.token
+
+      configuration.http.request(req) do |response|
+        debug_print(response.inspect)
+        raise response.inspect unless response.code == "200"
 
-      http.request(req) do |response|
         response.read_body do |chunk|
           block_given? ? yield(chunk) : debug_print(chunk)
           $stdout.flush
@@ -100,26 +81,8 @@ module Elelem
       end
     end
 
-    def execute_tool(name, args)
-      case name
-      when 'execute_command'
-        result = run_cmd(args['command'])
-        debug_print(result) unless result[:ok]
-        result[:output]
-      when 'ask_user'
-        puts("\u001b[35m#{args['question']}\u001b[0m")
-        print "> "
-        "User: #{STDIN.gets&.chomp}"
-      end
-    end
-
-    def run_cmd(command)
-      stdout, stderr, status = Open3.capture3('/bin/sh', '-c', command)
-      { output: stdout + stderr, ok: status.success? }
-    end
-
     def debug_print(body = nil)
-      @logger.debug(body) if @debug && body
+      configuration.logger.debug(body) if configuration.debug && body
     end
   end
 end
lib/elelem/application.rb
@@ -7,17 +7,21 @@ module Elelem
     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: ENV.fetch('DEBUG', '0') == '1'
+    method_option :debug, aliases: '--debug', type: :boolean, desc: 'Debug mode', default: false
     def chat(*)
       if options[:help]
         invoke :help, ['chat']
       else
-        agent = Agent.new(Configuration.new(
+        configuration = Configuration.new(
           host: options[:host],
           model: options[:model],
           token: options[:token],
           debug: options[:debug],
-        ))
+        )
+        say "Ollama Agent (#{configuration.model})", :green
+        say "Tools:\n  #{configuration.tools.banner}", :green
+
+        agent = Agent.new(configuration)
         agent.repl
       end
     end
lib/elelem/configuration.rb
@@ -12,7 +12,7 @@ module Elelem
     end
 
     def http
-      Net::HTTP.new(uri.host, uri.port).tap do |h|
+      @http ||= Net::HTTP.new(uri.host, uri.port).tap do |h|
         h.read_timeout = 3_600
         h.open_timeout = 10
       end
lib/elelem/tools.rb
@@ -13,6 +13,10 @@ module Elelem
             properties: { command: { type: 'string' } },
             required:   ['command']
           }
+        },
+        handler: -> (args) {
+          stdout, stderr, _status = Open3.capture3('/bin/sh', '-c', args['command'])
+          stdout + stderr
         }
       },
       {
@@ -25,6 +29,11 @@ module Elelem
             properties: { question: { type: 'string' } },
             required:   ['question']
           }
+        },
+        handler: ->(args) {
+          puts("\u001b[35m#{args['question']}\u001b[0m")
+          print "> "
+          STDIN.gets&.chomp
         }
       }
     ]
@@ -41,5 +50,28 @@ module Elelem
         ].join(": ")
       end.sort.join("\n  ")
     end
+
+    def execute(tool_call)
+      name = tool_call.dig('function', 'name')
+      args = tool_call.dig('function', 'arguments')
+
+      tool = @tools.find do |tool|
+        tool.dig(:function, :name) == name
+      end
+      tool.fetch(:handler).call(args)
+    end
+
+    def to_h
+      @tools.map do |tool|
+        {
+          type: tool[:type],
+          function: {
+            name: tool.dig(:function, :name),
+            description: tool.dig(:function, :description),
+            parameters: tool.dig(:function, :parameters)
+          }
+        }
+      end
+    end
   end
 end