agentic examples

Gem Scout

Gem Scout: describe what you need, get a ranked shortlist of gems. Search and scoring are separate capabilities; the search backend is the pluggable seam - offline it's a bundled index, online swap in the real WebSearch DuckDuckGo backend with one assignment.

Developer Experience Round 2 Andrew Kane exit 0

source on github

bundle exec ruby examples/gem_scout.rb

a real captured run

GEM SCOUT: "background jobs"

-> sidekiq             97.4  widely adopted (950M downloads); recently released (20d ago)
   good_job            70.5  recently released (14d ago)
   solid_queue         60.2  recently released (30d ago)

recommendation: start with sidekiq - background jobs backed by Redis, threads not forks

source

# frozen_string_literal: true

# Gem Scout: describe what you need, get a ranked shortlist of gems.
# Search and scoring are separate capabilities; the search backend is
# the pluggable seam - offline it's a bundled index, online swap in
# the real WebSearch DuckDuckGo backend with one assignment.
#
#   bundle exec ruby examples/gem_scout.rb "background jobs"
#   bundle exec ruby examples/gem_scout.rb "vector search"
#
# Runs offline by default.

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

# A small index standing in for the network - same shape a live search
# backend returns, so the rest of the program can't tell the difference
CATALOG = [
  {name: class="s">"sidekiq", summary: class="s">"background jobs backed by Redis, threads not forks",
   topics: %w[background jobs queue async workers], downloads_m: 950, last_release_days: 20},
  {name: class="s">"solid_queue", summary: class="s">"database-backed background jobs for Active Job",
   topics: %w[background jobs queue rails database], downloads_m: 15, last_release_days: 30},
  {name: class="s">"good_job", summary: class="s">"Postgres-based Active Job backend with dashboard",
   topics: %w[background jobs queue rails postgres], downloads_m: 40, last_release_days: 14},
  {name: class="s">"neighbor", summary: class="s">"nearest neighbor vector search for Rails and Postgres",
   topics: %w[vector search embeddings pgvector similarity], downloads_m: 8, last_release_days: 45},
  {name: class="s">"pgvector", summary: class="s">"pgvector support for Ruby",
   topics: %w[vector search embeddings postgres], downloads_m: 12, last_release_days: 60},
  {name: class="s">"searchkick", summary: class="s">"intelligent search made easy with Elasticsearch/OpenSearch",
   topics: %w[search elasticsearch full-text ranking], downloads_m: 130, last_release_days: 90},
  {name: class="s">"pagy", summary: class="s">"the fastest pagination gem",
   topics: %w[pagination performance views], downloads_m: 85, last_release_days: 10},
  {name: class="s">"strong_migrations", summary: class="s">"catch unsafe migrations in development",
   topics: %w[migrations database safety postgres], downloads_m: 70, last_release_days: 25}
].freeze

# Offline backend for the WebSearch seam built in round 1
Agentic:class="y">:Capabilities:class="y">:WebSearch.backend = lambda do |query:, num_results:|
  terms = query.downcase.split
  hits = CATALOG.map { |gem|
    haystack = class="s">"#{gem[class="y">:name]} #{gem[class="y">:summary]} #{gem[class="y">:topics].join(" class="s">")}"
    score = terms.count { |t| haystack.include?(t) }
    [gem, score]
  }.select { |_, s| s.positive? }.sort_by { |g, s| [-s, -g[class="y">:downloads_m]] }.first(num_results)

  {
    results: hits.map { |gem, _| class="s">"#{gem[class="y">:name]}: #{gem[class="y">:summary]}" },
    sources: hits.map { |gem, _| class="s">"https://rubygems.org/gems/#{gem[class="y">:name]}" }
  }
end

# Scoring is its own capability: search finds candidates, this ranks
# them on the things that matter when you have to live with a gem
spec = Agentic:class="y">:CapabilitySpecification.new(
  name: class="s">"score_gem",
  description: class="s">"Score a gem on adoption and maintenance",
  version: class="s">"1.0.0",
  inputs: {name: {type: class="s">"string", required: true}},
  outputs: {score: {type: class="s">"number", required: true}, notes: {type: class="s">"array", required: true}}
)
Agentic.register_capability(spec, Agentic:class="y">:CapabilityProvider.new(
  capability: spec,
  implementation: ->(inputs) {
    gem = CATALOG.find { |g| g[class="y">:name] == inputs[class="y">:name] } or
      next({score: 0.0, notes: [class="s">"unknown gem"]})

    adoption = Math.log10([gem[class="y">:downloads_m], 1].max) / 3.0 # 0..1 for 1M..1B
    freshness = [1.0 - gem[class="y">:last_release_days] / 365.0, 0].max
    notes = []
    notes << class="s">"widely adopted (#{gem[class="y">:downloads_m]}M downloads)" if gem[class="y">:downloads_m] > 50
    notes << class="s">"recently released (#{gem[class="y">:last_release_days]}d ago)" if gem[class="y">:last_release_days] < 31
    notes << class="s">"check maintenance cadence" if gem[class="y">:last_release_days] > 80

    {score: ((adoption * 0.6 + freshness * 0.4) * 100).round(1), notes: notes}
  }
))

scout = Agentic:class="y">:Agent.build { |a| a.name = class="s">"GemScout" }
Agentic:class="y">:Capabilities.register_standard_capabilities
scout.add_capability(class="s">"web_search")
scout.add_capability(class="s">"score_gem")

need = ARGV.join(class="s">" ")
need = class="s">"background jobs" if need.empty?

found = scout.execute_capability(class="s">"web_search", {query: need, num_results: 4})
candidates = found[class="y">:results].map { |line| line.split(class="s">":").first }

ranked = candidates.map { |name|
  verdict = scout.execute_capability(class="s">"score_gem", {name: name})
  {name: name, score: verdict[class="y">:score], notes: verdict[class="y">:notes]}
}.sort_by { |c| -c[class="y">:score] }

puts class="s">"GEM SCOUT: \"#{need}\class="s">""
puts
if ranked.empty?
  puts class="s">"  no candidates found - try different words, or plug in the live backend:"
  puts class="s">"  Agentic:class="y">:Capabilities:class="y">:WebSearch.backend = Agentic:class="y">:Capabilities:class="y">:WebSearch:class="y">:DuckDuckGo.new"
else
  ranked.each_with_index do |c, i|
    marker = (i == 0) ? class="s">"->" : class="s">"  "
    puts format(class="s">"%s %-18s %5.1f  %s", marker, c[class="y">:name], c[class="y">:score], c[class="y">:notes].join(class="s">"; "))
  end
  puts
  winner = ranked.first
  puts class="s">"recommendation: start with #{winner[class="y">:name]} - " \
    class="s">"#{CATALOG.find { |g| g[class="y">:name] == winner[class="y">:name] }[class="y">:summary]}"
end