Class: Brute::Rack::Adapter

Inherits:
Object
  • Object
show all
Defined in:
lib/brute/rack/adapter.rb

Constant Summary collapse

PROMPT_KEYS =

Body/JSON keys we accept a prompt under, in priority order. prompt is canonical; message/input are common aliases.

%w[prompt message input].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(agent) ⇒ Adapter

Returns a new instance of Adapter.



40
41
42
43
44
45
46
# File 'lib/brute/rack/adapter.rb', line 40

def initialize(agent)
  unless agent.respond_to?(:start)
    raise ArgumentError, "agent must respond to #start"
  end

  @agent = agent
end

Class Method Details

.for(agent) ⇒ Object

Convenience factory so the config.ru reads run Adapter.for(agent).



36
# File 'lib/brute/rack/adapter.rb', line 36

def self.for(agent) = new(agent)

Instance Method Details

#call(env) ⇒ Object

Rack entry point. Extract the prompt, run one agent turn, render the assistant's reply back as an HTTP response. A missing prompt is a 400 (client's fault); anything the turn raises is a 500.



51
52
53
54
55
56
57
58
59
60
61
# File 'lib/brute/rack/adapter.rb', line 51

def call(env)
  prompt = prompt_from(env)
  if prompt.nil? || prompt.empty?
    response_for(env, 400, "No prompt provided.")
  else
    turn = @agent.start(prompt)
    response_for(env, 200, output_of(turn))
  end
rescue => error
  response_for(env, 500, error.message)
end

#prompt_from(env) ⇒ Object

env -> prompt string. Four ways in, most explicit first:

1. a `?prompt=` query param (never touches the body),
2. a JSON body — a known key of an object, or a bare JSON string,
3. a form-encoded `prompt=` field,
4. otherwise the raw request body IS the prompt.

Returns nil when nothing usable is present.



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
# File 'lib/brute/rack/adapter.rb', line 71

def prompt_from(env)
  request = ::Rack::Request.new(env)

  if (query = request.GET["prompt"]) && !query.empty?
    query
  else
    body = read_body(request)
    if body.nil? || body.strip.empty?
      nil
    else
      from_json = nil
      if json?(request.media_type)
        case data = parse_json(body)
        when ::Hash   then from_json = PROMPT_KEYS.filter_map { |key| data[key] }.first&.to_s || body
        when ::String then from_json = data
        end
      end

      if from_json
        from_json
      elsif request.form_data? && (field = ::Rack::Utils.parse_nested_query(body)["prompt"])
        field
      else
        body
      end
    end
  end
end

#response_for(env, status, output) ⇒ Object

output -> [status, headers, body]. Content-negotiated: JSON in (or an Accept: application/json) gets {"response": "..."} back; everything else gets text/plain. Errors ride the same path so a 500 body is shaped like a 200 body.



104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/brute/rack/adapter.rb', line 104

def response_for(env, status, output)
  text = output.to_s

  if wants_json?(env)
    if status == 200
      key = :response
    else
      key = :error
    end
    [status, {"content-type" => "application/json"}, [::JSON.generate(key => text)]]
  else
    [status, {"content-type" => "text/plain; charset=utf-8"}, [text]]
  end
end