sublayer 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/bin/sublayer +1 -1
  3. data/lib/sublayer/cli/commands/action.rb +65 -0
  4. data/lib/sublayer/cli/commands/agent.rb +79 -0
  5. data/lib/sublayer/cli/commands/generator.rb +68 -0
  6. data/lib/sublayer/cli/commands/generators/example_action_api_call.rb +23 -0
  7. data/lib/sublayer/cli/commands/generators/example_action_file_manipulation.rb +12 -0
  8. data/lib/sublayer/cli/commands/generators/example_agent.rb +33 -0
  9. data/lib/sublayer/cli/commands/generators/example_generator.rb +26 -0
  10. data/lib/sublayer/cli/commands/generators/sublayer_action_generator.rb +55 -0
  11. data/lib/sublayer/cli/commands/generators/sublayer_agent_generator.rb +61 -0
  12. data/lib/sublayer/cli/commands/generators/sublayer_generator_generator.rb +96 -0
  13. data/lib/sublayer/cli/commands/new_project.rb +116 -0
  14. data/lib/sublayer/cli/commands/subcommand_base.rb +13 -0
  15. data/lib/sublayer/{templates/cli/PROJECT_NAME.gemspec → cli/templates/cli/%project_name%.gemspec.tt} +7 -7
  16. data/lib/sublayer/{templates/cli/README.md → cli/templates/cli/README.md.tt} +2 -2
  17. data/lib/sublayer/cli/templates/cli/bin/%project_name%.tt +5 -0
  18. data/lib/sublayer/{templates/cli/lib/PROJECT_NAME/actions/example_action.rb → cli/templates/cli/lib/%project_name%/actions/example_action.rb.tt} +1 -1
  19. data/lib/sublayer/{templates/cli/lib/PROJECT_NAME/agents/example_agent.rb → cli/templates/cli/lib/%project_name%/agents/example_agent.rb.tt} +1 -1
  20. data/lib/sublayer/cli/templates/cli/lib/%project_name%/cli.rb.tt +13 -0
  21. data/lib/sublayer/{templates/cli/lib/PROJECT_NAME/commands/base_command.rb → cli/templates/cli/lib/%project_name%/commands/base_command.rb.tt} +1 -1
  22. data/lib/sublayer/{templates/cli/lib/PROJECT_NAME/commands/example_command.rb → cli/templates/cli/lib/%project_name%/commands/example_command.rb.tt} +2 -2
  23. data/lib/sublayer/{templates/cli/lib/PROJECT_NAME/config.rb → cli/templates/cli/lib/%project_name%/config.rb.tt} +1 -1
  24. data/lib/sublayer/{templates/cli/lib/PROJECT_NAME/generators/example_generator.rb → cli/templates/cli/lib/%project_name%/generators/example_generator.rb.tt} +1 -1
  25. data/lib/sublayer/{templates/cli/lib/PROJECT_NAME/version.rb → cli/templates/cli/lib/%project_name%/version.rb.tt} +1 -1
  26. data/lib/sublayer/cli/templates/cli/lib/%project_name%.rb.tt +21 -0
  27. data/lib/sublayer/{templates/quick_script/project_name.rb → cli/templates/quick_script/%project_name%.rb} +2 -2
  28. data/lib/sublayer/{templates/quick_script/README.md → cli/templates/quick_script/README.md.tt} +3 -3
  29. data/lib/sublayer/cli.rb +46 -178
  30. data/lib/sublayer/components/output_adapters/single_integer.rb +25 -0
  31. data/lib/sublayer/providers/gemini.rb +10 -22
  32. data/lib/sublayer/version.rb +1 -1
  33. data/lib/sublayer.rb +4 -1
  34. data/sublayer.gemspec +0 -7
  35. metadata +36 -121
  36. data/lib/sublayer/templates/cli/bin/PROJECT_NAME +0 -5
  37. data/lib/sublayer/templates/cli/lib/PROJECT_NAME/cli.rb +0 -13
  38. data/lib/sublayer/templates/cli/lib/project_name.rb +0 -21
  39. /data/lib/sublayer/{templates → cli/templates}/cli/.gitignore +0 -0
  40. /data/lib/sublayer/{templates → cli/templates}/cli/Gemfile +0 -0
  41. /data/lib/sublayer/{templates/cli/lib/PROJECT_NAME → cli/templates/cli/lib/%project_name%}/config/.keep +0 -0
  42. /data/lib/sublayer/{templates → cli/templates}/cli/spec/.keep +0 -0
  43. /data/lib/sublayer/{templates → cli/templates}/quick_script/actions/example_action.rb +0 -0
  44. /data/lib/sublayer/{templates → cli/templates}/quick_script/agents/example_agent.rb +0 -0
  45. /data/lib/sublayer/{templates → cli/templates}/quick_script/generators/example_generator.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2238a9858f994058b23edf70f97255d6c3b20d648c7e73991c05eb7232a9ec6b
