sublayer 0.2.4 → 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/sublayer +1 -1
- data/lib/sublayer/cli/commands/action.rb +65 -0
- data/lib/sublayer/cli/commands/agent.rb +79 -0
- data/lib/sublayer/cli/commands/generator.rb +68 -0
- data/lib/sublayer/cli/commands/generators/example_action_api_call.rb +23 -0
- data/lib/sublayer/cli/commands/generators/example_action_file_manipulation.rb +12 -0
- data/lib/sublayer/cli/commands/generators/example_agent.rb +33 -0
- data/lib/sublayer/cli/commands/generators/example_generator.rb +26 -0
- data/lib/sublayer/cli/commands/generators/sublayer_action_generator.rb +55 -0
- data/lib/sublayer/cli/commands/generators/sublayer_agent_generator.rb +61 -0
- data/lib/sublayer/cli/commands/generators/sublayer_generator_generator.rb +96 -0
- data/lib/sublayer/cli/commands/new_project.rb +116 -0
- data/lib/sublayer/cli/commands/subcommand_base.rb +13 -0
- data/lib/sublayer/{templates/cli/PROJECT_NAME.gemspec → cli/templates/cli/%project_name%.gemspec.tt} +7 -7
- data/lib/sublayer/{templates/cli/README.md → cli/templates/cli/README.md.tt} +2 -2
- data/lib/sublayer/cli/templates/cli/bin/%project_name%.tt +5 -0
- 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
- 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
- data/lib/sublayer/cli/templates/cli/lib/%project_name%/cli.rb.tt +13 -0
- 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
- 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
- data/lib/sublayer/{templates/cli/lib/PROJECT_NAME/config.rb → cli/templates/cli/lib/%project_name%/config.rb.tt} +1 -1
- 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
- data/lib/sublayer/{templates/cli/lib/PROJECT_NAME/version.rb → cli/templates/cli/lib/%project_name%/version.rb.tt} +1 -1
- data/lib/sublayer/cli/templates/cli/lib/%project_name%.rb.tt +21 -0
- data/lib/sublayer/{templates/quick_script/project_name.rb → cli/templates/quick_script/%project_name%.rb} +2 -2
- data/lib/sublayer/{templates/quick_script/README.md → cli/templates/quick_script/README.md.tt} +3 -3
- data/lib/sublayer/cli.rb +46 -178
- data/lib/sublayer/components/output_adapters/single_integer.rb +25 -0
- data/lib/sublayer/providers/gemini.rb +10 -22
- data/lib/sublayer/version.rb +1 -1
- data/lib/sublayer.rb +0 -1
- data/sublayer.gemspec +0 -7
- metadata +36 -121
- data/lib/sublayer/templates/cli/bin/PROJECT_NAME +0 -5
- data/lib/sublayer/templates/cli/lib/PROJECT_NAME/cli.rb +0 -13
- data/lib/sublayer/templates/cli/lib/project_name.rb +0 -21
- /data/lib/sublayer/{templates → cli/templates}/cli/.gitignore +0 -0
- /data/lib/sublayer/{templates → cli/templates}/cli/Gemfile +0 -0
- /data/lib/sublayer/{templates/cli/lib/PROJECT_NAME → cli/templates/cli/lib/%project_name%}/config/.keep +0 -0
- /data/lib/sublayer/{templates → cli/templates}/cli/spec/.keep +0 -0
- /data/lib/sublayer/{templates → cli/templates}/quick_script/actions/example_action.rb +0 -0
- /data/lib/sublayer/{templates → cli/templates}/quick_script/agents/example_agent.rb +0 -0
- /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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ae309181fe8ac4451cb6740fbaaea2599539969ec10d67efe2de75a62e485194
|
4
|
+
data.tar.gz: 0c2f60f772efebe97bd2b9d1da34c6545a8bf67aa9f2dded954a5bb9a3c523f8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cffeb06614a0925c823fc0f05c79361b381146e6cba825d697f8796a8c961138f307c23faaf4fed2c4c2f5fcf532255e1374da148b4de427914fc51a9b16e0b0
|
7
|
+
data.tar.gz: 68d200d5297030675c4ce4616e30d25500f58e060e16ac22f8ddb3e6daf51d52f13ba0a92d9da85d27d5669c4106cc2a55feda98bcdf2a6846c1e098c3b84cb3
|
data/bin/sublayer
CHANGED
@@ -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
|