├── .contextignore ├── .github └── workflows │ ├── merged_pr_doc_update │ ├── actions │ │ ├── .keep │ │ ├── github_base.rb │ │ ├── github_create_pr_action.rb │ │ ├── github_create_branch_action.rb │ │ ├── github_modify_file_action.rb │ │ ├── get_pr_changes_action.rb │ │ ├── github_add_pr_label_action.rb │ │ └── github_get_file_contents_action.rb │ ├── agents │ │ └── .keep │ ├── generators │ │ ├── .keep │ │ └── update_actions_md_content_generator.rb │ └── merged_pr_doc_update.rb │ ├── daily_action_suggestion │ ├── actions │ │ ├── .keep │ │ ├── github_base.rb │ │ ├── github_create_pr_action.rb │ │ ├── github_create_branch_action.rb │ │ ├── github_create_file_action.rb │ │ ├── github_modify_file_action.rb │ │ ├── github_add_pr_label_action.rb │ │ └── get_context_action.rb │ ├── generators │ │ ├── .keep │ │ ├── action_ideas_generator.rb │ │ └── action_generator.rb │ └── daily_action_suggestion.rb │ ├── generate_specific_action │ ├── actions │ │ ├── .keep │ │ ├── github_base.rb │ │ ├── github_create_pr_action.rb │ │ ├── github_create_branch_action.rb │ │ ├── github_create_file_action.rb │ │ ├── github_modify_file_action.rb │ │ ├── github_add_pr_label_action.rb │ │ └── get_context_action.rb │ ├── generators │ │ ├── .keep │ │ └── action_generator.rb │ └── generate_specific_action.rb │ ├── merged_pr_discord_notifier │ ├── actions │ │ ├── .keep │ │ ├── get_pr_file_url_action.rb │ │ ├── get_pr_changes_action.rb │ │ └── discord_send_message_action.rb │ ├── generators │ │ ├── .keep │ │ └── sublayer_action_use_case_generator.rb │ └── merged_pr_discord_notifier.rb │ ├── merged_pr_related_suggestion │ ├── actions │ │ ├── .keep │ │ ├── github_base.rb │ │ ├── github_create_pr_action.rb │ │ ├── github_create_branch_action.rb │ │ ├── github_create_file_action.rb │ │ ├── github_modify_file_action.rb │ │ ├── get_pr_changes_action.rb │ │ ├── github_add_pr_label_action.rb │ │ └── get_context_action.rb │ ├── generators │ │ ├── .keep │ │ ├── related_action_ideas_generator.rb │ │ └── action_generator.rb │ └── merged_pr_related_suggestion.rb │ ├── merged_pr_discord_notifier.yml │ ├── daily_action_suggestion.yml │ ├── merged_pr_doc_update.yml │ ├── generate_specific_action.yml │ └── merged_pr_related_suggestion.yml ├── Github ├── github_base.rb ├── github_create_pr_action.rb ├── github_create_branch_action.rb ├── github_create_file_action.rb ├── github_modify_file_action.rb └── github_get_file_contents_action.rb ├── Asana ├── asana_base.rb ├── asana_get_task_name_action.rb ├── asana_get_task_description_action.rb ├── asana_get_latest_comment_action.rb └── asana_create_task_action.rb ├── Notion ├── notion_create_row_action.rb └── notion_query_database_action.rb ├── README.md ├── LICENSE ├── FileSystem └── write_file_action.rb ├── Slack └── slack_message_send_action.rb ├── OpenAI └── openai_image_generation_action.rb ├── Jira ├── jira_get_issue_description_action.rb ├── jira_get_project_issues_action.rb └── jira_create_issue_action.rb ├── AI_Utilities └── get_context_action.rb ├── Discord └── discord_send_message_action.rb └── AzureDevops ├── azure_devops_create_epic_action.rb └── azure_devops_create_epic_issue_action.rb /.contextignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github/**/* 3 | LICENSE 4 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_doc_update/actions/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_doc_update/agents/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/daily_action_suggestion/actions/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/daily_action_suggestion/generators/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/generate_specific_action/actions/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_discord_notifier/actions/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_doc_update/generators/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/generate_specific_action/generators/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_discord_notifier/generators/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_related_suggestion/actions/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_related_suggestion/generators/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/daily_action_suggestion/actions/github_base.rb: -------------------------------------------------------------------------------- 1 | class GithubBase < Sublayer::Actions::Base 2 | def initialize(repo:) 3 | @repo = repo 4 | @client = Octokit::Client.new(access_token: ENV["GITHUB_ACCESS_TOKEN"]) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_doc_update/actions/github_base.rb: -------------------------------------------------------------------------------- 1 | class GithubBase < Sublayer::Actions::Base 2 | def initialize(repo:) 3 | @repo = repo 4 | @client = Octokit::Client.new(access_token: ENV["GITHUB_ACCESS_TOKEN"]) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /.github/workflows/generate_specific_action/actions/github_base.rb: -------------------------------------------------------------------------------- 1 | class GithubBase < Sublayer::Actions::Base 2 | def initialize(repo:) 3 | @repo = repo 4 | @client = Octokit::Client.new(access_token: ENV["GITHUB_ACCESS_TOKEN"]) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_related_suggestion/actions/github_base.rb: -------------------------------------------------------------------------------- 1 | class GithubBase < Sublayer::Actions::Base 2 | def initialize(repo:) 3 | @repo = repo 4 | @client = Octokit::Client.new(access_token: ENV["GITHUB_ACCESS_TOKEN"]) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /Github/github_base.rb: -------------------------------------------------------------------------------- 1 | # Description: Base class for Github-related Sublayer::Actions that abstracts away handling of the Octokit client and access token 2 | class GithubBase < Sublayer::Actions::Base 3 | def initialize(repo:) 4 | @repo = repo 5 | @client = Octokit::Client.new(access_token: ENV["GITHUB_ACCESS_TOKEN"]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Github/github_create_pr_action.rb: -------------------------------------------------------------------------------- 1 | class GithubCreatePRAction < GithubBase 2 | def initialize(repo:, base:, head:, title:, body:) 3 | super(repo: repo) 4 | @base = base 5 | @head = head 6 | @title = title 7 | @body = body 8 | end 9 | 10 | def call 11 | results = @client.create_pull_request(@repo, @base, @head, @title, @body) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.github/workflows/daily_action_suggestion/actions/github_create_pr_action.rb: -------------------------------------------------------------------------------- 1 | class GithubCreatePRAction < GithubBase 2 | def initialize(repo:, base:, head:, title:, body:) 3 | super(repo: repo) 4 | @base = base 5 | @head = head 6 | @title = title 7 | @body = body 8 | end 9 | 10 | def call 11 | results = @client.create_pull_request(@repo, @base, @head, @title, @body) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_doc_update/actions/github_create_pr_action.rb: -------------------------------------------------------------------------------- 1 | class GithubCreatePRAction < GithubBase 2 | def initialize(repo:, base:, head:, title:, body:) 3 | super(repo: repo) 4 | @base = base 5 | @head = head 6 | @title = title 7 | @body = body 8 | end 9 | 10 | def call 11 | results = @client.create_pull_request(@repo, @base, @head, @title, @body) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Github/github_create_branch_action.rb: -------------------------------------------------------------------------------- 1 | class GithubCreateBranchAction < GithubBase 2 | def initialize(repo:, base_branch:, new_branch:) 3 | super(repo: repo) 4 | @base_branch = base_branch 5 | @new_branch = new_branch 6 | end 7 | 8 | def call 9 | ref = @client.ref(@repo, "heads/#{@base_branch}") 10 | @client.create_ref(@repo, "refs/heads/#{@new_branch}", ref.object.sha) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /.github/workflows/generate_specific_action/actions/github_create_pr_action.rb: -------------------------------------------------------------------------------- 1 | class GithubCreatePRAction < GithubBase 2 | def initialize(repo:, base:, head:, title:, body:) 3 | super(repo: repo) 4 | @base = base 5 | @head = head 6 | @title = title 7 | @body = body 8 | end 9 | 10 | def call 11 | results = @client.create_pull_request(@repo, @base, @head, @title, @body) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_related_suggestion/actions/github_create_pr_action.rb: -------------------------------------------------------------------------------- 1 | class GithubCreatePRAction < GithubBase 2 | def initialize(repo:, base:, head:, title:, body:) 3 | super(repo: repo) 4 | @base = base 5 | @head = head 6 | @title = title 7 | @body = body 8 | end 9 | 10 | def call 11 | results = @client.create_pull_request(@repo, @base, @head, @title, @body) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Asana/asana_base.rb: -------------------------------------------------------------------------------- 1 | # Description: Base class for Asana actions that abstracts away access_token handling and provides an Asana client instance to subclasses 2 | 3 | class AsanaBase < Sublayer::Actions::Base 4 | def initialize(access_token: nil) 5 | @access_token = access_token || ENV["ASANA_ACCESS_TOKEN"] 6 | @client = Asana::Client.new do |c| 7 | c.authentication :access_token, @access_token 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_doc_update/actions/github_create_branch_action.rb: -------------------------------------------------------------------------------- 1 | class GithubCreateBranchAction < GithubBase 2 | def initialize(repo:, base_branch:, new_branch:) 3 | super(repo: repo) 4 | @base_branch = base_branch 5 | @new_branch = new_branch 6 | end 7 | 8 | def call 9 | ref = @client.ref(@repo, "heads/#{@base_branch}") 10 | @client.create_ref(@repo, "refs/heads/#{@new_branch}", ref.object.sha) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /.github/workflows/daily_action_suggestion/actions/github_create_branch_action.rb: -------------------------------------------------------------------------------- 1 | class GithubCreateBranchAction < GithubBase 2 | def initialize(repo:, base_branch:, new_branch:) 3 | super(repo: repo) 4 | @base_branch = base_branch 5 | @new_branch = new_branch 6 | end 7 | 8 | def call 9 | ref = @client.ref(@repo, "heads/#{@base_branch}") 10 | @client.create_ref(@repo, "refs/heads/#{@new_branch}", ref.object.sha) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /.github/workflows/generate_specific_action/actions/github_create_branch_action.rb: -------------------------------------------------------------------------------- 1 | class GithubCreateBranchAction < GithubBase 2 | def initialize(repo:, base_branch:, new_branch:) 3 | super(repo: repo) 4 | @base_branch = base_branch 5 | @new_branch = new_branch 6 | end 7 | 8 | def call 9 | ref = @client.ref(@repo, "heads/#{@base_branch}") 10 | @client.create_ref(@repo, "refs/heads/#{@new_branch}", ref.object.sha) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_related_suggestion/actions/github_create_branch_action.rb: -------------------------------------------------------------------------------- 1 | class GithubCreateBranchAction < GithubBase 2 | def initialize(repo:, base_branch:, new_branch:) 3 | super(repo: repo) 4 | @base_branch = base_branch 5 | @new_branch = new_branch 6 | end 7 | 8 | def call 9 | ref = @client.ref(@repo, "heads/#{@base_branch}") 10 | @client.create_ref(@repo, "refs/heads/#{@new_branch}", ref.object.sha) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /Notion/notion_create_row_action.rb: -------------------------------------------------------------------------------- 1 | class NotionCreateRowAction < Sublayer::Actions::Base 2 | def initialize(database_id:, properties:) 3 | @database_id = database_id 4 | @properties = properties 5 | end 6 | 7 | def call 8 | notion = Notion::Client.new(token: ENV['NOTION_API_KEY']) 9 | 10 | notion.create_page( 11 | parent: { 12 | database_id: @database_id 13 | }, 14 | properties: @properties 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /Github/github_create_file_action.rb: -------------------------------------------------------------------------------- 1 | class GithubCreateFileAction < GithubBase 2 | def initialize(repo:, branch:, file_path:, file_content:) 3 | super(repo: repo) 4 | @branch = branch 5 | @file_path = file_path 6 | @file_content = file_content 7 | end 8 | 9 | def call 10 | @client.create_contents( 11 | @repo, 12 | @file_path, 13 | "Creating #{@file_path}", 14 | @file_content, 15 | branch: @branch 16 | ) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /.github/workflows/daily_action_suggestion/actions/github_create_file_action.rb: -------------------------------------------------------------------------------- 1 | class GithubCreateFileAction < GithubBase 2 | def initialize(repo:, branch:, file_path:, file_content:) 3 | super(repo: repo) 4 | @branch = branch 5 | @file_path = file_path 6 | @file_content = file_content 7 | end 8 | 9 | def call 10 | @client.create_contents( 11 | @repo, 12 | @file_path, 13 | "Creating #{@file_path}", 14 | @file_content, 15 | branch: @branch 16 | ) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_discord_notifier/actions/get_pr_file_url_action.rb: -------------------------------------------------------------------------------- 1 | class GetPRFileInfoAction < Sublayer::Actions::Base 2 | def initialize(repo:, pr_number:) 3 | @repo = repo 4 | @pr_number = pr_number 5 | @client = Octokit::Client.new(access_token: ENV["GITHUB_ACCESS_TOKEN"]) 6 | end 7 | 8 | def call 9 | files = @client.pull_request_files(@repo, @pr_number) 10 | 11 | { 12 | file_name: files.first.filename, 13 | file_url: files.first.blob_url 14 | } 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /.github/workflows/generate_specific_action/actions/github_create_file_action.rb: -------------------------------------------------------------------------------- 1 | class GithubCreateFileAction < GithubBase 2 | def initialize(repo:, branch:, file_path:, file_content:) 3 | super(repo: repo) 4 | @branch = branch 5 | @file_path = file_path 6 | @file_content = file_content 7 | end 8 | 9 | def call 10 | @client.create_contents( 11 | @repo, 12 | @file_path, 13 | "Creating #{@file_path}", 14 | @file_content, 15 | branch: @branch 16 | ) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_related_suggestion/actions/github_create_file_action.rb: -------------------------------------------------------------------------------- 1 | class GithubCreateFileAction < GithubBase 2 | def initialize(repo:, branch:, file_path:, file_content:) 3 | super(repo: repo) 4 | @branch = branch 5 | @file_path = file_path 6 | @file_content = file_content 7 | end 8 | 9 | def call 10 | @client.create_contents( 11 | @repo, 12 | @file_path, 13 | "Creating #{@file_path}", 14 | @file_content, 15 | branch: @branch 16 | ) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /Github/github_modify_file_action.rb: -------------------------------------------------------------------------------- 1 | class GithubModifyFileAction < GithubBase 2 | def initialize(repo:, branch:, file_path:, file_content:) 3 | super(repo: repo) 4 | @branch = branch 5 | @file_path = file_path 6 | @file_content = file_content 7 | end 8 | 9 | def call 10 | content = @client.contents(@repo, path: @file_path, ref: @branch) 11 | @client.update_contents( 12 | @repo, 13 | @file_path, 14 | "Updating #{@file_path}", 15 | content.sha, 16 | @file_content, 17 | branch: @branch 18 | ) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.github/workflows/daily_action_suggestion/actions/github_modify_file_action.rb: -------------------------------------------------------------------------------- 1 | class GithubModifyFileAction < GithubBase 2 | def initialize(repo:, branch:, file_path:, file_content:) 3 | super(repo: repo) 4 | @branch = branch 5 | @file_path = file_path 6 | @file_content = file_content 7 | end 8 | 9 | def call 10 | content = @client.contents(@repo, path: @file_path, ref: @branch) 11 | @client.update_contents( 12 | @repo, 13 | @file_path, 14 | "Updating #{@file_path}", 15 | content.sha, 16 | @file_content, 17 | branch: @branch 18 | ) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.github/workflows/generate_specific_action/actions/github_modify_file_action.rb: -------------------------------------------------------------------------------- 1 | class GithubModifyFileAction < GithubBase 2 | def initialize(repo:, branch:, file_path:, file_content:) 3 | super(repo: repo) 4 | @branch = branch 5 | @file_path = file_path 6 | @file_content = file_content 7 | end 8 | 9 | def call 10 | content = @client.contents(@repo, path: @file_path, ref: @branch) 11 | @client.update_contents( 12 | @repo, 13 | @file_path, 14 | "Updating #{@file_path}", 15 | content.sha, 16 | @file_content, 17 | branch: @branch 18 | ) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_doc_update/actions/github_modify_file_action.rb: -------------------------------------------------------------------------------- 1 | class GithubModifyFileAction < GithubBase 2 | def initialize(repo:, branch:, file_path:, file_content:) 3 | super(repo: repo) 4 | @branch = branch 5 | @file_path = file_path 6 | @file_content = file_content 7 | end 8 | 9 | def call 10 | content = @client.contents(@repo, path: @file_path, ref: @branch) 11 | @client.update_contents( 12 | @repo, 13 | @file_path, 14 | "Updating #{@file_path}", 15 | content.sha, 16 | @file_content, 17 | branch: @branch 18 | ) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_related_suggestion/actions/github_modify_file_action.rb: -------------------------------------------------------------------------------- 1 | class GithubModifyFileAction < GithubBase 2 | def initialize(repo:, branch:, file_path:, file_content:) 3 | super(repo: repo) 4 | @branch = branch 5 | @file_path = file_path 6 | @file_content = file_content 7 | end 8 | 9 | def call 10 | content = @client.contents(@repo, path: @file_path, ref: @branch) 11 | @client.update_contents( 12 | @repo, 13 | @file_path, 14 | "Updating #{@file_path}", 15 | content.sha, 16 | @file_content, 17 | branch: @branch 18 | ) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /Asana/asana_get_task_name_action.rb: -------------------------------------------------------------------------------- 1 | # Description: This Sublayer::Action is used to get the name of an Asana task. 2 | # 3 | # It is initialized with a task_gid and returns the name of the task. 4 | # 5 | # Example usage: If you have an AI agent monitoring asana tasks and performing actions based on user comments, 6 | # you can use this action to get the task name to use in a Sublayer::Generator for augmenting the prompt 7 | 8 | class AsanaGetTaskNameAction < AsanaBase 9 | def initialize(task_gid:, **kwargs) 10 | super(**kwargs) 11 | @task_gid = task_gid 12 | end 13 | 14 | def call 15 | task = @client.tasks.get_task(task_gid: @task_gid) 16 | task.name 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_doc_update/actions/get_pr_changes_action.rb: -------------------------------------------------------------------------------- 1 | class GetPRChangesAction < Sublayer::Actions::Base 2 | def initialize(repo:, pr_number:) 3 | @repo = repo 4 | @pr_number = pr_number 5 | @client = Octokit::Client.new(access_token: ENV["GITHUB_ACCESS_TOKEN"]) 6 | end 7 | 8 | def call 9 | pr = @client.pull_request(@repo, @pr_number) 10 | files = @client.pull_request_files(@repo, @pr_number) 11 | 12 | { 13 | title: pr.title, 14 | body: pr.body, 15 | changed_files: files.map do |file| 16 | { 17 | filename: file.filename, 18 | patch: file.patch 19 | } 20 | end 21 | } 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_discord_notifier/actions/get_pr_changes_action.rb: -------------------------------------------------------------------------------- 1 | class GetPRChangesAction < Sublayer::Actions::Base 2 | def initialize(repo:, pr_number:) 3 | @repo = repo 4 | @pr_number = pr_number 5 | @client = Octokit::Client.new(access_token: ENV["GITHUB_ACCESS_TOKEN"]) 6 | end 7 | 8 | def call 9 | pr = @client.pull_request(@repo, @pr_number) 10 | files = @client.pull_request_files(@repo, @pr_number) 11 | 12 | { 13 | title: pr.title, 14 | body: pr.body, 15 | changed_files: files.map do |file| 16 | { 17 | filename: file.filename, 18 | patch: file.patch 19 | } 20 | end 21 | } 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_related_suggestion/actions/get_pr_changes_action.rb: -------------------------------------------------------------------------------- 1 | class GetPRChangesAction < Sublayer::Actions::Base 2 | def initialize(repo:, pr_number:) 3 | @repo = repo 4 | @pr_number = pr_number 5 | @client = Octokit::Client.new(access_token: ENV["GITHUB_ACCESS_TOKEN"]) 6 | end 7 | 8 | def call 9 | pr = @client.pull_request(@repo, @pr_number) 10 | files = @client.pull_request_files(@repo, @pr_number) 11 | 12 | { 13 | title: pr.title, 14 | body: pr.body, 15 | changed_files: files.map do |file| 16 | { 17 | filename: file.filename, 18 | patch: file.patch 19 | } 20 | end 21 | } 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /Asana/asana_get_task_description_action.rb: -------------------------------------------------------------------------------- 1 | # Description: Sublayer::Actions used to get the description of an Asana task. 2 | # 3 | # It is initialized with a task_gid and returns the description (called notes in Asana) of the task. 4 | # 5 | # Example usage: We've used the description of an Asana task as the base content for a prompt 6 | # so if you have a workflow where you monitor Asana tasks, you can use this to get the description for sending in to a Sublayer::Generator 7 | 8 | class AsanaGetTaskDescriptionAction < AsanaBase 9 | def initialize(task_gid:, **kwargs) 10 | super(**kwargs) 11 | @task_gid = task_gid 12 | end 13 | 14 | def call 15 | task = @client.tasks.get_task(task_gid: @task_gid) 16 | task.notes 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /Notion/notion_query_database_action.rb: -------------------------------------------------------------------------------- 1 | class NotionQueryDatabaseAction < Sublayer::Actions::Base 2 | def initialize(database_id:, filter: {}, sorts: []) 3 | @database_id = database_id 4 | @filter = filter 5 | @sorts = sorts 6 | end 7 | 8 | def call 9 | notion = Notion::Client.new(token: ENV['NOTION_API_KEY']) 10 | 11 | begin 12 | params = { database_id: @database_id } 13 | params[:filter] = @filter unless @filter.empty? 14 | params[:sorts] = @sorts unless @sorts.empty? 15 | 16 | response = notion.database_query(query) 17 | 18 | response[:results] 19 | rescue StandardError => e 20 | logger.error "Error querying Notion database: #{e.message}" 21 | raise e 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_doc_update/actions/github_add_pr_label_action.rb: -------------------------------------------------------------------------------- 1 | class GithubAddPRLabelAction < Sublayer::Actions::Base 2 | def initialize(repo:, pr_number:, label:) 3 | @repo = repo 4 | @pr_number = pr_number 5 | @label = label 6 | @client = Octokit::Client.new(access_token: ENV["GITHUB_ACCESS_TOKEN"]) 7 | end 8 | 9 | def call 10 | begin 11 | @client.add_labels_to_an_issue(@repo, @pr_number, [@label]) 12 | Sublayer.configuration.logger.log(:info, "Label '#{@label}' successfully added to PR ##{@pr_number} in #{@repo}") 13 | true 14 | rescue Octokit::Error => e 15 | error_message = "Error adding label to PR: #{e.message}" 16 | Sublayer.configuration.logger.log(:error, error_message) 17 | false 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.github/workflows/daily_action_suggestion/actions/github_add_pr_label_action.rb: -------------------------------------------------------------------------------- 1 | class GithubAddPRLabelAction < Sublayer::Actions::Base 2 | def initialize(repo:, pr_number:, label:) 3 | @repo = repo 4 | @pr_number = pr_number 5 | @label = label 6 | @client = Octokit::Client.new(access_token: ENV["GITHUB_ACCESS_TOKEN"]) 7 | end 8 | 9 | def call 10 | begin 11 | @client.add_labels_to_an_issue(@repo, @pr_number, [@label]) 12 | Sublayer.configuration.logger.log(:info, "Label '#{@label}' successfully added to PR ##{@pr_number} in #{@repo}") 13 | true 14 | rescue Octokit::Error => e 15 | error_message = "Error adding label to PR: #{e.message}" 16 | Sublayer.configuration.logger.log(:error, error_message) 17 | false 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.github/workflows/generate_specific_action/actions/github_add_pr_label_action.rb: -------------------------------------------------------------------------------- 1 | class GithubAddPRLabelAction < Sublayer::Actions::Base 2 | def initialize(repo:, pr_number:, label:) 3 | @repo = repo 4 | @pr_number = pr_number 5 | @label = label 6 | @client = Octokit::Client.new(access_token: ENV["GITHUB_ACCESS_TOKEN"]) 7 | end 8 | 9 | def call 10 | begin 11 | @client.add_labels_to_an_issue(@repo, @pr_number, [@label]) 12 | Sublayer.configuration.logger.log(:info, "Label '#{@label}' successfully added to PR ##{@pr_number} in #{@repo}") 13 | true 14 | rescue Octokit::Error => e 15 | error_message = "Error adding label to PR: #{e.message}" 16 | Sublayer.configuration.logger.log(:error, error_message) 17 | false 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_related_suggestion/actions/github_add_pr_label_action.rb: -------------------------------------------------------------------------------- 1 | class GithubAddPRLabelAction < Sublayer::Actions::Base 2 | def initialize(repo:, pr_number:, label:) 3 | @repo = repo 4 | @pr_number = pr_number 5 | @label = label 6 | @client = Octokit::Client.new(access_token: ENV["GITHUB_ACCESS_TOKEN"]) 7 | end 8 | 9 | def call 10 | begin 11 | @client.add_labels_to_an_issue(@repo, @pr_number, [@label]) 12 | Sublayer.configuration.logger.log(:info, "Label '#{@label}' successfully added to PR ##{@pr_number} in #{@repo}") 13 | true 14 | rescue Octokit::Error => e 15 | error_message = "Error adding label to PR: #{e.message}" 16 | Sublayer.configuration.logger.log(:error, error_message) 17 | false 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /Asana/asana_get_latest_comment_action.rb: -------------------------------------------------------------------------------- 1 | # Description: Sublayer::Action responsible for getting the latest comment on a task in Asana. 2 | # 3 | # It is initialized with a task_gid and returns the text of the latest comment. 4 | # 5 | # Example usage: If you have an AI agent monitoring asana tasks and performing actions based on user comments, 6 | # you can use this action to get that comment to use in a Sublayer::Generator for augmenting the prompt with human guidance 7 | 8 | class AsanaGetLatestCommentAction < AsanaBase 9 | def initialize(task_gid:, **kwargs) 10 | super(**kwargs) 11 | @task_gid = task_gid 12 | end 13 | 14 | def call 15 | task = @client.tasks.get_task(task_gid: @task_gid) 16 | comments = task.stories.select { |story| story.type == "comment" } 17 | latest_comment = comments.sort_by { |comment| comment.created_at }.last 18 | latest.comment.text 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /Github/github_get_file_contents_action.rb: -------------------------------------------------------------------------------- 1 | class GithubGetFileContentsAction < Sublayer::Actions::Base 2 | def initialize(repo:, branch: "main", file_path:) 3 | @repo = repo 4 | @branch = branch 5 | @file_path = file_path 6 | @client = Octokit::Client.new(access_token: ENV["GITHUB_ACCESS_TOKEN"]) 7 | end 8 | 9 | def call 10 | begin 11 | content = @client.contents(@repo, path: @file_path, ref: @branch) 12 | Base64.decode64(content.content) 13 | rescue Octokit::NotFound => e 14 | error_message = "File not found: \#{e.message}" 15 | Sublayer.configuration.logger.log(:error, error_message) 16 | raise StandardError, error_message 17 | rescue StandardError => e 18 | error_message = "Error fetching file contents: \#{e.message}" 19 | Sublayer.configuration.logger.log(:error, error_message) 20 | raise StandardError, error_message 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_doc_update/actions/github_get_file_contents_action.rb: -------------------------------------------------------------------------------- 1 | class GithubGetFileContentsAction < Sublayer::Actions::Base 2 | def initialize(repo:, branch: "main", file_path:) 3 | @repo = repo 4 | @branch = branch 5 | @file_path = file_path 6 | @client = Octokit::Client.new(access_token: ENV["GITHUB_ACCESS_TOKEN"]) 7 | end 8 | 9 | def call 10 | begin 11 | content = @client.contents(@repo, path: @file_path, ref: @branch) 12 | Base64.decode64(content.content) 13 | rescue Octokit::NotFound => e 14 | error_message = "File not found: \#{e.message}" 15 | Sublayer.configuration.logger.log(:error, error_message) 16 | raise StandardError, error_message 17 | rescue StandardError => e 18 | error_message = "Error fetching file contents: \#{e.message}" 19 | Sublayer.configuration.logger.log(:error, error_message) 20 | raise StandardError, error_message 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sublayer Actions 2 | Sublayer Actions are small, reusable components in the [Sublayer AI Framework](https://github.com/sublayerapp/sublayer). 3 | 4 | They are responsible for performing specific operations to either get inputs to send to an LLM inside a `Sublayer::Generator` or to 5 | perform an action in a service in response to data returned from an LLM. 6 | 7 | `Sublayer::Actions` are designed to be generated by an LLM, and to that end, this repo has many automated workflows to grow capabilities over time. 8 | 9 | So far we have: 10 | * Every day, Gemini, GPT-4, and Claude make a PR with a suggestion for a new `Sublayer::Action` 11 | * Any time a PR is merged, Gemini, GPT-4, and Claude make a new PR that's a riff off the thing that was merged 12 | * Any time a PR is merged, a message is sent to [our Discord](https://discord.gg/gge62VGH6U) in the `#actions` channel describing the change and offering some ideas for how it could be used in AI applications. 13 | 14 | Many more ideas and integrations to come! 15 | 16 | -------------------------------------------------------------------------------- /Asana/asana_create_task_action.rb: -------------------------------------------------------------------------------- 1 | # Description: Sublayer::Action responsible for creating a task in Asana. 2 | # It inherits from AsanaBase which handles authentication 3 | # 4 | # It is initialized with a project_id, name, description, and attachment, it returns the task.gid to verify it was created. 5 | # 6 | # Example usage: When you have generated a list of tasks from an LLM and want to add them to 7 | # an Asana project for a human or an AI agent to work on 8 | 9 | class AsanaCreateTaskAction < AsanaBase 10 | def initialize(project_id:, name:, description: nil, attachment: nil, **kwargs) 11 | super(**kwargs) 12 | @project_id = project_id 13 | @name = name 14 | @description = description 15 | @attachment = attachment 16 | end 17 | 18 | def call 19 | task = @client.tasks.create_task( 20 | projects: [@project_id], 21 | name: @name, 22 | notes: @description, 23 | approval_status: "pending", 24 | resource_subtype: "approval" 25 | ) 26 | 27 | task.gid 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sublayer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_discord_notifier/generators/sublayer_action_use_case_generator.rb: -------------------------------------------------------------------------------- 1 | class SublayerActionUseCaseGenerator < Sublayer::Generators::Base 2 | llm_output_adapter type: :list_of_strings, 3 | name: "use_cases", 4 | description: "List of potential use cases for the Sublayer::Action" 5 | 6 | def initialize(pr_changes:) 7 | @pr_changes = pr_changes 8 | end 9 | 10 | def prompt 11 | <<~PROMPT 12 | A PR was just merged into the Sublayer::Actions repository and we need to generate some potential use cases for the new action for inspiration. 13 | 14 | Sublayer::Actions are used similarly to tools in other agent frameworks. They're designed 15 | to be used to get information from external sources to be used in prompts or to perform actions on external services in response to 16 | responses received from AI models. 17 | 18 | The following PR was just merged: #{@pr_changes} 19 | 20 | Generate 3 potential, practical, and useful use cases for the new Sublayer::Action. 21 | 22 | Each use case should be a brief, practical example of how this action could be used in an AI automation scenario. 23 | PROMPT 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_doc_update/generators/update_actions_md_content_generator.rb: -------------------------------------------------------------------------------- 1 | class UpdateActionsMdContentGenerator < Sublayer::Generators::Base 2 | llm_output_adapter type: :single_string, 3 | name: "updated_content", 4 | description: "The updated content of the actions.md file" 5 | 6 | def initialize(pr_contents:, file_content:) 7 | @pr_contents = pr_contents 8 | @file_content = file_content 9 | end 10 | 11 | def prompt 12 | <<-PROMPT 13 | You are tasked with updating the following actions.md file with a new Sublayer::Action entry. 14 | 15 | Current content of actions.md: 16 | #{@file_content} 17 | 18 | The PR for the new entry to be added is: 19 | #{@pr_contents} 20 | 21 | Please update the actions.md content by adding the new action entry under the appropriate header. 22 | If the header does not exist, please create a new header for the action entry. 23 | 24 | Maintain the existing format and structure of the document. 25 | Ensure the new entry is added in alphabetical order within its category. 26 | 27 | Return the entire update content of the actions.md file including this new entry. 28 | PROMPT 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /.github/workflows/daily_action_suggestion/actions/get_context_action.rb: -------------------------------------------------------------------------------- 1 | class GetContextAction < Sublayer::Actions::Base 2 | def initialize(path:) 3 | @path = path 4 | end 5 | 6 | def call 7 | ignored_patterns = load_contextignore 8 | files = get_files(ignored_patterns) 9 | concatenate_files(files) 10 | end 11 | 12 | private 13 | 14 | def load_contextignore 15 | contextignore_path = File.join(@path, '.contextignore') 16 | return [] unless File.exist?(contextignore_path) 17 | 18 | File.readlines(contextignore_path).map(&:strip).reject do |line| 19 | line.empty? || line.start_with?('#') 20 | end 21 | end 22 | 23 | def get_files(ignored_patterns) 24 | Dir.chdir(@path) do 25 | all_files = `git ls-files`.split("\n") 26 | all_files.reject do |file| 27 | ignored_patterns.any? do |pattern| 28 | File.fnmatch?(pattern, file) || 29 | file.start_with?(pattern.chomp('/')) 30 | end 31 | end 32 | end 33 | end 34 | 35 | def concatenate_files(files) 36 | files.map do |file| 37 | content = File.read(File.join(@path, file)) 38 | "File: #{file}\n#{content}\n\n" 39 | end.join 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /.github/workflows/generate_specific_action/actions/get_context_action.rb: -------------------------------------------------------------------------------- 1 | class GetContextAction < Sublayer::Actions::Base 2 | def initialize(path:) 3 | @path = path 4 | end 5 | 6 | def call 7 | ignored_patterns = load_contextignore 8 | files = get_files(ignored_patterns) 9 | concatenate_files(files) 10 | end 11 | 12 | private 13 | 14 | def load_contextignore 15 | contextignore_path = File.join(@path, '.contextignore') 16 | return [] unless File.exist?(contextignore_path) 17 | 18 | File.readlines(contextignore_path).map(&:strip).reject do |line| 19 | line.empty? || line.start_with?('#') 20 | end 21 | end 22 | 23 | def get_files(ignored_patterns) 24 | Dir.chdir(@path) do 25 | all_files = `git ls-files`.split("\n") 26 | all_files.reject do |file| 27 | ignored_patterns.any? do |pattern| 28 | File.fnmatch?(pattern, file) || 29 | file.start_with?(pattern.chomp('/')) 30 | end 31 | end 32 | end 33 | end 34 | 35 | def concatenate_files(files) 36 | files.map do |file| 37 | content = File.read(File.join(@path, file)) 38 | "File: #{file}\n#{content}\n\n" 39 | end.join 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_related_suggestion/actions/get_context_action.rb: -------------------------------------------------------------------------------- 1 | class GetContextAction < Sublayer::Actions::Base 2 | def initialize(path:) 3 | @path = path 4 | end 5 | 6 | def call 7 | ignored_patterns = load_contextignore 8 | files = get_files(ignored_patterns) 9 | concatenate_files(files) 10 | end 11 | 12 | private 13 | 14 | def load_contextignore 15 | contextignore_path = File.join(@path, '.contextignore') 16 | return [] unless File.exist?(contextignore_path) 17 | 18 | File.readlines(contextignore_path).map(&:strip).reject do |line| 19 | line.empty? || line.start_with?('#') 20 | end 21 | end 22 | 23 | def get_files(ignored_patterns) 24 | Dir.chdir(@path) do 25 | all_files = `git ls-files`.split("\n") 26 | all_files.reject do |file| 27 | ignored_patterns.any? do |pattern| 28 | File.fnmatch?(pattern, file) || 29 | file.start_with?(pattern.chomp('/')) 30 | end 31 | end 32 | end 33 | end 34 | 35 | def concatenate_files(files) 36 | files.map do |file| 37 | content = File.read(File.join(@path, file)) 38 | "File: #{file}\n#{content}\n\n" 39 | end.join 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /FileSystem/write_file_action.rb: -------------------------------------------------------------------------------- 1 | # Description: Sublayer::Action responsible for writing content to a specified file path. 2 | # 3 | # This action allows for easy file writing operations within a Sublayer workflow, 4 | # enabling data persistence or creation of output files from AI-driven processes. 5 | # 6 | # It is initialized with a file_path and file_contents. 7 | # On successful execution, it writes the content to the file at the specified path. 8 | # 9 | # Example usage: When you want to save LLM-generated data to a file for later use. 10 | 11 | class WriteFileAction < Sublayer::Actions::Base 12 | def initialize(file_path:, file_contents:) 13 | @file_path = file_path 14 | @file_contents = file_contents 15 | end 16 | 17 | def call 18 | begin 19 | write_to_file 20 | Sublayer.configuration.logger.log(:info, "Successfully wrote to \\#{@file_path}") 21 | rescue IOError => e 22 | error_message = "Error writing to file: \\#{e.message}" 23 | Sublayer.configuration.logger.log(:error, error_message) 24 | raise StandardError, error_message 25 | end 26 | end 27 | 28 | private 29 | 30 | def write_to_file 31 | File.open(@file_path, 'w') do |file| 32 | file.write(@file_contents) 33 | end 34 | end 35 | end -------------------------------------------------------------------------------- /.github/workflows/merged_pr_discord_notifier.yml: -------------------------------------------------------------------------------- 1 | name: merged_pr_discord_notifier 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | workflow_dispatch: 7 | inputs: 8 | pr_number: 9 | description: "PR to analyze for discord notifications" 10 | required: true 11 | type: string 12 | 13 | jobs: 14 | merged_pr_discord_notifier: 15 | if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'ai-generated')) 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4.2.0 19 | - name: Set up Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: 3.2 23 | - name: Install dependencies 24 | run: | 25 | gem install sublayer octokit 26 | - name: Run merged_pr_discord_notifier 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} 30 | DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} 31 | PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} 32 | run: ruby .github/workflows/merged_pr_discord_notifier/merged_pr_discord_notifier.rb 33 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_discord_notifier/merged_pr_discord_notifier.rb: -------------------------------------------------------------------------------- 1 | require "base64" 2 | 3 | require "sublayer" 4 | require "octokit" 5 | 6 | # Load all Sublayer Actions, Generators, and Agents 7 | Dir[File.join(__dir__, "actions", "*.rb")].each { |file| require file } 8 | Dir[File.join(__dir__, "generators", "*.rb")].each { |file| require file } 9 | Dir[File.join(__dir__, "agents", "*.rb")].each { |file| require file } 10 | 11 | Sublayer.configuration.ai_provider = Sublayer::Providers::Gemini 12 | Sublayer.configuration.ai_model = "gemini-2.0-flash" 13 | 14 | # Add custom Github Action code below: 15 | 16 | repo = "sublayerapp/sublayer_actions" 17 | pr_number = ENV["PR_NUMBER"] 18 | 19 | pr_changes = GetPRChangesAction.new(repo: repo, pr_number: pr_number).call 20 | file_info = GetPRFileInfoAction.new(repo: repo, pr_number: pr_number).call 21 | 22 | use_cases = SublayerActionUseCaseGenerator.new(pr_changes: pr_changes).generate 23 | 24 | message = <<~MESSAGE 25 | 🎉 **New Sublayer::Action just merged: #{file_info[:file_name]}** 26 | 🔗 [View on GitHub](<#{file_info[:file_url]}>) 27 | 28 | #{pr_changes[:body]} 29 | 30 | This action is designed to be used in the following ways: 31 | #{use_cases.map { |use_case| "• #{use_case}" }.join("\n")} 32 | MESSAGE 33 | 34 | DiscordSendMessageAction.new(webhook_url: ENV["DISCORD_WEBHOOK_URL"], message: message).call 35 | -------------------------------------------------------------------------------- /Slack/slack_message_send_action.rb: -------------------------------------------------------------------------------- 1 | # Description: Sublayer::Action responsible for sending a message to a specific Slack channel or user. 2 | # It can be used for notifications or updates from AI-driven processes. 3 | # 4 | # Requires: `slack-ruby-client` gem 5 | # $ gem install slack-ruby-client 6 | # Or 7 | # add `gem "slack-ruby-client"` to your gemfile 8 | # and add `requires "slack-ruby-client"` somewhere in your app. 9 | # 10 | # It is initialized with a channel (can be a channel name or user ID) and a message. 11 | # It returns the timestamp of the sent message to confirm it was sent successfully. 12 | # 13 | # Example usage: When you want to send a notification or update from an AI process to a Slack channel or user. 14 | 15 | class SlackMessageSendAction < Sublayer::Actions::Base 16 | def initialize(channel:, message:) 17 | @channel = channel 18 | @message = message 19 | @client = Slack::Web::Client.new(token: ENV['SLACK_API_TOKEN']) 20 | end 21 | 22 | def call 23 | begin 24 | response = @client.chat_postMessage(channel: @channel, text: @message) 25 | Sublayer.configuration.logger.log(:info, "Message sent successfully to #{@channel}") 26 | response.ts 27 | rescue Slack::Web::Api::Errors::SlackError => e 28 | Sublayer.configuration.logger.log(:error, "Error sending Slack message: #{e.message}") 29 | raise e 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /.github/workflows/daily_action_suggestion.yml: -------------------------------------------------------------------------------- 1 | name: daily_action_suggestion 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 10 * * *' 7 | 8 | jobs: 9 | daily_action_suggestion: 10 | strategy: 11 | matrix: 12 | ai_provider: ["openai", "claude", "gemini"] 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code repository 16 | uses: actions/checkout@v4.2.0 17 | with: 18 | repository: sublayerapp/sublayer 19 | path: sublayer 20 | fetch-depth: 0 21 | - name: Checkout Current Repository 22 | uses: actions/checkout@v4.2.0 23 | with: 24 | path: sublayer_actions 25 | - name: Set up Ruby 26 | uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: 3.2 29 | - name: Install dependencies 30 | run: | 31 | gem install sublayer octokit 32 | - name: Run daily_action_suggestion_oai 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | GITHUB_ACCESS_TOKEN: ${{ secrets.REPOSITORY_ACCESS_TOKEN }} 36 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 37 | ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} 38 | GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} 39 | AI_PROVIDER: ${{ matrix.ai_provider }} 40 | run: ruby sublayer_actions/.github/workflows/daily_action_suggestion/daily_action_suggestion.rb 41 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_doc_update.yml: -------------------------------------------------------------------------------- 1 | name: merged_pr_doc_update 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | workflow_dispatch: 7 | inputs: 8 | pr_number: 9 | description: "PR to analyze for doc updates" 10 | required: true 11 | type: string 12 | 13 | jobs: 14 | merged_pr_doc_update: 15 | if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'ai-generated')) 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout Docs Repository 19 | uses: actions/checkout@v4.2.0 20 | with: 21 | repository: sublayerapp/sublayer_documentation 22 | path: docs 23 | fetch-depth: 0 24 | - uses: actions/checkout@v4.2.0 25 | - name: Set up Ruby 26 | uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: 3.2 29 | - name: Install dependencies 30 | run: | 31 | gem install sublayer octokit 32 | - name: Run merged_pr_doc_update 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} 36 | GITHUB_ACCESS_TOKEN: ${{ secrets.DOCS_ACCESS_TOKEN }} 37 | PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} 38 | 39 | run: ruby .github/workflows/merged_pr_doc_update/merged_pr_doc_update.rb 40 | -------------------------------------------------------------------------------- /OpenAI/openai_image_generation_action.rb: -------------------------------------------------------------------------------- 1 | require 'openai' 2 | 3 | # Description: Sublayer::Action responsible for generating images using OpenAI's DALL-E API. 4 | # This action allows easy integration of AI-generated images into Sublayer workflows, 5 | # expanding the capabilities of text-based generators. 6 | # 7 | # It is initialized with a prompt, size, and optionally the number of images to generate. 8 | # It returns an array of image URLs. 9 | # 10 | # Example usage: When you want to generate images based on text descriptions in your Sublayer workflow. 11 | 12 | class OpenAIImageGenerationAction < Sublayer::Actions::Base 13 | def initialize(prompt:, size: '1024x1024', n: 1) 14 | @prompt = prompt 15 | @size = size 16 | @n = n 17 | @client = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY']) 18 | end 19 | 20 | def call 21 | begin 22 | response = @client.images.generate(parameters: { 23 | prompt: @prompt, 24 | size: @size, 25 | n: @n 26 | }) 27 | 28 | image_urls = response['data'].map { |image| image['url'] } 29 | 30 | Sublayer.configuration.logger.log(:info, "Generated #{image_urls.size} images successfully") 31 | 32 | image_urls 33 | rescue OpenAI::Error => e 34 | error_message = "Error generating images: #{e.message}" 35 | Sublayer.configuration.logger.log(:error, error_message) 36 | raise StandardError, error_message 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /Jira/jira_get_issue_description_action.rb: -------------------------------------------------------------------------------- 1 | require 'jira-ruby' 2 | 3 | # Description: Sublayer::Action responsible for retrieving the description of a specific Jira issue. 4 | # This action allows for fetching additional context about an issue, which can be useful for analysis or further processing. 5 | # 6 | # Requires: 'jira-ruby' gem 7 | # $ gem install jira-ruby 8 | # Or add `gem 'jira-ruby'` to your Gemfile 9 | # 10 | # It is initialized with the issue_key of the Jira issue. 11 | # It returns the description of the Jira issue. 12 | # 13 | # Example usage: When you want to get the description of a Jira issue for use in an AI-driven workflow or analysis. 14 | 15 | class JiraGetIssueDescriptionAction < Sublayer::Actions::Base 16 | def initialize(issue_key:) 17 | @issue_key = issue_key 18 | @client = JIRA::Client.new( 19 | username: ENV['JIRA_USERNAME'], 20 | password: ENV['JIRA_API_TOKEN'], 21 | site: ENV['JIRA_SITE'], 22 | context_path: '', 23 | auth_type: :basic 24 | ) 25 | end 26 | 27 | def call 28 | begin 29 | issue = @client.Issue.find(@issue_key) 30 | description = issue.description 31 | Sublayer.configuration.logger.log(:info, "Successfully retrieved description for Jira issue #{@issue_key}") 32 | description 33 | rescue JIRA::HTTPError => e 34 | error_message = "Error fetching Jira issue description: #{e.message}" 35 | Sublayer.configuration.logger.log(:error, error_message) 36 | raise StandardError, error_message 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /.github/workflows/generate_specific_action.yml: -------------------------------------------------------------------------------- 1 | name: generate_specific_action 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | action_name: 7 | description: "The name of the action to generate" 8 | required: true 9 | type: string 10 | action_description: 11 | description: "A description of the action to generate" 12 | required: true 13 | type: string 14 | 15 | jobs: 16 | generate_specific_action: 17 | strategy: 18 | matrix: 19 | ai_provider: ["openai", "claude", "gemini"] 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout Current Repository 23 | uses: actions/checkout@v4.2.0 24 | with: 25 | path: sublayer_actions 26 | - name: Set up Ruby 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: 3.2 30 | - name: Install dependencies 31 | run: | 32 | gem install sublayer octokit 33 | - name: Run generate_specific_action 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | GITHUB_ACCESS_TOKEN: ${{ secrets.REPOSITORY_ACCESS_TOKEN }} 37 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 38 | ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} 39 | GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} 40 | AI_PROVIDER: ${{ matrix.ai_provider }} 41 | ACTION_DESCRIPTION: ${{ github.event.inputs.action_description }} 42 | ACTION_NAME: ${{ github.event.inputs.action_name }} 43 | run: ruby sublayer_actions/.github/workflows/generate_specific_action/generate_specific_action.rb 44 | -------------------------------------------------------------------------------- /AI_Utilities/get_context_action.rb: -------------------------------------------------------------------------------- 1 | # Sublayer::Action that generates a concatenated list of files in a directory, 2 | # respecting a .contextignore file that lists files to ignore. 3 | # 4 | # Initialized with a path to the directory to scan and returns the concatenated contents of all files in the directory 5 | # 6 | # Example usage: When you want to pass the entire contents of a directory into a Sublayer::Generator for use in a prompt. 7 | # 8 | # We use this action for the daily PRs in the sublayerapp/sublayer_actions repo and in sublayerapp/sublayer_documentation 9 | # among other places. 10 | 11 | class GetContextAction < Sublayer::Actions::Base 12 | def initialize(path:) 13 | @path = path 14 | end 15 | 16 | def call 17 | ignored_patterns = load_contextignore 18 | files = get_files(ignored_patterns) 19 | concatenate_files(files) 20 | end 21 | 22 | private 23 | 24 | def load_contextignore 25 | contextignore_path = File.join(@path, '.contextignore') 26 | return [] unless File.exist?(contextignore_path) 27 | 28 | File.readlines(contextignore_path).map(&:strip).reject do |line| 29 | line.empty? || line.start_with?('#') 30 | end 31 | end 32 | 33 | def get_files(ignored_patterns) 34 | Dir.chdir(@path) do 35 | all_files = `git ls-files`.split("\n") 36 | all_files.reject do |file| 37 | ignored_patterns.any? do |pattern| 38 | File.fnmatch?(pattern, file) || 39 | file.start_with?(pattern.chomp('/')) 40 | end 41 | end 42 | end 43 | end 44 | 45 | def concatenate_files(files) 46 | files.map do |file| 47 | content = File.read(File.join(@path, file)) 48 | "File: #{file}\n#{content}\n\n" 49 | end.join 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /Discord/discord_send_message_action.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'uri' 3 | require 'json' 4 | 5 | # Description: Sublayer::Action responsible for sending a formatted message to a Discord webhook. 6 | # This action is intended to be used for sending LLM-generated messages to a Discord channel. 7 | # 8 | # It is initialized with a webhook_url and a message (which can be formatted as per Discord's message formatting). 9 | # It returns the HTTP response code to confirm the message was sent successfully. 10 | # 11 | # Example usage: When you want to send a notification or update from an AI process to a Discord channel. 12 | 13 | class DiscordSendMessageAction < Sublayer::Actions::Base 14 | def initialize(webhook_url:, message:) 15 | @webhook_url = webhook_url 16 | @message = message 17 | end 18 | 19 | def call 20 | uri = URI.parse(@webhook_url) 21 | http = Net::HTTP.new(uri.host, uri.port) 22 | http.use_ssl = true 23 | 24 | request = Net::HTTP::Post.new(uri.request_uri) 25 | request.content_type = 'application/json' 26 | request.body = { content: @message }.to_json 27 | 28 | begin 29 | response = http.request(request) 30 | case response.code.to_i 31 | when 200..299 32 | Sublayer.configuration.logger.log(:info, "Message sent successfully to Discord webhook") 33 | response.code.to_i 34 | else 35 | error_message = "Failed to send message to Discord. HTTP Response Code: #{response.code}" 36 | Sublayer.configuration.logger.log(:error, error_message) 37 | raise StandardError, error_message 38 | end 39 | rescue StandardError => e 40 | Sublayer.configuration.logger.log(:error, "Error sending Discord message: #{e.message}") 41 | raise e 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_related_suggestion.yml: -------------------------------------------------------------------------------- 1 | name: merged_pr_related_suggestion 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | workflow_dispatch: 7 | inputs: 8 | pr_number: 9 | description: "PR number to analyze" 10 | required: true 11 | type: string 12 | 13 | jobs: 14 | merged_pr_related_suggestion: 15 | if: github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' 16 | strategy: 17 | matrix: 18 | ai_provider: ["openai", "claude", "gemini"] 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout code repository 22 | uses: actions/checkout@v4.2.0 23 | with: 24 | repository: sublayerapp/sublayer 25 | path: sublayer 26 | fetch-depth: 0 27 | - name: Checkout Current Repository 28 | uses: actions/checkout@v4.2.0 29 | with: 30 | path: sublayer_actions 31 | - name: Set up Ruby 32 | uses: ruby/setup-ruby@v1 33 | with: 34 | ruby-version: 3.2 35 | - name: Install dependencies 36 | run: | 37 | gem install sublayer octokit 38 | - name: Run merged_pr_related_suggestion 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | GITHUB_ACCESS_TOKEN: ${{ secrets.REPOSITORY_ACCESS_TOKEN }} 42 | ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} 43 | GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} 44 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 45 | AI_PROVIDER: ${{ matrix.ai_provider }} 46 | PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} 47 | run: ruby sublayer_actions/.github/workflows/merged_pr_related_suggestion/merged_pr_related_suggestion.rb 48 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_discord_notifier/actions/discord_send_message_action.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'uri' 3 | require 'json' 4 | 5 | # Description: Sublayer::Action responsible for sending a formatted message to a Discord webhook. 6 | # This action is intended to be used for sending LLM-generated messages to a Discord channel. 7 | # 8 | # It is initialized with a webhook_url and a message (which can be formatted as per Discord's message formatting). 9 | # It returns the HTTP response code to confirm the message was sent successfully. 10 | # 11 | # Example usage: When you want to send a notification or update from an AI process to a Discord channel. 12 | 13 | class DiscordSendMessageAction < Sublayer::Actions::Base 14 | def initialize(webhook_url:, message:) 15 | @webhook_url = webhook_url 16 | @message = message 17 | end 18 | 19 | def call 20 | uri = URI.parse(@webhook_url) 21 | http = Net::HTTP.new(uri.host, uri.port) 22 | http.use_ssl = true 23 | 24 | request = Net::HTTP::Post.new(uri.request_uri) 25 | request.content_type = 'application/json' 26 | request.body = { content: @message }.to_json 27 | 28 | begin 29 | response = http.request(request) 30 | case response.code.to_i 31 | when 200..299 32 | Sublayer.configuration.logger.log(:info, "Message sent successfully to Discord webhook") 33 | response.code.to_i 34 | else 35 | error_message = "Failed to send message to Discord. HTTP Response Code: #{response.code}" 36 | Sublayer.configuration.logger.log(:error, error_message) 37 | raise StandardError, error_message 38 | end 39 | rescue StandardError => e 40 | Sublayer.configuration.logger.log(:error, "Error sending Discord message: #{e.message}") 41 | raise e 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_related_suggestion/generators/related_action_ideas_generator.rb: -------------------------------------------------------------------------------- 1 | class RelatedActionIdeasGenerator < Sublayer::Generators::Base 2 | llm_output_adapter type: :list_of_named_strings, 3 | name: "action_ideas", 4 | description: "List of related Sublayer::Action ideas", 5 | item_name: "idea", 6 | attributes: [ 7 | { name: "title", description: "Title of the Sublayer::Action idea" }, 8 | { name: "description", description: "Description of the Sublayer::Action idea" }, 9 | { name: "usefulness_score", description: "A score from 1-10 indicating how useful this action might be" } 10 | ] 11 | 12 | def initialize(pr_changes:, existing_actions:) 13 | @pr_changes = pr_changes 14 | @existing_actions = existing_actions 15 | end 16 | 17 | def generate 18 | super 19 | end 20 | 21 | def prompt 22 | <<-PROMPT 23 | You are an expert Ruby developer specializing in creating Sublayer::Actions. 24 | 25 | Sublayer::Actions are small, reusable, but very specific units of functionality that can be combined to create more complex workflows. 26 | 27 | Based on the following pull request changes and existing Sublayer::Actions, 28 | generate 5 ideas for new, related Sublayer::Actions that could be useful. 29 | 30 | Pull request changes: 31 | #{@pr_changes} 32 | 33 | Existing Sublayer::Actions: 34 | #{@existing_actions} 35 | 36 | For each idea, provide: 37 | - title: concise title for the Sublayer::Action 38 | - description: A brief description of what the action does and why it's useful 39 | - usefulness_score: A score from 1-10 indicating how useful this action might be 40 | 41 | Focus on cre3ating actions that complement or extend the functionality introduced in the PR changes. 42 | PROMPT 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /.github/workflows/daily_action_suggestion/generators/action_ideas_generator.rb: -------------------------------------------------------------------------------- 1 | class ActionIdeasGenerator < Sublayer::Generators::Base 2 | llm_output_adapter type: :list_of_named_strings, 3 | name: "action_ideas", 4 | description: "List of sublayer action ideas with usefulness scores", 5 | item_name: "idea", 6 | attributes: [ 7 | { name: "title", description: "The title of the action idea" }, 8 | { name: "description", description: "A brief description of the action" }, 9 | { name: "usefulness_score", description: "A score from 1-10 indicating the usefulness of the action with 10 being the best" } 10 | ] 11 | 12 | def initialize(code_context:, action_code_context:) 13 | @code_context = code_context 14 | @action_code_context = action_code_context 15 | end 16 | 17 | def generate 18 | super 19 | end 20 | 21 | def prompt 22 | <<-PROMPT 23 | You are an AI assistant tasked with generating ideas for new Sublayer actions. 24 | 25 | Sublayer actions are small, reusable but very specific units of functionality that can be combined to create more complex workflows. 26 | 27 | In the sublayer repo we have examples of using them in tests and you can see the base class. 28 | 29 | Sublayer repo contents: 30 | #{@code_context} 31 | 32 | Below is a repo of existing sublayer actions we've found to be useful in the past. 33 | 34 | Note how they are small individual things you want to do with a specific service. 35 | The idea is the more specific and single responsibility they are the more easily reuable they are. 36 | 37 | Existing Sublayer actions: 38 | #{@action_code_context} 39 | 40 | Please generate a list of 5 ideas for new Sublayer actions that would be useful additions to the existing set. 41 | For each idea, provide: 42 | - A title 43 | - A brief description 44 | - A usefulness score from 1-10 45 | 46 | Focus on actions that would be generally useful and align well with Sublayer's goals and existing functionality. 47 | PROMPT 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_doc_update/merged_pr_doc_update.rb: -------------------------------------------------------------------------------- 1 | require "base64" 2 | 3 | require "sublayer" 4 | require "octokit" 5 | 6 | # Load all Sublayer Actions, Generators, and Agents 7 | Dir[File.join(__dir__, "actions", "*.rb")].each { |file| require file } 8 | Dir[File.join(__dir__, "generators", "*.rb")].each { |file| require file } 9 | Dir[File.join(__dir__, "agents", "*.rb")].each { |file| require file } 10 | 11 | Sublayer.configuration.ai_provider = Sublayer::Providers::Gemini 12 | Sublayer.configuration.ai_model = "gemini-2.0-flash" 13 | 14 | # Add custom Github Action code below: 15 | 16 | pr_number = ENV["PR_NUMBER"] 17 | 18 | # Get Actions resources markdown file 19 | file_content = GithubGetFileContentsAction.new(repo: "sublayerapp/sublayer_documentation", file_path: "docs/resources/actions.md").call 20 | pr_contents = GetPRChangesAction.new(repo: "sublayerapp/sublayer_actions", pr_number: pr_number).call 21 | updated_content = UpdateActionsMdContentGenerator.new(pr_contents: pr_contents, file_content: file_content).generate 22 | 23 | branch_name = "pr-doc-update-#{pr_number}-#{Time.now.strftime("%Y-%m-%d-%H-%M-%S")}" 24 | 25 | GithubCreateBranchAction.new( 26 | repo: "sublayerapp/sublayer_documentation", 27 | base_branch: "main", 28 | new_branch: branch_name 29 | ).call 30 | 31 | GithubModifyFileAction.new( 32 | repo: "sublayerapp/sublayer_documentation", 33 | branch: branch_name, 34 | file_path: "docs/resources/actions.md", 35 | file_content: updated_content 36 | ).call 37 | 38 | new_pr = GithubCreatePRAction.new( 39 | repo: "sublayerapp/sublayer_documentation", 40 | base: "main", 41 | head: branch_name, 42 | title: "Add new Sublayer::Action to resources from PR ##{pr_number}", 43 | body: "This PR adds a new Sublayer::Action to the resources documentation." 44 | ).call 45 | 46 | GithubAddPRLabelAction.new(repo: "sublayerapp/sublayer_documentation", pr_number: new_pr.number, label: "ai-generated").call 47 | GithubAddPRLabelAction.new(repo: "sublayerapp/sublayer_documentation", pr_number: new_pr.number, label: "gemini").call 48 | -------------------------------------------------------------------------------- /.github/workflows/daily_action_suggestion/generators/action_generator.rb: -------------------------------------------------------------------------------- 1 | class ActionGenerator < Sublayer::Generators::Base 2 | llm_output_adapter type: :named_strings, 3 | name: "action", 4 | description: "Generated Sublayer action", 5 | attributes: [ 6 | { name: "file_path", description: "The full path and filename (ending with _action.rb) for the new action" }, 7 | { name: "content", description: "The content of the new action file" } 8 | ] 9 | 10 | def initialize(idea:, action_examples:) 11 | @idea = idea 12 | @action_examples = action_examples 13 | end 14 | 15 | def generate 16 | super 17 | end 18 | 19 | def prompt 20 | <<-PROMPT 21 | Sublayer Actions are small, reusable classes for use in the Sublayer AI framework. 22 | They are designed to either get information from somewhere for sending to an LLM or for using information that was received by an LLM in some way. 23 | 24 | The following are examples of working Sublayer actions that are currently in use in different ways in the sublayer_actions repository: 25 | #{@action_examples} 26 | 27 | You are an AI assistant tasked with creating a new Sublayer action based on the following idea: 28 | 29 | Title: #{@idea.title} 30 | Description: #{@idea.description} 31 | 32 | Please generate a Sublayer action that implements this idea. The action should: 33 | 1. Inherit from Sublayer::Actions::Base 34 | 2. Have a meaningful name that reflects its purpose 35 | 3. Implement the necessary methods (e.g., initialize, call) 36 | 4. Include relevant error handling and logging 37 | 5. Follow Ruby best practices and conventions 38 | 6. Follow the convention of being stored in a folder named after the service or category it belongs to the way they are structured in the sublayer_actions repository above. 39 | 40 | Provide your response as: 41 | 1. The full path and filename for the action (should end with _action.rb) 42 | 2. The complete content of the action file 43 | 44 | Remember to make the action as specific as possible while keeping it simple to use. 45 | PROMPT 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /.github/workflows/generate_specific_action/generators/action_generator.rb: -------------------------------------------------------------------------------- 1 | class ActionGenerator < Sublayer::Generators::Base 2 | llm_output_adapter type: :named_strings, 3 | name: "action", 4 | description: "Generated Sublayer action", 5 | attributes: [ 6 | { name: "file_path", description: "The full path and filename (ending with _action.rb) for the new action" }, 7 | { name: "content", description: "The content of the new action file" } 8 | ] 9 | 10 | def initialize(idea:, action_examples:) 11 | @idea = idea 12 | @action_examples = action_examples 13 | end 14 | 15 | def generate 16 | super 17 | end 18 | 19 | def prompt 20 | <<-PROMPT 21 | Sublayer Actions are small, reusable classes for use in the Sublayer AI framework. 22 | They are designed to either get information from somewhere for sending to an LLM or for using information that was received by an LLM in some way. 23 | 24 | The following are examples of working Sublayer actions that are currently in use in different ways in the sublayer_actions repository: 25 | #{@action_examples} 26 | 27 | You are an AI assistant tasked with creating a new Sublayer action based on the following idea: 28 | 29 | Title: #{@idea.title} 30 | Description: #{@idea.description} 31 | 32 | Please generate a Sublayer action that implements this idea. The action should: 33 | 1. Inherit from Sublayer::Actions::Base 34 | 2. Have a meaningful name that reflects its purpose 35 | 3. Implement the necessary methods (e.g., initialize, call) 36 | 4. Include relevant error handling and logging 37 | 5. Follow Ruby best practices and conventions 38 | 6. Follow the convention of being stored in a folder named after the service or category it belongs to the way they are structured in the sublayer_actions repository above. 39 | 40 | Provide your response as: 41 | 1. The full path and filename for the action (should end with _action.rb) 42 | 2. The complete content of the action file 43 | 44 | Remember to make the action as specific as possible while keeping it simple to use. 45 | PROMPT 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_related_suggestion/generators/action_generator.rb: -------------------------------------------------------------------------------- 1 | class ActionGenerator < Sublayer::Generators::Base 2 | llm_output_adapter type: :named_strings, 3 | name: "action", 4 | description: "Generated Sublayer action", 5 | attributes: [ 6 | { name: "file_path", description: "The full path and filename (ending with _action.rb) for the new action" }, 7 | { name: "content", description: "The content of the new action file" } 8 | ] 9 | 10 | def initialize(idea:, action_examples:) 11 | @idea = idea 12 | @action_examples = action_examples 13 | end 14 | 15 | def generate 16 | super 17 | end 18 | 19 | def prompt 20 | <<-PROMPT 21 | Sublayer Actions are small, reusable classes for use in the Sublayer AI framework. 22 | They are designed to either get information from somewhere for sending to an LLM or for using information that was received by an LLM in some way. 23 | 24 | The following are examples of working Sublayer actions that are currently in use in different ways in the sublayer_actions repository: 25 | #{@action_examples} 26 | 27 | You are an AI assistant tasked with creating a new Sublayer action based on the following idea: 28 | 29 | Title: #{@idea.title} 30 | Description: #{@idea.description} 31 | 32 | Please generate a Sublayer action that implements this idea. The action should: 33 | 1. Inherit from Sublayer::Actions::Base 34 | 2. Have a meaningful name that reflects its purpose 35 | 3. Implement the necessary methods (e.g., initialize, call) 36 | 4. Include relevant error handling and logging 37 | 5. Follow Ruby best practices and conventions 38 | 6. Follow the convention of being stored in a folder named after the service or category it belongs to the way they are structured in the sublayer_actions repository above. 39 | 40 | Provide your response as: 41 | 1. The full path and filename for the action (should end with _action.rb) 42 | 2. The complete content of the action file 43 | 44 | Remember to make the action as specific as possible while keeping it simple to use. 45 | PROMPT 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /Jira/jira_get_project_issues_action.rb: -------------------------------------------------------------------------------- 1 | require 'jira-ruby' 2 | 3 | # Description: Sublayer::Action responsible for retrieving a list of issues from a Jira project's backlog. 4 | # This action allows for integration with Jira, a popular project management tool. 5 | # It can be used to fetch issues for analysis, reporting, or further processing in AI-driven workflows. 6 | # 7 | # Requires: 'jira-ruby' gem 8 | # $ gem install jira-ruby 9 | # Or add `gem 'jira-ruby'` to your Gemfile 10 | # 11 | # It is initialized with a project_key and optional parameters for filtering and sorting. 12 | # It returns an array of Jira issues from the project's backlog. 13 | # 14 | # Example usage: When you want to analyze or process issues in a Jira project's backlog as part of an AI workflow. 15 | 16 | class JiraGetProjectIssuesAction < Sublayer::Actions::Base 17 | def initialize(project_key:, max_results: 50, start_at: 0, jql_filter: nil) 18 | @project_key = project_key 19 | @max_results = max_results 20 | @start_at = start_at 21 | @jql_filter = "project = #{@project_key}" 22 | @jql_filter = @jql_filter + " && " +jql_filter if jql_filter 23 | @client = JIRA::Client.new( 24 | username: ENV['JIRA_USERNAME'], 25 | password: ENV['JIRA_API_TOKEN'], 26 | site: ENV['JIRA_SITE'], 27 | context_path: '', 28 | auth_type: :basic 29 | ) 30 | end 31 | 32 | def call 33 | begin 34 | issues = fetch_issues 35 | Sublayer.configuration.logger.log(:info, "Successfully retrieved #{issues.count} issues from Jira project #{@project_key}") 36 | issues 37 | rescue JIRA::HTTPError => e 38 | error_message = "Error fetching Jira issues: #{e.message}" 39 | Sublayer.configuration.logger.log(:error, error_message) 40 | raise StandardError, error_message 41 | end 42 | end 43 | 44 | private 45 | 46 | def fetch_issues 47 | options = { 48 | max_results: @max_results, 49 | start_at: @start_at, 50 | fields: [:key, :summary, :status, :issuetype, :priority, :created, :updated], 51 | validate_query: true 52 | } 53 | 54 | @client.Issue.jql(@jql_filter, options) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /.github/workflows/generate_specific_action/generate_specific_action.rb: -------------------------------------------------------------------------------- 1 | require "base64" 2 | require "ostruct" 3 | 4 | require "sublayer" 5 | require "octokit" 6 | 7 | # Load all Sublayer Actions, Generators, and Agents 8 | Dir[File.join(__dir__, "actions", "*.rb")].each { |file| require file } 9 | Dir[File.join(__dir__, "generators", "*.rb")].each { |file| require file } 10 | Dir[File.join(__dir__, "agents", "*.rb")].each { |file| require file } 11 | 12 | case ENV["AI_PROVIDER"] 13 | when "openai" 14 | Sublayer.configuration.ai_provider = Sublayer::Providers::OpenAI 15 | Sublayer.configuration.ai_model = "gpt-4o" 16 | when "gemini" 17 | Sublayer.configuration.ai_provider = Sublayer::Providers::Gemini 18 | Sublayer.configuration.ai_model = "gemini-2.0-flash" 19 | when "claude" 20 | Sublayer.configuration.ai_provider = Sublayer::Providers::Claude 21 | Sublayer.configuration.ai_model = "claude-3-5-sonnet-latest" 22 | end 23 | 24 | # Add custom Github Action code below: 25 | repo = "sublayerapp/sublayer_actions" 26 | 27 | existing_actions = GetContextAction.new(path: "#{ENV['GITHUB_WORKSPACE']}/sublayer_actions").call 28 | 29 | requested_action = OpenStruct.new(title: ENV["ACTION_NAME"], description: ENV["ACTION_DESCRIPTION"]) 30 | new_action = ActionGenerator.new(idea: requested_action, action_examples: existing_actions).generate 31 | 32 | branch_name = "requested-action-#{ENV["AI_PROVIDER"]}-#{Time.now.strftime("%Y-%m-%d-%H-%M-%S")}" 33 | 34 | GithubCreateBranchAction.new( 35 | repo: repo, 36 | base_branch: "main", 37 | new_branch: branch_name 38 | ).call 39 | 40 | GithubCreateFileAction.new( 41 | repo: repo, 42 | branch: branch_name, 43 | file_path: new_action.file_path, 44 | file_content: new_action.content 45 | ).call 46 | 47 | new_pr = GithubCreatePRAction.new( 48 | repo: repo, 49 | base: "main", 50 | head: branch_name, 51 | title: "New Sublayer::Action from #{ENV["AI_PROVIDER"]}: #{requested_action.title}", 52 | body: requested_action.description 53 | ).call 54 | 55 | GithubAddPRLabelAction.new(repo: repo, pr_number: new_pr.number, label: "ai-generated").call 56 | GithubAddPRLabelAction.new(repo: repo, pr_number: new_pr.number, label: ENV["AI_PROVIDER"]).call 57 | -------------------------------------------------------------------------------- /Jira/jira_create_issue_action.rb: -------------------------------------------------------------------------------- 1 | require 'jira-ruby' 2 | 3 | # Description: Sublayer::Action responsible for creating an issue in Jira. 4 | # This action allows for integration with Jira, a popular project management tool. 5 | # It can be used to automatically create tasks based on AI-generated insights or code analysis. 6 | # 7 | # Requires: 'jira-ruby' gem 8 | # $ gem install jira-ruby 9 | # Or add `gem 'jira-ruby'` to your Gemfile 10 | # 11 | # It is initialized with project_key, issue_type, summary, and optional description and custom_fields. 12 | # It returns the key of the created Jira issue. 13 | # 14 | # Example usage: When you want to create a Jira issue based on AI-generated insights or automated processes. 15 | 16 | class JiraCreateIssueAction < Sublayer::Actions::Base 17 | def initialize(project_key:, issue_type:, summary:, description: nil, custom_fields: {}) 18 | @project_key = project_key 19 | @issue_type = issue_type 20 | @summary = summary 21 | @description = description 22 | @custom_fields = custom_fields 23 | @client = JIRA::Client.new( 24 | username: ENV['JIRA_USERNAME'], 25 | password: ENV['JIRA_API_TOKEN'], 26 | site: ENV['JIRA_SITE'], 27 | context_path: '', 28 | auth_type: :basic 29 | ) 30 | end 31 | 32 | def call 33 | begin 34 | issue = create_issue 35 | Sublayer.configuration.logger.log(:info, "Jira issue created successfully: #{issue.id}") 36 | return issue 37 | rescue JIRA::HTTPError => e 38 | error_message = "Error creating Jira issue: #{e.message}" 39 | Sublayer.configuration.logger.log(:error, error_message) 40 | raise StandardError, error_message 41 | end 42 | end 43 | 44 | private 45 | 46 | def create_issue 47 | issue = @client.Issue.build 48 | 49 | issue_params = { 50 | 'fields' => { 51 | 'project' => { 'key' => @project_key }, 52 | 'summary' => @summary, 53 | 'issuetype' => { 'name' => @issue_type }, 54 | 'description' => @description 55 | } 56 | } 57 | 58 | @custom_fields.each do |field_id, value| 59 | issue_params['fields'][field_id] = value 60 | end 61 | 62 | issue.save(issue_params) 63 | return issue 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /.github/workflows/merged_pr_related_suggestion/merged_pr_related_suggestion.rb: -------------------------------------------------------------------------------- 1 | require "base64" 2 | 3 | require "sublayer" 4 | require "octokit" 5 | 6 | # Load all Sublayer Actions, Generators, and Agents 7 | Dir[File.join(__dir__, "actions", "*.rb")].each { |file| require file } 8 | Dir[File.join(__dir__, "generators", "*.rb")].each { |file| require file } 9 | Dir[File.join(__dir__, "agents", "*.rb")].each { |file| require file } 10 | 11 | case ENV["AI_PROVIDER"] 12 | when "openai" 13 | Sublayer.configuration.ai_provider = Sublayer::Providers::OpenAI 14 | Sublayer.configuration.ai_model = "gpt-4o" 15 | when "gemini" 16 | Sublayer.configuration.ai_provider = Sublayer::Providers::Gemini 17 | Sublayer.configuration.ai_model = "gemini-2.0-flash" 18 | when "claude" 19 | Sublayer.configuration.ai_provider = Sublayer::Providers::Claude 20 | Sublayer.configuration.ai_model = "claude-3-5-sonnet-latest" 21 | end 22 | 23 | repo = "sublayerapp/sublayer_actions" 24 | pr_number = ENV["PR_NUMBER"] 25 | 26 | pr_changes = GetPRChangesAction.new(repo: repo, pr_number: pr_number).call 27 | 28 | existing_actions = GetContextAction.new(path: "#{ENV['GITHUB_WORKSPACE']}/sublayer_actions").call 29 | 30 | action_ideas = RelatedActionIdeasGenerator.new(pr_changes: pr_changes, existing_actions: existing_actions).generate 31 | 32 | best_idea = action_ideas.max_by { |idea| idea.usefulness_score.to_i } 33 | 34 | new_action = ActionGenerator.new(idea: best_idea, action_examples: existing_actions).generate 35 | 36 | branch_name = "pr-related-suggestion-#{pr_number}-#{ENV["AI_PROVIDER"]}-#{Time.now.strftime("%Y-%m-%d-%H-%M-%S")}" 37 | 38 | GithubCreateBranchAction.new( 39 | repo: repo, 40 | base_branch: "main", 41 | new_branch: branch_name 42 | ).call 43 | 44 | GithubCreateFileAction.new( 45 | repo: repo, 46 | branch: branch_name, 47 | file_path: new_action.file_path, 48 | file_content: new_action.content 49 | ).call 50 | 51 | new_pr = GithubCreatePRAction.new( 52 | repo: repo, 53 | base: "main", 54 | head: branch_name, 55 | title: "New Sublayer::Action from #{ENV["AI_PROVIDER"]}: #{best_idea.title}", 56 | body: best_idea.description 57 | ).call 58 | 59 | GithubAddPRLabelAction.new(repo: repo, pr_number: new_pr.number, label: "ai-generated").call 60 | GithubAddPRLabelAction.new(repo: repo, pr_number: new_pr.number, label: ENV["AI_PROVIDER"]).call 61 | -------------------------------------------------------------------------------- /.github/workflows/daily_action_suggestion/daily_action_suggestion.rb: -------------------------------------------------------------------------------- 1 | require "base64" 2 | 3 | require "sublayer" 4 | require "octokit" 5 | 6 | # Load all Sublayer Actions, Generators, and Agents 7 | Dir[File.join(__dir__, "actions", "*.rb")].each { |file| require file } 8 | Dir[File.join(__dir__, "generators", "*.rb")].each { |file| require file } 9 | Dir[File.join(__dir__, "agents", "*.rb")].each { |file| require file } 10 | 11 | case ENV["AI_PROVIDER"] 12 | when "openai" 13 | Sublayer.configuration.ai_provider = Sublayer::Providers::OpenAI 14 | Sublayer.configuration.ai_model = "gpt-4o" 15 | when "gemini" 16 | Sublayer.configuration.ai_provider = Sublayer::Providers::Gemini 17 | Sublayer.configuration.ai_model = "gemini-2.0-flash" 18 | when "claude" 19 | Sublayer.configuration.ai_provider = Sublayer::Providers::Claude 20 | Sublayer.configuration.ai_model = "claude-3-5-sonnet-latest" 21 | end 22 | 23 | Sublayer.configuration.logger = Sublayer::Logging::DebugLogger.new 24 | 25 | # Add custom Github Action code below: 26 | 27 | code_repo_path = "#{ENV['GITHUB_WORKSPACE']}/sublayer" 28 | action_repo_path = "#{ENV['GITHUB_WORKSPACE']}/sublayer_actions" 29 | 30 | code_context = GetContextAction.new(path: code_repo_path).call 31 | action_code_context = GetContextAction.new(path: action_repo_path).call 32 | 33 | action_ideas = ActionIdeasGenerator.new(code_context: code_context, action_code_context: action_code_context).generate 34 | best_idea = action_ideas.sort_by { |idea| idea.usefulness_score.to_i }.reverse.first 35 | new_action = ActionGenerator.new(idea: best_idea, action_examples: action_code_context).generate 36 | 37 | branch_name = "daily-action-suggestion-#{ENV["AI_PROVIDER"]}-#{Time.now.strftime("%Y-%m-%d-%H-%M-%S")}" 38 | repo = "sublayerapp/sublayer_actions" 39 | 40 | GithubCreateBranchAction.new( repo: repo, base_branch: "main", new_branch: branch_name).call 41 | GithubCreateFileAction.new( repo: repo, branch: branch_name, file_path: new_action.file_path, file_content: new_action.content).call 42 | new_pr = GithubCreatePRAction.new( repo: repo, base: "main", head: branch_name, title: "(#{ENV["AI_PROVIDER"]}): #{best_idea.title}", body: best_idea.description).call 43 | 44 | GithubAddPRLabelAction.new(repo: repo, pr_number: new_pr.number, label: "ai-generated").call 45 | GithubAddPRLabelAction.new(repo: repo, pr_number: new_pr.number, label: ENV["AI_PROVIDER"]).call 46 | -------------------------------------------------------------------------------- /AzureDevops/azure_devops_create_epic_action.rb: -------------------------------------------------------------------------------- 1 | require 'httparty' 2 | 3 | # Description: Sublayer::Action responsible for creating an epic in Azure DevOps. 4 | # This action integrates with Azure DevOps using the REST API and HTTParty. 5 | # 6 | # It is initialized with the organization, project, epic_title, and optionally, description and area_path. 7 | # It returns the ID of the created epic. 8 | # 9 | # Example usage: When you want to create a new epic in Azure DevOps based on AI-generated insights or project requirements. 10 | 11 | class AzureDevopsCreateEpicAction < Sublayer::Actions::Base 12 | include HTTParty 13 | format :json 14 | 15 | def initialize(organization:, project:, epic_title:, description: '') 16 | @organization = organization 17 | @project = project 18 | @epic_title = epic_title 19 | @description = description 20 | @personal_access_token = ENV['AZURE_DEVOPS_PAT'] 21 | end 22 | 23 | def call 24 | create_epic 25 | rescue HTTParty::Error => e 26 | error_message = "HTTP error during epic creation: #{e.message}" 27 | Sublayer.configuration.logger.log(:error, error_message) 28 | raise StandardError, error_message 29 | rescue StandardError => e 30 | error_message = "Error creating Azure DevOps epic: #{e.message}" 31 | Sublayer.configuration.logger.log(:error, error_message) 32 | raise e 33 | end 34 | 35 | private 36 | 37 | def create_epic 38 | url = "https://dev.azure.com/#{@organization}/#{@project}/_apis/wit/workitems/$Epic?api-version=7.1" 39 | headers = { 40 | "Content-Type" => "application/json-patch+json", 41 | "Authorization" => "Basic #{Base64.strict_encode64(':' + @personal_access_token)}" 42 | } 43 | 44 | body = [ 45 | { "op" => "add", "path" => "/fields/System.Title", "value" => @epic_title }, 46 | { "op" => "add", "path" => "/fields/System.Description", "value" => @description } 47 | ] 48 | 49 | response = self.class.post(url, headers: headers, body: body.to_json) 50 | 51 | if response.success? 52 | epic_id = response.parsed_response['id'] 53 | Sublayer.configuration.logger.log(:info, "Epic created successfully in Azure DevOps with ID: #{epic_id}") 54 | epic_id 55 | else 56 | error_message = "Failed to create epic: HTTP #{response.code} - #{response.message}" 57 | Sublayer.configuration.logger.log(:error, error_message) 58 | raise StandardError, error_message 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /AzureDevops/azure_devops_create_epic_issue_action.rb: -------------------------------------------------------------------------------- 1 | require 'httparty' 2 | 3 | # Description: Sublayer::Action responsible for creating an issue within an existing epic in Azure DevOps. 4 | # This action integrates with Azure DevOps using the REST API and HTTParty. 5 | # 6 | # It is initialized with the organization, project, epic_id, issue_title, and optionally, description and area_path. 7 | # It returns the ID of the created issue. 8 | # 9 | # Example usage: When you want to add new issues to an epic in Azure DevOps based on AI-generated insights or project requirements. 10 | 11 | class AzureDevopsCreateEpicIssueAction < Sublayer::Actions::Base 12 | include HTTParty 13 | format :json 14 | 15 | def initialize(organization:, project:, epic_id:, issue_title:, description: '') 16 | @organization = organization 17 | @project = project 18 | @epic_id = epic_id 19 | @issue_title = issue_title 20 | @description = description 21 | @personal_access_token = ENV['AZURE_DEVOPS_PAT'] 22 | end 23 | 24 | def call 25 | create_issue_within_epic 26 | rescue HTTParty::Error => e 27 | error_message = "HTTP error during issue creation: #{e.message}" 28 | Sublayer.configuration.logger.log(:error, error_message) 29 | raise StandardError, error_message 30 | rescue StandardError => e 31 | error_message = "Error creating Azure DevOps issue: #{e.message}" 32 | Sublayer.configuration.logger.log(:error, error_message) 33 | raise e 34 | end 35 | 36 | private 37 | 38 | def create_issue_within_epic 39 | url = "https://dev.azure.com/#{@organization}/#{@project}/_apis/wit/workitems/$Issue?api-version=7.1" 40 | headers = { 41 | "Content-Type" => "application/json-patch+json", 42 | "Authorization" => "Basic #{Base64.strict_encode64(':' + @personal_access_token)}" 43 | } 44 | 45 | body = [ 46 | { "op" => "add", "path" => "/fields/System.Title", "value" => @issue_title }, 47 | { "op" => "add", "path" => "/fields/System.Description", "value" => @description }, 48 | { "op" => "add", "path" => "/relations/-", "value" => { "rel" => "System.LinkTypes.Hierarchy-Reverse", "url" => "https://dev.azure.com/#{@organization}/#{@project}/_apis/wit/workitems/#{@epic_id}" }} 49 | ] 50 | 51 | response = self.class.post(url, headers: headers, body: body.to_json) 52 | 53 | if response.success? 54 | issue_id = response.parsed_response['id'] 55 | Sublayer.configuration.logger.log(:info, "Issue created successfully in Azure DevOps with ID: #{issue_id}") 56 | issue_id 57 | else 58 | error_message = "Failed to create issue: HTTP #{response.code} - #{response.message}" 59 | Sublayer.configuration.logger.log(:error, error_message) 60 | raise StandardError, error_message 61 | end 62 | end 63 | end 64 | --------------------------------------------------------------------------------