summaryrefslogtreecommitdiff
path: root/main.rb
blob: 3988c8fedaff28515bb71c2179ee44433108fd9c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#!/usr/bin/env ruby
# GPT-5 Action-Oriented Agent - pure stdlib

require "net/http"
require "json"
require "uri"

API_KEY = ENV["OPENAI_API_KEY"] or abort("Set OPENAI_API_KEY")
MODEL   = "gpt-5-mini"

# Tools GPT-5 can use
TOOLS = [
  {
    type: "function",
    function: {
      name: "read_file",
      description: "Read contents of a file",
      parameters: {
        type: "object",
        properties: { path: { type: "string" } },
        required: ["path"]
      }
    }
  },
  {
    type: "function",
    function: {
      name: "write_file",
      description: "Write contents to a file",
      parameters: {
        type: "object",
        properties: { path: { type: "string" }, content: { type: "string" } },
        required: ["path","content"]
      }
    }
  },
  {
    type: "function",
    function: {
      name: "run_command",
      description: "Run a shell command and return output",
      parameters: {
        type: "object",
        properties: { command: { type: "string" } },
        required: ["command"]
      }
    }
  }
]

# Execute tools
def run_tool(name, args)
  case name
  when "read_file"
    begin
      { content: File.read(args["path"]) }
    rescue => e
      { error: e.message }
    end
  when "write_file"
    begin
      File.write(args["path"], args["content"])
      { status: "ok" }
    rescue => e
      { error: e.message }
    end
  when "run_command"
    begin
      { output: `#{args["command"]}`.strip }
    rescue => e
      { error: e.message }
    end
  else
    { error: "unknown tool #{name}" }
  end
end

# Call OpenAI
def openai_chat(messages, tools)
  uri = URI("https://api.openai.com/v1/chat/completions")
  req = Net::HTTP::Post.new(uri)
  req["Authorization"] = "Bearer #{API_KEY}"
  req["Content-Type"] = "application/json"

  req.body = {
    model: MODEL,
    messages: messages,
    tools: tools,
    tool_choice: "auto"
  }.to_json

  res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
  raise "HTTP #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
  JSON.parse(res.body)
end

# Conversation with bias-for-action
messages = [
  { role: "system", content: <<~MSG
      You are a reasoning coding and system agent. Prioritize taking practical actions whenever possible.
      Use read_file, write_file, and run_command proactively to achieve user goals.
      Explain your reasoning briefly in content. Avoid unnecessary delays; act whenever sufficient info is available.
  MSG
  }
]

puts ">> Type instructions (or 'exit')"

loop do
  print "\nUser> "
  input = STDIN.gets&.strip
  break if input.nil? || input.downcase == "exit"
  messages << { role: "user", content: input }

  loop do
    resp = openai_chat(messages, TOOLS)
    msg = resp.dig("choices", 0, "message")
    messages << msg

    # Print reasoning content immediately
    if msg["content"] && !msg["content"].empty?
      puts "\nAssistant> #{msg['content']}"
    end

    # Execute any tool calls
    if msg["tool_calls"]
      msg["tool_calls"].each do |call|
        name = call.dig("function","name")
        args = JSON.parse(call.dig("function","arguments"))
        result = run_tool(name, args)
        messages << { role: "tool", tool_call_id: call["id"], content: JSON.dump(result) }
      end
      next # continue reasoning on tool results
    end

    break # no tool calls left, turn is done
  end
end