4
- data.tar.gz: 935f154d71e8dab6ee3906f700d1f42092f33b9e15ce07c3b305817bcca8177e
3
+ metadata.gz: cd6e0c13942ecc4a6a8a41dc3f80705f363e71fed66993db9ac455e753027937
4
+ data.tar.gz: 7998c6aecf3a1fdab9c31b5dd0cdc3b2838e4ed0d4719de2a9f7242a37c27906
5
5
  SHA512:
6
- metadata.gz: ae8d296c701a18a7e4dde9366f05e4ecf8a5afc964cdc336a563201049660d92345f92219bd1537517bc961e6baf74bab4b9f4652d79666cdf2041e510be2451
7
- data.tar.gz: af8c3a33325de5e2354cc82edd73212f91e5f24bf8c70ae2fac31b58d480ea1c08c76e938e60be714a5f67871940d497b21611daf1e576d6333bb52772e1e4cd
6
+ metadata.gz: b8d97003e00766bd9280cd73c8477908d250b689a65cda9f711d35117b8d4b28bea4559c6b743e20ce8fc1e73f724396e1ac96c1eb93f4ec670cad81b5b267ba
7
+ data.tar.gz: f62ddfa1032ed5595d99fff93f5027f6c4d07e61183fdb290d3bc84cda3d2b85fd0fb17e7ca03222040f10786b1a8789be5e4f57f66cb17b0bdfacb570009712
data/bin/sublayer CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "sublayer/cli"
3
+ require_relative "../lib/sublayer/cli"
4
4
 
5
5
  Sublayer::CLI.start(ARGV)
@@ -0,0 +1,65 @@
1
+ require_relative "./generators/sublayer_action_generator"
2
+
3
+ module Sublayer
4
+ module Commands
5
+ class Action < Thor::Group
6
+ include Thor::Actions
7
+
8
+ class_option :description, type: :string, desc: "Description of the action you want to generate", aliases: :d
9
+ class_option :provider, type: :string, desc: "AI provider (OpenAI, Claude, or Gemini)", aliases: :p
10
+ 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
11
+
12
+ def confirm_usage_of_ai_api
13
+ puts "You are about to generate a new agent that uses an AI API to generate content."
14
+ puts "Please ensure you have the necessary API keys and that you are aware of the costs associated with using the API."
15
+ exit unless yes?("Do you want to continue?")
16
+ end
17
+
18
+ def determine_available_providers
19
+ @available_providers = []
20
+
21
+ @available_providers << "OpenAI" if ENV["OPENAI_API_KEY"]
22
+ @available_providers << "Claude" if ENV["ANTHROPIC_API_KEY"]
23
+ @available_providers << "Gemini" if ENV["GEMINI_API_KEY"]
24
+ end
25
+
26
+ def ask_for_action_details
27
+ @ai_provider = options[:provider] || ask("Select an AI provider:", default: "OpenAI", limited_to: @available_providers)
28
+ @ai_model = options[:model] || select_ai_model
29
+
30
+ @description = options[:description] || ask("Enter a description for the Sublayer Action you'd like to create:")
31
+ end
32
+
33
+ def generate_action
34
+ @results = SublayerActionGenerator.new(description: @description).generate
35
+ end
36
+
37
+ def determine_destination_folder
38
+ @destination_folder = if File.directory?("./actions")
39
+ "./actions"
40
+ elsif Dir.glob("./lib/**/actions").any?
41
+ Dir.glob("./lib/**/actions").first
42
+ else
43
+ "./"
44
+ end
45
+ end
46
+
47
+ def save_action_to_destination_folder
48
+ create_file File.join(@destination_folder, @results.filename), @results.code
49
+ end
50
+
51
+ private
52
+
53
+ def select_ai_model
54
+ case @ai_provider
55
+ when "OpenAI"
56
+ ask("Which OpenAI model would you like to use?", default: "gpt-4o")
57
+ when "Claude"
58
+ ask("Which Anthropic model would you like to use?", default: "claude-3-5-sonnet-20240620")
59
+ when "Gemini"
60
+ ask("Which Google model would you like to use?", default: "gemini-1.5-flash-latest")
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,79 @@
1
+ require_relative "./generators/sublayer_agent_generator"
2
+
3
+ module Sublayer
4
+ module Commands
5
+ class Agent < Thor::Group
6
+ include Thor::Actions
7
+
8
+ class_option :description, type: :string, desc: "Description of the agent you want to generate", aliases: :d
9
+ class_option :provider, type: :string, desc: "AI provider (OpenAI, Claude, or Gemini)", aliases: :p
10
+ 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
11
+
12
+ def confirm_usage_of_ai_api
13
+ puts "You are about to generate a new agent that uses an AI API to generate content."
14
+ puts "Please ensure you have the necessary API keys and that you are aware of the costs associated with using the API."
15
+ exit unless yes?("Do you want to continue?")
16
+ end
17
+
18
+ def determine_available_providers
19
+ @available_providers = []
20
+
21
+ @available_providers << "OpenAI" if ENV["OPENAI_API_KEY"]
22
+ @available_providers << "Claude" if ENV["ANTHROPIC_API_KEY"]
23
+ @available_providers << "Gemini" if ENV["GEMINI_API_KEY"]
24
+ end
25
+
26
+ def ask_for_agent_details
27
+ @ai_provider = options[:provider] || ask("Select an AI provider:", default: "OpenAI", limited_to: @available_providers)
28
+ @ai_model = options[:model] || select_ai_model
29
+ @description = options[:description] || ask("Enter a description for the Sublayer Agent you'd like to create:")
30
+ @trigger = ask("What should trigger this agent to start acting?")
31
+ @goal = ask("What is the agent's goal condition?")
32
+ @check_status = ask("How should the agent check its status toward the goal?")
33
+ @step = ask("How does the agent take a step toward its goal?")
34
+ end
35
+
36
+ def generate_agent
37
+ Sublayer.configuration.ai_provider = Object.const_get("Sublayer::Providers::#{@ai_provider}")
38
+ Sublayer.configuration.ai_model = @ai_model
39
+
40
+ say "Generating Sublayer Agent..."
41
+
42
+ @results = SublayerAgentGenerator.new(
43
+ description: @description,
44
+ trigger: @trigger_explanation,
45
+ goal: @goal,
46
+ check_status: @check_status,
47
+ step: @step
48
+ ).generate
49
+ end
50
+
51
+ def determine_destination_folder
52
+ @destination_folder = if File.directory?("./agents")
53
+ "./agents"
54
+ elsif Dir.glob("./lib/**/agents").any?
55
+ Dir.glob("./lib/**/agents").first
56
+ else
57
+ "./"
58
+ end
59
+ end
60
+
61
+ def save_agent_to_destination_folder
62
+ create_file File.join(@destination_folder, @results.filename), @results.code
63
+ end
64
+
65
+ private
66
+
67
+ def select_ai_model
68
+ case @ai_provider
69
+ when "OpenAI"
70
+ ask("Which OpenAI model would you like to use?", default: "gpt-4o")
71
+ when "Claude"
72
+ ask("Which Anthropic model would you like to use?", default: "claude-3-5-sonnet-20240620")
73
+ when "Gemini"
74
+ ask("Which Google model would you like to use?", default: "gemini-1.5-flash-latest")
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,68 @@
1
+ require_relative "generators/sublayer_generator_generator"
2
+
3
+ module Sublayer
4
+ module Commands
5
+ class Generator < Thor::Group
6
+ include Thor::Actions
7
+
8
+ class_option :description, type: :string, desc: "Description of the generator you want to generate", aliases: :d
9
+ class_option :provider, type: :string, desc: "AI provider (OpenAI, Claude, or Gemini)", aliases: :p
10
+ 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
11
+
12
+ def confirm_usage_of_ai_api
13
+ puts "You are about to generate a new generator that uses an AI API to generate content."
14
+ puts "Please ensure you have the necessary API keys and that you are aware of the costs associated with using the API."
15
+ exit unless yes?("Do you want to continue?")
16
+ end
17
+
18
+ def determine_available_providers
19
+ @available_providers = []
20
+
21
+ @available_providers << "OpenAI" if ENV["OPENAI_API_KEY"]
22
+ @available_providers << "Claude" if ENV["ANTHROPIC_API_KEY"]
23
+ @available_providers << "Gemini" if ENV["GEMINI_API_KEY"]
24
+ end
25
+
26
+ def ask_for_generator_details
27
+ @description = options[:description] || ask("Enter a description for the Sublayer Generator you'd like to create:")
28
+ @ai_provider = options[:provider] || ask("Select an AI provider:", default: "OpenAI", limited_to: @available_providers)
29
+ @ai_model = options[:model] || select_ai_model
30
+ end
31
+
32
+ def generate_generator
33
+ Sublayer.configuration.ai_provider = Object.const_get("Sublayer::Providers::#{@ai_provider}")
34
+ Sublayer.configuration.ai_model = @ai_model
35
+
36
+ say "Generating Sublayer Generator..."
37
+ @results = SublayerGeneratorGenerator.new(description: @description).generate
38
+ end
39
+
40
+ def determine_destination_folder
41
+ # Find either a ./generators folder or a generators folder nested one level below ./lib
42
+ @destination_folder = if File.directory?("./generators")
43
+ "./generators"
44
+ elsif Dir.glob("./lib/**/generators").any?
45
+ Dir.glob("./lib/**/generators").first
46
+ else
47
+ "./"
48
+ end
49
+ end
50
+
51
+ def save_generator_to_destination_folder
52
+ create_file File.join(@destination_folder, @results.filename), @results.code
53
+ end
54
+
55
+ private
56
+ def select_ai_model
57
+ case @ai_provider
58
+ when "OpenAI"
59
+ ask("Which OpenAI model would you like to use?", default: "gpt-4o")
60
+ when "Claude"
61
+ ask("Which Anthropic model would you like to use?", default: "claude-3-5-sonnet-20240620")
62
+ when "Gemini"
63
+ ask("Which Google model would you like to use?", default: "gemini-1.5-flash-latest")
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,23 @@
1
+ class TextToSpeechAction < Sublayer::Actions::Base
2
+ def initialize(text)
3
+ @text = text
4
+ end
5
+
6
+ def call
7
+ speech = HTTParty.post(
8
+ "https://api.openai.com/v1/audio/speech",
9
+ headers: {
10
+ "Authorization" => "Bearer #{ENV["OPENAI_API_KEY"]}",
11
+ "Content-Type" => "application/json",
12
+ },
13
+ body: {
14
+ "model": "tts-1",
15
+ "input": @text,
16
+ "voice": "nova",
17
+ "response_format": "wav"
18
+ }.to_json
19
+ )
20
+
21
+ speech
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ class WriteFileAction < Sublayer::Actions::Base
2
+ def initialize(file_contents:, file_path:)
3
+ @file_contents = file_contents
4
+ @file_path = file_path
5
+ end
6
+
7
+ def call
8
+ File.open(@file_path, 'wb') do |file|
9
+ file.write(@file_contents)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,33 @@
1
+ class RSpecAgent < Sublayer::Agents::Base
2
+ def initialize(implementation_file_path:, test_file_path:)
3
+ @implementation_file_path = implementation_file_path
4
+ @test_file_path = test_file_path
5
+ @tests_passing = false
6
+ end
7
+
8
+ trigger_on_files_changed { [@implementation_file_path, @test_file_path] }
9
+
10
+ goal_condition { @tests_passing == true }
11
+
12
+ check_status do
13
+ stdout, stderr, status = Sublayer::Actions::RunTestCommandAction.new(
14
+ test_command: "rspec #{@test_file_path}"
15
+ ).call
16
+
17
+ @test_output = stdout
18
+ @tests_passing = (status.exitstatus == 0)
19
+ end
20
+
21
+ step do
22
+ modified_implementation = Sublayer::Generators::ModifiedImplementationToPassTestsGenerator.new(
23
+ implementation_file_contents: File.read(@implementation_file_path),
24
+ test_file_contents: File.read(@test_file_path),
25
+ test_output: @test_output
26
+ ).generate
27
+
28
+ Sublayer::Actions::WriteFileAction.new(
29
+ file_contents: modified_implementation,
30
+ file_path: @implementation_file_path
31
+ ).call
32
+ end
33
+ end
@@ -0,0 +1,26 @@
1
+ class CodeFromDescriptionGenerator < Sublayer::Generators::Base
2
+ llm_output_adapter type: :single_string,
3
+ name: "generated_code",
4
+ description: "The generated code in the requested language"
5
+
6
+ def initialize(description:, technologies:)
7
+ @description = description
8
+ @technologies = technologies
9
+ end
10
+
11
+ def generate
12
+ super
13
+ end
14
+
15
+ def prompt
16
+ <<-PROMPT
17
+ You are an expert programmer in #{@technologies.join(", ")}.
18
+
19
+ You are tasked with writing code using the following technologies: #{@technologies.join(", ")}.
20
+
21
+ The description of the task is #{@description}
22
+
23
+ Take a deep breath and think step by step before you start coding.
24
+ PROMPT
25
+ end
26
+ end
@@ -0,0 +1,55 @@
1
+ class SublayerActionGenerator < Sublayer::Generators::Base
2
+ llm_output_adapter type: :named_strings,
3
+ name: "sublayer_action",
4
+ description: "The new sublayer action based on the description and supporting information",
5
+ attributes: [
6
+ { name: "code", description: "The code of the generated Sublayer action" },
7
+ { name: "filename", description: "The filename of the generated sublayer action snake cased with a .rb extension" }
8
+ ]
9
+
10
+ def initialize(description:)
11
+ @description = description
12
+ end
13
+
14
+ def generate
15
+ super
16
+ end
17
+
18
+ def prompt
19
+ <<-PROMPT
20
+ You are an expert ruby programmer and are great at repurposing code examples to use for new situations.
21
+
22
+ A Sublayer Action is an example of a command pattern that is used in the Sublayer AI framework to perform actions in the outside world such as manipulating files or making API calls.
23
+
24
+ The Sublayer framework also has a component called a Generator that takes data in, sends it to an LLM and gets structured data out.
25
+ Sublayer::Actions are used both to retrieve data for use in a generator or perform actions based on the output of the generator.
26
+ This is used to both aid in generating new composable bits of functionality and to ease testing.
27
+
28
+ A sublayer action is initialized with the data it needs and then exposes a `call` method which is used to perform the action.
29
+
30
+ An example of an action being used to save a file to the file system, for example after generating the contents is:
31
+ <example_file_manipulation_action>
32
+ #{example_filesystem_action}
33
+ </example_file_manipulation_action>
34
+
35
+ An example action being used to make a call to an external api, such as OpenAI's text to speech API is:
36
+ <example_api_call_action>
37
+ #{example_api_action}
38
+ </example_api_call_action>
39
+
40
+ Your task is to generate a new Sublayer::Action::Base subclass that performs an action based on the description provided.
41
+ <description>
42
+ #{@description}
43
+ </description>
44
+ PROMPT
45
+ end
46
+
47
+ private
48
+ def example_filesystem_action
49
+ File.read(File.join(File.dirname(__FILE__), 'example_action_file_manipulation.rb'))
50
+ end
51
+
52
+ def example_api_action
53
+ File.read(File.join(File.dirname(__FILE__), 'example_action_api_call.rb'))
54
+ end
55
+ end
@@ -0,0 +1,61 @@
1
+ class SublayerAgentGenerator < Sublayer::Generators::Base
2
+ llm_output_adapter type: :named_strings,
3
+ name: "sublayer_agent",
4
+ description: "The new sublayer agent based on the description and supporting information",
5
+ attributes: [
6
+ { name: "code", description: "The code of the generated Sublayer agent" },
7
+ { name: "filename", description: "The filename of the generated sublayer agent snake cased with a .rb extension" }
8
+ ]
9
+
10
+ def initialize(description:, trigger:, goal:, check_status:, step:)
11
+ @description = description
12
+ @trigger_condition = trigger
13
+ @goal = goal
14
+ @check_status = check_status
15
+ @step = step
16
+ end
17
+
18
+ def generate
19
+ super
20
+ end
21
+
22
+ def prompt
23
+ <<-PROMPT
24
+ You are an expert ruby programmer and great at repurposing code examples to use for new situations.
25
+
26
+ A Sublayer agent is a DSL for defining a feedback loop for an AI agent. The agents sit running like a daemon and are triggered to run and step toward their goal checking status along the way.
27
+
28
+ One example of a Sublayer agent is this one for doing TDD with RSpec:
29
+ <example_tdd_agent>
30
+ #{example_agent}
31
+ </example_tdd_agent>
32
+
33
+ Sublayer Agents take advantage of other Sublayer components to perform their tasks.
34
+ Sublayer::Actions are used to perform actions in the outside world, things like saving files, making external api calls, retrieving data, etc
35
+ Sublayer::Generators are used to make calls to LLMs based on information they receive from Sublayer::Actions or other sources and return structured data for other Sublayer::Actions to do use in the outside world
36
+
37
+ The Sublayer Agent DSL consists of 4 main parts:
38
+ 1. trigger: What triggers the agent, currently built into the framework is the trigger_on_files_changed, but the trigger method also accepts a Sublayer::Triggers::Base subclass that defines an initialize method and a setup method that takes an agent as an argument
39
+ The setup method then calls activate(agent) when the trigger condition is met
40
+ 2. goal: What the agent is trying to achieve, this is a block that returns a boolean
41
+ 3. check_status: A method that checks the status of the agent on its way toward the goal. Usually you update the goal condition here or can perform any Sublayer::Actions to examine the state of the outside world
42
+ 4. step: A method that encapsulates the way the agent should work toward the goal. Sublayer::Actions and Sublayer::Generators are used heavily here.
43
+
44
+ Your goal is to rely on the above information about how Sublayer Agents work to generate a new Sublayer agent based on the following information:
45
+
46
+ Agent description: #{@description}
47
+ Trigger condition: #{@trigger}
48
+ Goal condition: #{@goal}
49
+ Status check method: #{@check_status}
50
+ Step action method: #{@step}
51
+
52
+ You can assume that any Sublayer::Actions or Sublayer::Generators you need are available to you in the Sublayer framework
53
+
54
+ Take a deep breath and think step by step before coding. You can do this!
55
+ PROMPT
56
+ end
57
+
58
+ def example_agent
59
+ File.read(File.join(__dir__, "example_agent.rb"))
60
+ end
61
+ end
@@ -0,0 +1,96 @@
1
+ class SublayerGeneratorGenerator < Sublayer::Generators::Base
2
+ llm_output_adapter type: :named_strings,
3
+ name: "sublayer_generator",
4
+ description: "The new Sublayer generator code based on the description",
5
+ attributes: [
6
+ { name: "code", description: "The code of the generated Sublayer generator" },
7
+ { name: "filename", description: "The filename of the generated Sublayer generator snake cased and with a .rb extension" }
8
+ ]
9
+
10
+ def initialize(description:)
11
+ @description = description
12
+ end
13
+
14
+ def generate
15
+ super
16
+ end
17
+
18
+ def prompt
19
+ <<-PROMPT
20
+ You are an expert ruby programmer and and great at repurposing code examples to use for new situations.
21
+
22
+ A Sublayer generator is a class that acts as an abstraction for sending a request to an LLM and getting structured data back.
23
+
24
+ An example Sublayer generator is:
25
+ <example_generator>
26
+ #{example_generator}
27
+ </example_generator>
28
+
29
+ And it is the generator we're currently using for taking in a description from a user and generating a new Sublayer generator.
30
+
31
+ 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.
32
+
33
+ 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.
34
+
35
+ the other available options and their example usage is:
36
+ llm_output_adapter type: :list_of_strings,
37
+ name: "suggestions",
38
+ description: "List of keyword suggestions"
39
+
40
+ llm_output_adapter type: :single_integer,
41
+ name: "four_digit_passcode",
42
+ description: "an uncommon and difficult to guess four digit passcode"
43
+
44
+ llm_output_adapter type: :list_of_named_strings,
45
+ name: "review_summaries",
46
+ description: "List of movie reviews",
47
+ item_name: "review",
48
+ attributes: [
49
+ { name: "movie_title", description: "The title of the movie" },
50
+ { name: "reviewer_name", description: "The name of the reviewer" },
51
+ { name: "rating", description: "The rating given by the reviewer (out of 5 stars)" },
52
+ { name: "brief_comment", description: "A brief summary of the movie" }
53
+ ]
54
+
55
+ llm_output_adapter type: :named_strings,
56
+ name: "product_description",
57
+ description: "Generate product descriptions",
58
+ attributes: [
59
+ { name: "short_description", description: "A brief one-sentence description of the product" },
60
+ { name: "long_description", description: "A detailed paragraph describing the product" },
61
+ { name: "key_features", description: "A comma-separated list of key product features" },
62
+ { name: "target_audience", description: "A brief description of the target audience for this product" }
63
+ ]
64
+
65
+ # Where :available_routes is a method that returns an array of available routes
66
+ llm_output_adapter type: :string_selection_from_list,
67
+ name: "route",
68
+ description: "A route selected from the list",
69
+ options: :available_routes
70
+
71
+ # Where @sentiment_options is an array of sentiment values passed in to the initializer
72
+ llm_output_adapter type: :string_selection_from_list,
73
+ name: "sentiment_value",
74
+ description: "A sentiment value from the list",
75
+ options: -> { @sentiment_options }
76
+
77
+ 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
78
+ is either passed in through the initializer or accessed in the prompt itself.
79
+
80
+ You have a description provided by the user to generate a new sublayer generator.
81
+
82
+ You are tasked with creating a new sublayer generator according to the given description.
83
+
84
+ Consider the details and requirements mentioned in the description to create the appropriate Sublayer::Generator.
85
+ <users_description>
86
+ #{@description}
87
+ </users_description>
88
+
89
+ Take a deep breath and reflect on each aspect of the description before proceeding with the generation.
90
+ PROMPT
91
+ end
92
+
93
+ def example_generator
94
+ File.read(File.join(File.dirname(__FILE__), "example_generator.rb"))
95
+ end
96
+ end
@@ -0,0 +1,116 @@
1
+ module Sublayer
2
+ module Commands
3
+ class NewProject < Thor::Group
4
+ include Thor::Actions
5
+
6
+ argument :project_name, type: :string, desc: "The name of your project"
7
+
8
+ class_option :template, type: :string, desc: "Type of project (CLI or QuickScript)", aliases: :t
9
+ class_option :provider, type: :string, desc: "AI provider (OpenAI, Claude, or Gemini)", aliases: :p
10
+ 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
11
+
12
+ def sublayer_version
13
+ Sublayer::VERSION
14
+ end
15
+
16
+ def self.source_root
17
+ File.dirname(__FILE__)
18
+ end
19
+
20
+ def ask_for_project_details
21
+ puts options[:template]
22
+ @project_template = options[:template] || ask("Select a project template:", default: "CLI", limited_to: %w[CLI QuickScript])
23
+ @ai_provider = options[:provider] || ask("Select an AI provider:", default: "OpenAI", limited_to: %w[OpenAI Claude Gemini])
24
+ @ai_model = options[:model] || select_ai_model
25
+ end
26
+
27
+ def create_project_directory
28
+ say "Creating project directory", :green
29
+ empty_directory project_name
30
+ end
31
+
32
+ def copy_template_files
33
+ say "Copying template files", :green
34
+ template_dir = @project_template == "CLI" ? "cli" : "quick_script"
35
+ directory "../templates/#{template_dir}", project_name
36
+ empty_directory File.join(project_name, "log") if @project_template =="CLI"
37
+ end
38
+
39
+ def generate_config_file
40
+ say "Generating configuration", :green
41
+
42
+ config = {
43
+ project_name: project_name,
44
+ project_template: @project_template,
45
+ ai_provider: @ai_provider,
46
+ ai_model: @ai_model
47
+ }
48
+
49
+ if @project_template == "CLI"
50
+ create_file File.join(project_name, "lib", project_name, "config", "sublayer.yml"), YAML.dump(config)
51
+ else
52
+ append_to_file File.join(project_name, "#{project_name}.rb") do
53
+ <<~CONFIG
54
+ Sublayer.configuration.ai_provider = Sublayer::Providers::#{config[:ai_provider]}
55
+ Sublayer.configuration.ai_model = "#{config[:ai_model]}"
56
+ CONFIG
57
+ end
58
+ end
59
+ end
60
+
61
+ def finalize_project
62
+ say "Finalizing project", :green
63
+ inside(project_name) do
64
+ if @project_template == "CLI"
65
+ chmod("bin/#{project_name}", "+x")
66
+ run("bundle install") if yes?("Install gems?")
67
+ else
68
+ append_to_file "#{project_name}.rb" do
69
+ <<~INSTRUCTIONS
70
+ puts "Welcome to your quick Sublayer script!"
71
+ puts "To get started, create some generators, actions, or agents in their respective directories and call them here"
72
+ puts "For more information, visit https://docs.sublayer.com"
73
+ INSTRUCTIONS
74
+ end
75
+ end
76
+
77
+ run("git init") if yes?("Initialize a git repository?")
78
+ end
79
+ end
80
+
81
+ def print_next_steps
82
+ say "\nSublayer project '#{project_name}' created successfully!", :green
83
+ say "To get started, run:"
84
+ say " cd #{project_name}"
85
+ if @project_template == "CLI"
86
+ say " ./bin/#{project_name}"
87
+ else
88
+ say " ruby #{project_name}.rb"
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def select_ai_model
95
+ case @ai_provider
96
+ when "OpenAI"
97
+ ask("Which OpenAI model would you like to use?", default: "gpt-4o", limited_to: %w[gpt-4o gpt-4o-mini gpt-4-turbo gpt-3.5-turbo])
98
+ when "Claude"
99
+ ask("Which Anthropic model would you like to use?", default: "claude-3-5-sonnet-20240620", limited_to: %w[claude-3-5-sonnet-20240620 claude-3-opus-20240620 claude-3-haiku-20240307])
100
+ when "Gemini"
101
+ ask("Which Google model would you like to use?", default: "gemini-1.5-flash-latest", limited_to: %w[gemini-1.5-flash-latest gemini-1.5-pro-latest])
102
+ end
103
+ end
104
+
105
+ def rename_project_name_directory
106
+ old_path = File.join(project_name, "lib", "PROJECT_NAME")
107
+ new_path = File.join(project_name, "lib", project_name.gsub("-", "_").downcase)
108
+
109
+ if File.directory?(old_path)
110
+ say "Renaming project directory", :green
111
+ FileUtils.mv(old_path, new_path)
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end