Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generating generators #91

Merged
merged 4 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions lib/sublayer/cli.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,56 @@
require "thor"

require "sublayer"
require "sublayer/version"
require "yaml"
require "fileutils"
require "active_support/inflector"

require_relative "cli/commands/subcommand_base"
require_relative "cli/commands/new_project"
require_relative "cli/commands/generate"

module Sublayer
class CLI < Thor

register(Sublayer::Commands::NewProject, "new", "new PROJECT_NAME", "Creates a new Sublayer project")

desc "generate", "Generate Sublayer Actions, Generators, and Agents with an LLM"
subcommand "generate", Sublayer::Commands::Generate

desc "version", "Prints the Sublayer version"
def version
puts Sublayer::VERSION
end

desc "help [COMMAND]", "Describe available commands or one specific command"
def help(command = nil, subcommand = false)
if command.nil?
puts "Sublayer CLI"
puts
puts "Usage:"
puts " sublayer COMMAND [OPTIONS]"
puts
puts "Commands:"
print_commands(self.class.commands.reject { |name, _| name == "help" || name == "version" })
puts
print_commands(self.class.commands.select { |name, _| name == "help" })
print_commands(self.class.commands.select { |name, _| name == "version" })
puts
puts "Run 'sublayer COMMAND --help' for more information on a command."
else
super
end
end

default_command :help

private

def print_commands(commands)
commands.each do |name, command|
puts " #{name.ljust(15)} # #{command.description}"
end
end
end
end
9 changes: 9 additions & 0 deletions lib/sublayer/cli/commands/generate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require_relative "./generator"

module Sublayer
module Commands
class Generate < SubCommandBase
register(Sublayer::Commands::Generator, "generator", "generator", "Generates a new Sublayer::Generator subclass for your project")
end
end
end
68 changes: 68 additions & 0 deletions lib/sublayer/cli/commands/generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
require_relative "generators/sublayer_generator_generator"

module Sublayer
module Commands
class Generator < Thor::Group
include Thor::Actions

class_option :description, type: :string, desc: "Description of the generator you want to generate", aliases: :d
class_option :provider, type: :string, desc: "AI provider (OpenAI, Claude, or Gemini)", aliases: :p
class_option :model, type: :string, desc: "AI model name to use (e.g. gpt-4o, claude-3-haiku-20240307, gemini-1.5-flash-latest)", aliases: :m

def confirm_usage_of_ai_api
puts "You are about to generate a new generator that uses an AI API to generate content."
puts "Please ensure you have the necessary API keys and that you are aware of the costs associated with using the API."
exit unless yes?("Do you want to continue?")
end

def determine_available_providers
@available_providers = []

@available_providers << "OpenAI" if ENV["OPENAI_API_KEY"]
@available_providers << "Claude" if ENV["ANTHROPIC_API_KEY"]
@available_providers << "Gemini" if ENV["GEMINI_API_KEY"]
end

def ask_for_generator_details
@description = options[:description] || ask("Enter a description for the Sublayer Generator you'd like to create:")
@ai_provider = options[:provider] || ask("Select an AI provider:", default: "OpenAI", limited_to: @available_providers)
@ai_model = options[:model] || select_ai_model
end

def generate_generator
Sublayer.configuration.ai_provider = Object.const_get("Sublayer::Providers::#{@ai_provider}")
Sublayer.configuration.ai_model = @ai_model

say "Generating Sublayer Generator..."
@results = SublayerGeneratorGenerator.new(description: @description).generate
end

def determine_destination_folder
# Find either a ./generators folder or a generators folder nested one level below ./lib
@destination_folder = if File.directory?("./generators")
"./generators"
elsif Dir.glob("./lib/**/generators").any?
Dir.glob("./lib/**/generators").first
else
"./"
end
end

def save_generator_to_destination_folder
create_file File.join(@destination_folder, @results.filename), @results.code
end

private
def select_ai_model
case @ai_provider
when "OpenAI"
ask("Which OpenAI model would you like to use?", default: "gpt-4o")
when "Claude"
ask("Which Anthropic model would you like to use?", default: "claude-3-5-sonnet-20240620")
when "Gemini"
ask("Which Google model would you like to use?", default: "gemini-1.5-flash-latest")
end
end
end
end
end
26 changes: 26 additions & 0 deletions lib/sublayer/cli/commands/generators/example_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class CodeFromDescriptionGenerator < Sublayer::Generators::Base
llm_output_adapter type: :single_string,
name: "generated_code",
description: "The generated code in the requested language"

def initialize(description:, technologies:)
@description = description
@technologies = technologies
end

def generate
super
end

def prompt
<<-PROMPT
You are an expert programmer in #{@technologies.join(", ")}.

You are tasked with writing code using the following technologies: #{@technologies.join(", ")}.

The description of the task is #{@description}

Take a deep breath and think step by step before you start coding.
PROMPT
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
class SublayerGeneratorGenerator < Sublayer::Generators::Base
llm_output_adapter type: :named_strings,
name: "sublayer_generator",
description: "The new Sublayer generator code based on the description",
attributes: [
{ name: "code", description: "The code of the generated Sublayer generator" },
{ name: "filename", description: "The filename of the generated Sublayer generator camel cased and with a .rb extension" }
]

def initialize(description:)
@description = description
end

def generate
super
end

def prompt
<<-PROMPT
You are an expert ruby programmer and and great at repurposing code examples to use for new situations.

A Sublayer generator is a class that acts as an abstraction for sending a request to an LLM and getting structured data back.

An example Sublayer generator is:
<example_generator>
#{example_generator}
</example_generator>

And it is the generator we're currently using for taking in a description from a user and generating a new Sublayer generator.

All Sublayer::Generators inherit from Sublayer::Generators::Base and have a generate method that simply calls super for the user to use and modify for their uses.

the llm_output_adapter directive is used to instruct the LLM on what structure of output to generate. In the example we're using type: :single_string which takes a name and description as arguments.

the other available options and their example usage is:
llm_output_adapter type: :list_of_strings,
name: "suggestions",
description: "List of keyword suggestions"

llm_output_adapter type: :single_integer,
name: "four_digit_passcode",
description: "an uncommon and difficult to guess four digit passcode"

llm_output_adapter type: :list_of_named_strings,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gonna open an issue for this.

one idea here is that we can create a very flexible output adapter that is as amorphic as (direct translation of) the json shema (essential takes type as a named param and can do so recursively)

it could serve as both a fallback adapter as well as a simple adapter for this kind of use case (prompts)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name: "review_summaries",
description: "List of movie reviews",
item_name: "review",
attributes: [
{ name: "movie_title", description: "The title of the movie" },
{ name: "reviewer_name", description: "The name of the reviewer" },
{ name: "rating", description: "The rating given by the reviewer (out of 5 stars)" },
{ name: "brief_comment", description: "A brief summary of the movie" }
]

llm_output_adapter type: :named_strings,
name: "product_description",
description: "Generate product descriptions",
attributes: [
{ name: "short_description", description: "A brief one-sentence description of the product" },
{ name: "long_description", description: "A detailed paragraph describing the product" },
{ name: "key_features", description: "A comma-separated list of key product features" },
{ name: "target_audience", description: "A brief description of the target audience for this product" }
]

# Where :available_routes is a method that returns an array of available routes
llm_output_adapter type: :string_selection_from_list,
name: "route",
description: "A route selected from the list",
options: :available_routes

# Where @sentiment_options is an array of sentiment values passed in to the initializer
llm_output_adapter type: :string_selection_from_list,
name: "sentiment_value",
description: "A sentiment value from the list",
options: -> { @sentiment_options }

Besides that, a Sublayer::Generator also has a prompt method that describes the task to the LLM and provides any necessary context for the generation which
is either passed in through the initializer or accessed in the prompt itself.

You have a description provided by the user to generate a new sublayer generator.

You are tasked with creating a new sublayer generator according to the given description.

Consider the details and requirements mentioned in the description to create the appropriate Sublayer::Generator.
<users_description>
#{@description}
</users_description>

Take a deep breath and reflect on each aspect of the description before proceeding with the generation.
PROMPT
end

def example_generator
File.read(File.join(File.dirname(__FILE__), "example_generator.rb"))
end
end
13 changes: 13 additions & 0 deletions lib/sublayer/cli/commands/subcommand_base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Sublayer
module Commands
class SubCommandBase < Thor
def self.banner(command, namespace = nil, subcommand = false)
"#{basename} #{subcommand_prefix} #{command.usage}"
end

def self.subcommand_prefix
self.name.gsub(%r{.*::}, '').gsub(%r{^[A-Z]}) { |match| match[0].downcase }.gsub(%r{[A-Z]}) { |match| "-#{match[0].downcase}" }
end
end
end
end
65 changes: 65 additions & 0 deletions spec/cli/generators/generator_generator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
require "spec_helper"

require_relative "../../../lib/sublayer/cli/commands/generators/sublayer_generator_generator"

RSpec.describe SublayerGeneratorGenerator do
def generate(description)
described_class.new(description: description).generate
end

context "Claude" do
before do
Sublayer.configuration.ai_provider = Sublayer::Providers::Claude
Sublayer.configuration.ai_model = "claude-3-haiku-20240307"
end

it "generates the Sublayer Generator code and filename" do
VCR.use_cassette("claude/cli/generators/sublayer_generator_generator") do
results = generate("a sublayer generator that takes in code and converts it to the users requested programming language")

expect(results.filename).to be_a(String)
expect(results.filename.length).to be > 0
expect(results.code).to be_a(String)
expect(results.code.length).to be > 0
end
end
end

context "OpenAI" do
before do
Sublayer.configuration.ai_provider = Sublayer::Providers::OpenAI
Sublayer.configuration.ai_model = "gpt-4o-mini"
end

it "generates the Sublayer Generator code and filename" do
VCR.use_cassette("openai/cli/generators/sublayer_generator_generator") do
results = generate("a sublayer generator that takes in code and converts it to the users requested programming language")

expect(results.filename).to be_a(String)
expect(results.filename.length).to be > 0
expect(results.code).to be_a(String)
expect(results.code.length).to be > 0
end
end

end

context "Gemini" do
before do
Sublayer.configuration.ai_provider = Sublayer::Providers::Gemini
Sublayer.configuration.ai_model = "gemini-1.5-pro-latest"
end

it "generates the Sublayer Generator code and filename" do
VCR.use_cassette("gemini/cli/generators/sublayer_generator_generator") do
results = generate("a sublayer generator that takes in code and converts it to the users requested programming language")

expect(results.filename).to be_a(String)
expect(results.filename.length).to be > 0
expect(results.code).to be_a(String)
expect(results.code.length).to be > 0
end
end

end
end
Loading
Loading