-
-
Save jonmagic/83445be775791b7d3a5cc6c97641dc57 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module Commands | |
| class Snippets | |
| def self.usage | |
| "snippets [options]" | |
| end | |
| def self.description | |
| [ | |
| "Generate snippets for leadership based on the conversations you participated in during the designated time period.", | |
| ].join("\n") | |
| end | |
| def initialize(date_range: nil, model: nil) | |
| @date_range_input = date_range | |
| @model = model | |
| @github_login = nil | |
| end | |
| attr_accessor :github_login | |
| def run | |
| $GLOBAL_DEBUG = true | |
| date_range = parse_date_range | |
| github_client = setup_github_client | |
| require_relative "../llm_client" | |
| self.github_login = github_client.user.login | |
| output_dir = setup_output_dir(date_range) | |
| activity_urls = load_or_compute( | |
| path: File.join(output_dir, "01_activity_urls.json"), | |
| compute: -> { fetch_activity_urls(date_range, client: github_client) } | |
| ) | |
| raw_data = load_or_compute( | |
| path: File.join(output_dir, "02_raw_data.json"), | |
| compute: -> { fetch_data_for_urls(activity_urls, client: github_client) } | |
| ) | |
| filtered_data = load_or_compute( | |
| path: File.join(output_dir, "03_filtered_data.json"), | |
| compute: -> { filter_data(raw_data, date_range) } | |
| ) | |
| executive_summaries = load_or_compute( | |
| path: File.join(output_dir, "04_executive_summaries.json"), | |
| compute: -> { generate_executive_summaries(filtered_data) } | |
| ) | |
| require "erb" | |
| classifications_data = load_or_compute( | |
| path: File.join(output_dir, "05_classifications.json"), | |
| compute: -> { classify_data(filtered_data) } | |
| ) | |
| grouped_data = load_or_compute( | |
| path: File.join(output_dir, "06_grouped_data.json"), | |
| compute: -> { build_groups_from_classified_data(classifications_data) } | |
| ) | |
| snippets_data = load_or_compute( | |
| path: File.join(output_dir, "07_snippets.json"), | |
| compute: -> { generate_snippets(executive_summaries:, classifications_data:, grouped_data:) } | |
| ) | |
| # sorted_snippets_data = load_or_compute( | |
| # path: File.join(output_dir, "08_sorted_snippets.json"), | |
| # compute: -> { sort_and_limit_snippets(snippets_data:) } | |
| # ) | |
| load_or_compute( | |
| path: File.join(output_dir, "09_snippets.md"), | |
| compute: -> { print_snippets(snippets_data) }, | |
| json: false | |
| ) | |
| rescue ArgumentError => e | |
| abort("Error: #{e.message}") | |
| end | |
| SECTIONS = { | |
| "Key Ships & Accomplishments" => { | |
| header: ":ship: Key Ships & Accomplishments", | |
| template: "key_ships_and_accomplishments.md" | |
| }, | |
| "Risks and Themes" => { | |
| header: ":warning: Risks and Themes", | |
| template: "risks_and_themes.md" | |
| }, | |
| "Blockers" => { | |
| header: ":red_circle: Blockers", | |
| template: "blockers.md" | |
| }, | |
| "Ideas" => { | |
| header: ":bulb: Ideas", | |
| template: "ideas.md" | |
| }, | |
| "Collaborations" => { | |
| header: ":fist_right: :fist_left: Collaborations", | |
| template: "collaborations.md" | |
| }, | |
| "Shoutouts" => { | |
| header: ":sparkles: Shoutouts", | |
| template: "shoutouts.md" | |
| } | |
| }.freeze | |
| IMPACT_RANK = { | |
| "high" => 1, | |
| "medium" => 2, | |
| "low" => 3 | |
| }.freeze | |
| private def llm_client | |
| @llm_client ||= @model ? LlmClient[@model] : LlmClient.default | |
| end | |
| private def parse_date_range | |
| require_relative "../date_range" | |
| DateRange.new(@date_range_input || "#{Date.today - 7}..#{Date.today}") | |
| end | |
| private def setup_github_client | |
| require_relative "../github_client" | |
| GithubClient.client | |
| end | |
| private def setup_output_dir(date_range) | |
| dir = File.join(Dir.pwd, "projects", "snippets", "#{date_range.start_date}-to-#{date_range.end_date}") | |
| if File.directory?(dir) | |
| puts "Directory already exists: #{dir}" | |
| else | |
| require "fileutils" | |
| FileUtils.mkdir_p(dir) | |
| puts "Created directory: #{dir}" | |
| end | |
| dir | |
| end | |
| private def fetch_activity_urls(date_range, client:) | |
| puts "Fetching activity URLs..." | |
| require_relative "../github_search" | |
| query = "created:#{date_range.start_date}..#{date_range.end_date}" | |
| role_types = ["author", "commenter", "reviewer", "assignee", "mention", "involves"] | |
| activity_urls = [] | |
| if query !~ /\b(?:author|commenter|reviewer|assignee|mention|involves):\S+/ | |
| role_types.each do |role| | |
| modified_query = "#{query} #{role}:#{github_login}" | |
| activity_urls += GithubSearch.new(query: modified_query, client: client).fetch_data.map { |url, _| url } | |
| end | |
| else | |
| activity_urls = GithubSearch.new(query: query, client: client).fetch_data.map { |url, _| url } | |
| end | |
| activity_urls | |
| end | |
| private def fetch_data_for_urls(activity_urls, client:) | |
| require_relative "../github_data_fetcher" | |
| skipped_urls = [] | |
| results = activity_urls.each_with_object({}) do |url, data| | |
| puts "Fetching data for #{url}" | |
| begin | |
| result = GithubDataFetcher.new(url: url, client:).fetch_to_hash | |
| result[:source_url] = url | |
| data[url] = result | |
| rescue Octokit::NotFound => e | |
| puts "Warning: Skipping #{url} as it appears to be moved or deleted (404 error)" | |
| puts " Error: #{e.message}" | |
| skipped_urls << url | |
| rescue => e | |
| puts "Error fetching data for #{url}: #{e.message}" | |
| puts e.backtrace.join("\n") if $GLOBAL_DEBUG | |
| skipped_urls << url | |
| end | |
| end | |
| if skipped_urls.any? | |
| puts "Skipped #{skipped_urls.size} URLs due to errors:" | |
| skipped_urls.each { |url| puts " - #{url}" } | |
| end | |
| puts "Fetched data for #{results.size} URLs successfully!" | |
| results | |
| end | |
| private def filter_data(raw_data, date_range) | |
| start_date = Time.parse(date_range.start_date) | |
| filtered = raw_data.select do |url, record| | |
| begin | |
| authored_by_me = record["author"] == github_login | |
| merged_by_me = record["merged"] && record["merged_by"] == github_login | |
| commented_by_me = record["comments"]&.any? { |c| c["actor"] == github_login } | |
| recent_comments_by_me = record["comments"]&.count { |c| | |
| c["actor"] == github_login && | |
| Time.parse(c["created_at"]) >= start_date | |
| } || 0 | |
| reviewed_by_me = record["reviews"]&.any? { |r| r["actor"] == github_login } | |
| non_assign_event = record["events"]&.any? do |e| | |
| e["actor"] == github_login && e["event_type"] == "committed" | |
| end | |
| # Parse dates for age checking | |
| created_at = Time.parse(record["created_at"]) | |
| closed_at = record["closed_at"] ? Time.parse(record["closed_at"]) : nil | |
| six_months_ago = Time.now - (6 * 30 * 24 * 60 * 60) # approximate 6 months in seconds | |
| is_housekeeping = created_at < six_months_ago && | |
| closed_at && | |
| closed_at >= start_date && | |
| !merged_by_me && | |
| recent_comments_by_me <= 1 && | |
| !reviewed_by_me | |
| # Keep if not old housekeeping and has meaningful interaction | |
| !is_housekeeping && ((authored_by_me && merged_by_me) || commented_by_me || reviewed_by_me || non_assign_event) | |
| rescue => e | |
| puts "Error filtering record from #{url}: #{e.message}" | |
| false | |
| end | |
| end | |
| if filtered.keys.count == 0 | |
| puts "Exiting because there are no records to process after filtering" | |
| exit | |
| end | |
| puts "Filtered down to #{filtered.keys.count} records" | |
| filtered | |
| end | |
| private def generate_executive_summaries(filtered_data) | |
| puts "Generating executive summaries..." | |
| system_prompt = File.read(File.join(Dir.pwd, "prompts/executive_summary.md")) | |
| filtered_data.each_with_object({}) do |(url, record), results| | |
| puts "Generating executive summary for #{url}..." | |
| record = right_size_record(record) | |
| attempts = 0 | |
| begin | |
| prompt = JSON.pretty_generate(record) | |
| llm_result = llm_client.result(prompt, system_prompt:) | |
| results[url] = llm_result | |
| rescue => e | |
| attempts += 1 | |
| if attempts < 3 | |
| sleep(5 * attempts) | |
| retry | |
| else | |
| puts "Warning: Failed to generate executive summary for #{url}" | |
| results[url] = "" | |
| end | |
| end | |
| results[url] | |
| end | |
| end | |
| private def classify_data(filtered_data, retries: 3, delay: 5) | |
| filtered_data.each_with_object({}) do |(url, record), results| | |
| puts "Classifying record from #{url}..." | |
| record = right_size_record(record) | |
| attempts = 0 | |
| begin | |
| system_prompt = [ | |
| "You are an AI assistant that processes JSON data describing GitHub work items (issues, pull requests, and discussions). You will output metadata about the data in valid JSON.", | |
| "Here is context to help you understand the data and the task:", | |
| File.read(File.join(Dir.pwd, "prompts/github_mission_semester_priorities.md")), | |
| File.read(File.join(Dir.pwd, "prompts/who_are_safety_integrity.md")), | |
| File.read(File.join(Dir.pwd, "prompts/who_is_#{github_login}.md")), | |
| File.read(File.join(Dir.pwd, "prompts/snippets/sections.md")), | |
| File.read(File.join(Dir.pwd, "prompts/classify_data.md")), | |
| ].join("\n\n") | |
| prompt = JSON.pretty_generate(record) | |
| llm_result = llm_client.result(prompt, json: true, system_prompt:) | |
| results[url] = JSON.parse(llm_result) | |
| rescue => e | |
| attempts += 1 | |
| if attempts < retries | |
| sleep(delay * attempts) | |
| retry | |
| else | |
| puts "Warning: Failed to classify #{record["source_url"]}" | |
| puts "Error: #{e.message}" | |
| results[url] = {} | |
| end | |
| end | |
| results[url] | |
| end | |
| end | |
| private def right_size_record(record, max_size: 3000) | |
| # Step 1: Remove the "events" array | |
| record.delete("events") | |
| # Step 2: Filter comments and review_comments | |
| ["comments", "review_comments"].each do |key| | |
| next unless record[key].is_a?(Array) | |
| record[key].reject! do |comment| | |
| actor = comment["actor"] | |
| actor == "hubot" || actor.end_with?("[bot]") | |
| end | |
| end | |
| # Step 3: Serialize to JSON and check size | |
| serialized = record.to_json | |
| if (serialized.bytesize / 4.0).ceil > max_size | |
| # Step 4: Remove "diff" key if size is too large | |
| record.delete("diff") | |
| end | |
| record | |
| end | |
| private def build_groups_from_classified_data(classifications_data, retries: 3, delay: 5) | |
| puts "Grouping data..." | |
| attempts = 0 | |
| filtered_classification_data = classifications_data.transform_values do |details| | |
| { | |
| "summary" => details["summary"], | |
| "xrefs" => details["xrefs"] | |
| } | |
| end | |
| begin | |
| system_prompt = File.read(File.join(Dir.pwd, "prompts/group_data.md")) | |
| prompt = JSON.pretty_generate(filtered_classification_data) | |
| llm_result = llm_client.result(prompt, system_prompt:) | |
| llm_result = llm_result.gsub(/^```json/, "").gsub(/```$/, "") | |
| result = JSON.parse(llm_result) | |
| result = result.is_a?(Array) ? result : [] | |
| original_urls = filtered_classification_data.keys.to_set | |
| grouped_urls = result.flat_map { |group| group["urls"] }.to_set | |
| missing_urls = original_urls - grouped_urls | |
| unless missing_urls.empty? | |
| puts "Warning: Some URLs were not included in any group:" | |
| missing_urls.each { |url| puts " - #{url}" } | |
| end | |
| return result | |
| rescue => e | |
| attempts += 1 | |
| if attempts < retries | |
| sleep(delay * attempts) | |
| retry | |
| else | |
| puts "Warning: Failed to create groupings" | |
| puts "Error: #{e.message}" | |
| {} | |
| end | |
| end | |
| end | |
| private def generate_snippets(executive_summaries:, classifications_data:, grouped_data:) | |
| puts "Generating snippets..." | |
| snippets = SECTIONS.each_with_object({}) do |(section_name, section), result| | |
| result[section_name] = [] | |
| end | |
| grouped_data.each_with_index do |group, group_index| | |
| SECTIONS.each do |section_name, section| | |
| items = group["urls"].each_with_object({}) do |url, result| | |
| next unless executive_summaries.key?(url) | |
| executive_summary = executive_summaries[url] | |
| if executive_summary.nil? || executive_summary.empty? | |
| puts "Warning: No executive summary for #{url}" | |
| next | |
| end | |
| metadata = classifications_data[url] | |
| result[url] = { "executive_summary" => executive_summary, "metadata" => metadata } | |
| end | |
| # Generate a snippet for the current section and group | |
| snippet = generate_snippet(section_name, items, retries: 3, delay: 5) | |
| if snippet.nil? || snippet.empty? | |
| puts "Warning: No snippet generated for #{section_name} and group #{group["name"]}" | |
| next | |
| end | |
| # Add snippet number (Group #) at the beginning of each snippet | |
| snippet_with_number = "[G#{group_index + 1}] #{snippet.gsub(/\n+/, " ")}" | |
| snippets[section_name] << snippet_with_number | |
| end | |
| end | |
| snippets | |
| end | |
| private def generate_snippet(section, items, retries: 3, delay: 5) | |
| attempts = 0 | |
| begin | |
| system_prompt = [ | |
| "You are an AI assistant that processes JSON data describing my GitHub work items (issues, pull requests, and discussions) and the relationships between that data to generate snippets for leadership.", | |
| "I am #{github_login} and these are my snippets and you should focus on the WE and not the I. For example, use 'We shipped X' instead of 'I shipped X' or 'led by @#{github_login}'.", | |
| "Here is context to help you understand the data and the task:", | |
| File.read(File.join(Dir.pwd, "prompts/github_mission_semester_priorities.md")), | |
| File.read(File.join(Dir.pwd, "prompts/who_is_#{github_login}.md")), | |
| File.read(File.join(Dir.pwd, "prompts/snippets/#{SECTIONS[section][:template]}")), | |
| ] | |
| prompt = JSON.pretty_generate(items) | |
| llm_client.result(prompt, system_prompt: system_prompt.join("\n\n")) | |
| rescue | |
| attempts += 1 | |
| if attempts < retries | |
| sleep(delay * attempts) | |
| retry | |
| else | |
| puts "Warning: Failed to generate snippet" | |
| nil | |
| end | |
| end | |
| end | |
| private def sort_and_limit_snippets(snippets_data:, retries: 3, delay: 5) | |
| puts "Sorting and limiting snippets..." | |
| attempts = 0 | |
| begin | |
| system_prompt = File.read(File.join(Dir.pwd, "prompts/snippets/sort_and_limit.md")) | |
| prompt = JSON.pretty_generate(snippets_data) | |
| llm_result = llm_client.result(prompt, system_prompt:) | |
| result = JSON.parse(llm_result) | |
| result | |
| rescue => e | |
| attempts += 1 | |
| if attempts < retries | |
| sleep(delay * attempts) | |
| retry | |
| else | |
| puts "Warning: Failed to sort and limit snippets" | |
| puts "Error: #{e.message}" | |
| {} | |
| end | |
| end | |
| end | |
| private def print_snippets(snippets_data) | |
| puts "Generated Snippets for #{github_login}" | |
| output = [] | |
| SECTIONS.each do |section_name, section| | |
| next unless snippets_data[section_name].any? | |
| output << "### #{section[:header]}" | |
| snippets_data[section_name].each do |snippet| | |
| output << "- #{snippet}" | |
| end | |
| output << "" | |
| end | |
| output.join("\n") | |
| end | |
| private def load_or_compute(path:, compute:, json: true) | |
| if File.exist?(path) | |
| puts "Found existing file at #{path}, skipping computation..." | |
| begin | |
| data = File.read(path) | |
| result = json ? JSON.parse(data) : data | |
| puts "Successfully loaded data from #{path}" | |
| return result | |
| rescue JSON::ParserError => e | |
| puts "Error parsing JSON from #{path}: #{e.message}" | |
| puts "Falling back to computation..." | |
| rescue => e | |
| puts "Error loading from #{path}: #{e.message}" | |
| puts "Falling back to computation..." | |
| end | |
| end | |
| begin | |
| result = compute.call | |
| if json | |
| result = JSON.pretty_generate(result) | |
| end | |
| File.open(path, "w") do |f| | |
| f.write(result) | |
| end | |
| puts "Generated file at #{path} successfully!" | |
| json ? JSON.parse(result) : result | |
| rescue => e | |
| puts "Error during computation or file writing: #{e.message}" | |
| puts e.backtrace.join("\n") | |
| raise | |
| end | |
| end | |
| end | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment