-
Notifications
You must be signed in to change notification settings - Fork 2
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
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
893a4fe
First pass at adding the sublayer generate generator command
swerner bf623fb
Add a bit of indication of what is going on
swerner f5bb031
Add some tests for the sublayer generator generator
swerner 7a6bc78
Fix the usage instructions and remove extra newlines
swerner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
96 changes: 96 additions & 0 deletions
96
lib/sublayer/cli/commands/generators/sublayer_generator_generator.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#101