agentic examples

The Polite Form

The Polite Form: a contract usually speaks AFTER you fail - a 422, a stack of violations. This assistant makes it speak FIRST, turning every declaration into a question: required keys become requests, bounds become gentle corrections, and relation rules become the follow-ups a good clerk asks ("express? then I'll need a customs code"). Zero errors are ever shown; the contract is the script.

Developer Experience Round 10 Matz exit 0

source on github

bundle exec ruby examples/polite_form.rb

a real captured run

THE POLITE FORM (quote_shipping v2.1.0)

  user:      here's what I have: {"express":true,"api_key":"k-123","oauth_token":"t-456","volume":2500}
  assistant: may I have your mode? (air, sea, road)
  user:      "air"
  assistant: may I have your weight? (number)
  user:      6000
  assistant: ah - weight must be less than or equal to 5000. shall we adjust it?
  user:      4500
  assistant: together weight and volume come to 7000, and 6000 is our limit - could we lower the volume?
  user:      1500
  assistant: since you chose express, I'll also need your customs_code - what is it?
  user:      "HS-42"
  assistant: you've given me api_key and oauth_token - I only need one; which shall we keep?
  user:      :api_key

  assistant: all set. here's your form: {"express":true,"api_key":"k-123","volume":1500,"mode":"air","weight":4500,"customs_code":"HS-42"}

  the same contract that would have stacked up 422s asked six
  questions instead. nothing here was written twice: the
  requests came from required:, the correction from max:, and
  the follow-ups from the relations - requires became "then I'll
  also need", sum_lte became "could we lower it", and
  mutually_exclusive became "which shall we keep?". an error
  message is just a question you asked too late.

source

# frozen_string_literal: true

# The Polite Form: a contract usually speaks AFTER you fail - a 422,
# a stack of violations. This assistant makes it speak FIRST, turning
# every declaration into a question: required keys become requests,
# bounds become gentle corrections, and relation rules become the
# follow-ups a good clerk asks ("express? then I'll need a customs
# code"). Zero errors are ever shown; the contract is the script.
#
#   bundle exec ruby examples/polite_form.rb
#
# Runs offline; the "user" answers from a queue.

require class="s">"bundler/setup"
require class="s">"agentic"
require class="s">"json"

SPEC = Agentic:class="y">:CapabilitySpecification.new(
  name: class="s">"quote_shipping", description: class="s">"Quote a shipment", version: class="s">"2.1.0",
  inputs: {
    mode: {type: class="s">"string", required: true, enum: %w[air sea road]},
    weight: {type: class="s">"number", required: true, min: 1, max: 5_000},
    volume: {type: class="s">"number", min: 0},
    express: {type: class="s">"boolean"},
    customs_code: {type: class="s">"string"},
    api_key: {type: class="s">"string"},
    oauth_token: {type: class="s">"string"}
  },
  rules: {
    fits: {relation: class="y">:sum_lte, fields: [class="y">:weight, class="y">:volume], limit: 6_000},
    customs: {relation: class="y">:requires, fields: [class="y">:express, class="y">:customs_code]},
    one_auth: {relation: class="y">:mutually_exclusive, fields: [class="y">:api_key, class="y">:oauth_token]}
  }
)

# The half-filled form the user pasted in
answers = {express: true, api_key: class="s">"k-123", oauth_token: class="s">"t-456", volume: 2_500}

# What the user will say when asked (a queue per field)
REPLIES = {
  mode: [class="s">"air"],
  weight: [6_000, 4_500], # first too heavy, then adjusted
  volume: [1_500],        # reduced when the total is too much
  customs_code: [class="s">"HS-42"],
  keep: [class="y">:api_key]
}.transform_values(&class="y">:dup)

def say(role, line)
  puts format(class="s">"  %-10s %s", class="s">"#{role}:", line)
end

def ask(field, question, answers)
  say(class="s">"assistant", question)
  reply = REPLIES.fetch(field).shift
  say(class="s">"user", reply.inspect)
  answers[field] = reply
end

validator = Agentic:class="y">:CapabilityValidator.new(SPEC)
puts class="s">"THE POLITE FORM (#{SPEC.name} v#{SPEC.version})"
puts
say(class="s">"user", class="s">"here's what I have: #{JSON.generate(answers)}")

10.times do
  validator.validate_inputs!(answers)
  break
rescue Agentic:class="y">:Errors:class="y">:ValidationError => e
  if e.rule_violations.any?
    violation = e.rule_violations.first
    rule = SPEC.rules[violation[class="y">:rule]]
    case rule[class="y">:relation]
    when class="y">:requires
      needed = rule[class="y">:fields].drop(1).find { |f| answers[f].nil? }
      ask(needed, class="s">"since you chose #{rule[class="y">:fields].first}, I'll also need your #{needed} - what is it?", answers)
    when class="y">:sum_lte
      total = rule[class="y">:fields].sum { |f| answers[f] || 0 }
      target = rule[class="y">:fields].last
      ask(target, class="s">"together #{rule[class="y">:fields].join(" and class="s">")} come to #{total}, and #{rule[class="y">:limit]} is our limit - could we lower the #{target}?", answers)
    when class="y">:mutually_exclusive
      say(class="s">"assistant", class="s">"you've given me #{violation[class="y">:fields].join(" and class="s">")} - I only need one; which shall we keep?")
      keep = REPLIES.fetch(class="y">:keep).shift
      say(class="s">"user", keep.inspect)
      (violation[class="y">:fields] - [keep]).each { |f| answers.delete(f) }
    end
  else
    field, messages = e.violations.first
    if messages.first.include?(class="s">"missing")
      ask(field, class="s">"may I have your #{field}? (#{SPEC.inputs[field][class="y">:enum]&.join(", class="s">") || SPEC.inputs[field][class="y">:type]})", answers)
    else
      ask(field, class="s">"ah - #{field} #{messages.first}. shall we adjust it?", answers)
    end
  end
end

puts
say(class="s">"assistant", class="s">"all set. here's your form: #{JSON.generate(answers)}")
validator.validate_inputs!(answers) # the countersignature
puts
puts class="s">"  the same contract that would have stacked up 422s asked six"
puts class="s">"  questions instead. nothing here was written twice: the"
puts class="s">"  requests came from required:, the correction from max:, and"
puts class="s">"  the follow-ups from the relations - requires became \"then I'll"
puts class="s">"  also need\", sum_lte became \class="s">"could we lower it\", and"
puts class="s">"  mutually_exclusive became \"which shall we keep?\class="s">". an error"
puts class="s">"  message is just a question you asked too late."