sublayer 0.2.3 → 0.2.5
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.
- checksums.yaml +4 -4
- data/bin/sublayer +5 -0
- 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/cli/templates/cli/%project_name%.gemspec.tt +35 -0
- data/lib/sublayer/cli/templates/cli/.gitignore +14 -0
- data/lib/sublayer/cli/templates/cli/Gemfile +7 -0
- data/lib/sublayer/cli/templates/cli/README.md.tt +22 -0
- data/lib/sublayer/cli/templates/cli/bin/%project_name%.tt +5 -0
- data/lib/sublayer/cli/templates/cli/lib/%project_name%/actions/example_action.rb.tt +15 -0
- data/lib/sublayer/cli/templates/cli/lib/%project_name%/agents/example_agent.rb.tt +21 -0
- data/lib/sublayer/cli/templates/cli/lib/%project_name%/cli.rb.tt +13 -0
- data/lib/sublayer/cli/templates/cli/lib/%project_name%/commands/base_command.rb.tt +21 -0
- data/lib/sublayer/cli/templates/cli/lib/%project_name%/commands/example_command.rb.tt +13 -0
- data/lib/sublayer/cli/templates/cli/lib/%project_name%/config/.keep +0 -0
- data/lib/sublayer/cli/templates/cli/lib/%project_name%/config.rb.tt +19 -0
- data/lib/sublayer/cli/templates/cli/lib/%project_name%/generators/example_generator.rb.tt +25 -0
- data/lib/sublayer/cli/templates/cli/lib/%project_name%/version.rb.tt +5 -0
- data/lib/sublayer/cli/templates/cli/lib/%project_name%.rb.tt +21 -0
- data/lib/sublayer/cli/templates/cli/spec/.keep +0 -0
- data/lib/sublayer/cli/templates/quick_script/%project_name%.rb +7 -0
- data/lib/sublayer/cli/templates/quick_script/README.md.tt +16 -0
- data/lib/sublayer/cli/templates/quick_script/actions/example_action.rb +11 -0
- data/lib/sublayer/cli/templates/quick_script/agents/example_agent.rb +17 -0
- data/lib/sublayer/cli/templates/quick_script/generators/example_generator.rb +21 -0
- data/lib/sublayer/cli.rb +59 -0
- data/lib/sublayer/components/output_adapters/formattable.rb +1 -0
- data/lib/sublayer/components/output_adapters/list_of_named_strings.rb +48 -0
- data/lib/sublayer/components/output_adapters/named_strings.rb +3 -1
- 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 +1 -1
- data/sublayer.gemspec +3 -3
- metadata +46 -36
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
ADDED
@@ -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
|