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.

Raises:

  • (ArgumentError)


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

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

  @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.



49
50
51
52
53
54
55
56
57
# File 'lib/brute/rack/adapter.rb', line 49

def call(env)
  prompt = prompt_from(env)
  return response_for(env, 400, "No prompt provided.") if prompt.nil? || prompt.empty?

  turn = @agent.start(prompt)
  response_for(env, 200, output_of(turn))
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.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/brute/rack/adapter.rb', line 67

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

  if (query = request.GET["prompt"]) && !query.empty?
    return query
  end

  body = read_body(request)
  return nil if body.nil? || body.strip.empty?

  if json?(request.media_type)
    case data = parse_json(body)
    when ::Hash   then return PROMPT_KEYS.filter_map { |key| data[key] }.first&.to_s || body
    when ::String then return data
    end
  end

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

  body
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.



95
96
97
98
99
100
101
102
103
104
# File 'lib/brute/rack/adapter.rb', line 95

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

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