Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions crates/rust-project-goals-cli-llm/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,18 @@ async fn main() -> anyhow::Result<()> {
} = Opt::parse();
let UpdateArgs {
milestone,
quick,
vscode,
output_file,
start_date,
end_date,
model_id,
region,
} = &serde_json::from_str(&updates_json)?;
updates::updates(
&repository,
milestone,
output_file.as_deref(),
start_date,
end_date,
*quick,
*vscode,
model_id.as_deref(),
region.as_deref(),
)
.await?;
Ok(())
Expand Down
9 changes: 6 additions & 3 deletions crates/rust-project-goals-cli-llm/src/templates.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::path::{Path, PathBuf};

use handlebars::{DirectorySourceOptions, Handlebars};
use rust_project_goals::gh::issues::ExistingGithubComment;
use serde::Serialize;

use rust_project_goals_json::Progress;
Expand Down Expand Up @@ -44,8 +45,7 @@ handlebars::handlebars_helper!(is_complete: |p: Progress| match p {
pub struct Updates {
pub milestone: String,
pub flagship_goals: Vec<UpdatesGoal>,
pub other_goals_with_updates: Vec<UpdatesGoal>,
pub other_goals_without_updates: Vec<UpdatesGoal>,
pub other_goals: Vec<UpdatesGoal>,
}

impl Updates {
Expand Down Expand Up @@ -74,7 +74,10 @@ pub struct UpdatesGoal {
pub is_closed: bool,

/// Markdown with update text (bullet list)
pub updates_markdown: String,
pub comments: Vec<ExistingGithubComment>,

/// Comments.len but accessible to the template
pub num_comments: usize,

/// Progress towards the goal
pub progress: Progress,
Expand Down
162 changes: 12 additions & 150 deletions crates/rust-project-goals-cli-llm/src/updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,25 @@ use std::io::Write;
use std::path::Path;
use std::process::{Command, Stdio};

use crate::llm::LargeLanguageModel;
use crate::templates::{self, Updates, UpdatesGoal};
use rust_project_goals::gh::issues::ExistingGithubIssue;
use rust_project_goals::gh::{
issue_id::{IssueId, Repository},
issues::{checkboxes, list_issues_in_milestone, ExistingGithubComment},
};

const QUICK_UPDATES: &[&str] = &[
"Jack and Jill went up the hill",
"To fetch a pail of water",
"Jack fell down and broke his crown",
"And Jill came tumbling after.",
"Up Jack got and home did trot,",
"As fast as he could caper;",
"Went to bed to mend his head",
"With vinegar and brown paper.",
"Jill came in and she did grin",
"To see his paper plaster;",
"Mother, vex’d, did whip her next",
"For causing Jack's disaster.",
];

fn comments_forever() -> impl Iterator<Item = &'static str> {
QUICK_UPDATES.iter().copied().cycle()
}

pub async fn updates(
repository: &Repository,
milestone: &str,
output_file: Option<&Path>,
start_date: &Option<NaiveDate>,
end_date: &Option<NaiveDate>,
quick: bool,
vscode: bool,
model_id: Option<&str>,
region: Option<&str>,
) -> anyhow::Result<()> {
if output_file.is_none() && !vscode {
anyhow::bail!("either `--output-file` or `--vscode` must be specified");
}

let llm = LargeLanguageModel::new(model_id, region).await?;

let issues = list_issues_in_milestone(repository, milestone)?;

let filter = Filter {
Expand All @@ -69,14 +44,10 @@ pub async fn updates(

let mut updates = templates::Updates {
milestone: milestone.to_string(),
flagship_goals: vec![],
other_goals_with_updates: vec![],
other_goals_without_updates: vec![],
flagship_goals: prepare_goals(repository, &issues, &filter, true).await?,
other_goals: prepare_goals(repository, &issues, &filter, false).await?,
};

prepare_flagship_goals(repository, &issues, &filter, &llm, quick, &mut updates).await?;
prepare_other_goals(repository, &issues, &filter, &llm, quick, &mut updates).await?;

progress_bar::finalize_progress_bar();

// Render the output using handlebars and write it to the file.
Expand Down Expand Up @@ -108,17 +79,16 @@ pub async fn updates(
Ok(())
}

async fn prepare_flagship_goals(
async fn prepare_goals(
repository: &Repository,
issues: &[ExistingGithubIssue],
filter: &Filter<'_>,
llm: &LargeLanguageModel,
quick: bool,
updates: &mut Updates,
) -> anyhow::Result<()> {
flagship: bool,
) -> anyhow::Result<Vec<UpdatesGoal>> {
let mut result = vec![];
// First process the flagship goals, for which we capture the full text of comments.
for issue in issues {
if !issue.has_flagship_label() {
if flagship != issue.has_flagship_label() {
continue;
}

Expand All @@ -135,34 +105,9 @@ async fn prepare_flagship_goals(

let mut comments = issue.comments.clone();
comments.sort_by_key(|c| c.created_at.clone());
comments.retain(|c| filter.matches(c));
comments.retain(|c| !c.is_automated_comment() && filter.matches(c));

let summary: String = if comments.len() == 0 {
format!("No updates in this period.")
} else if quick {
QUICK_UPDATES.iter().copied().collect()
} else {
let prompt = format!(
"The following comments are updates to a project goal entitled '{title}'. \
The goal is assigned to {people} ({assignees}). \
Summarize the major developments, writing for general Rust users. \
Write the update in the third person and do not use pronouns when referring to people. \
Do not respond with anything but the summary paragraphs. \
",
people = if issue.assignees.len() == 1 {
"1 person".to_string()
} else {
format!("{} people", issue.assignees.len())
},
assignees = comma(&issue.assignees),
);
let updates: String = comments.iter().map(|c| format!("\n{}\n", c.body)).collect();
llm.query(&prompt, &updates)
.await
.with_context(|| format!("making request to LLM failed"))?
};

updates.flagship_goals.push(UpdatesGoal {
result.push(UpdatesGoal {
title: title.clone(),
issue_number: issue.number,
issue_assignees: comma(&issue.assignees),
Expand All @@ -173,96 +118,13 @@ async fn prepare_flagship_goals(
.url(),
progress,
is_closed: issue.state == GithubIssueState::Closed,
updates_markdown: summary,
num_comments: comments.len(),
comments,
});

progress_bar::inc_progress_bar();
}
Ok(())
}

async fn prepare_other_goals(
repository: &Repository,
issues: &[ExistingGithubIssue],
filter: &Filter<'_>,
llm: &LargeLanguageModel,
quick: bool,
updates: &mut Updates,
) -> anyhow::Result<()> {
// Next process the remaining goals, for which we generate a summary using an LLVM.
let mut quick_comments = comments_forever();
for issue in issues {
if issue.has_flagship_label() {
continue;
}

let title = &issue.title;

progress_bar::print_progress_bar_info(
&format!("Issue #{number}", number = issue.number),
title,
progress_bar::Color::Green,
progress_bar::Style::Bold,
);

// Find the relevant updates that have occurred.
let mut comments = issue.comments.clone();
comments.sort_by_key(|c| c.created_at.clone());
comments.retain(|c| filter.matches(c));

// Use an LLM to summarize the updates.
let summary = if comments.len() == 0 {
format!("* No updates in this period.")
} else if quick {
let num_comments = std::cmp::min(comments.len(), 3);
quick_comments
.by_ref()
.take(num_comments)
.map(|c| format!("* {c}\n"))
.collect()
} else {
let prompt = format!(
"The following comments are updates to a project goal entitled '{title}'. \
The goal is assigned to {people} ({assignees}). \
Summarize the updates with a list of one or two bullet points, each one sentence. \
Write the update in the third person and do not use pronouns when referring to people. \
Format the bullet points as markdown with each bullet point beginning with `* `. \
Do not respond with anything but the bullet points. \
",
people = if issue.assignees.len() == 1 {
"1 person".to_string()
} else {
format!("{} people", issue.assignees.len())
},
assignees = comma(&issue.assignees),
);
let updates: String = comments.iter().map(|c| format!("\n{}\n", c.body)).collect();
llm.query(&prompt, &updates).await?
};

let goal = UpdatesGoal {
title: title.clone(),
issue_number: issue.number,
issue_assignees: comma(&issue.assignees),
issue_url: IssueId {
repository: repository.clone(),
number: issue.number,
}
.url(),
is_closed: issue.state == GithubIssueState::Closed,
updates_markdown: summary,
progress: checkboxes(&issue),
};

if comments.len() > 0 {
updates.other_goals_with_updates.push(goal);
} else {
updates.other_goals_without_updates.push(goal);
}

progress_bar::inc_progress_bar();
}
Ok(())
Ok(result)
}

struct Filter<'f> {
Expand Down
23 changes: 19 additions & 4 deletions crates/rust-project-goals-cli/src/rfc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use rust_project_goals::{
gh::{
issue_id::{IssueId, Repository},
issues::{
change_milestone, create_comment, create_issue, fetch_issue, list_issues_in_milestone, lock_issue, sync_assignees, update_issue_body, FLAGSHIP_LABEL, LOCK_TEXT
change_milestone, change_title, create_comment, create_issue, fetch_issue, list_issues_in_milestone, lock_issue, sync_assignees, update_issue_body, CONTINUING_GOAL_PREFIX, FLAGSHIP_LABEL, LOCK_TEXT
},
labels::GhLabel,
},
Expand Down Expand Up @@ -199,6 +199,11 @@ enum GithubAction<'doc> {
issue: GithubIssue<'doc>,
},

ChangeTitle {
number: u64,
title: String,
},

ChangeMilestone {
number: u64,
milestone: String,
Expand Down Expand Up @@ -333,6 +338,10 @@ fn initialize_issues<'doc>(
.collect(),
});
}

if existing_issue.title != desired_issue.title {
actions.insert(GithubAction::ChangeTitle { number: existing_issue.number, title: desired_issue.title });
}

if existing_issue.milestone.as_ref().map(|m| m.title.as_str()) != Some(timeframe) {
actions.insert(GithubAction::ChangeMilestone {
Expand All @@ -342,9 +351,7 @@ fn initialize_issues<'doc>(
actions.insert(GithubAction::Comment {
number: existing_issue.number,
body: format!(
"This is a continuing project goal, and the updates below \
this comment will be for the new period {}",
timeframe
"{CONTINUING_GOAL_PREFIX} {timeframe}",
),
});
}
Expand Down Expand Up @@ -525,6 +532,9 @@ impl Display for GithubAction<'_> {
GithubAction::ChangeMilestone { number, milestone } => {
write!(f, "update issue #{} milestone to \"{}\"", number, milestone)
}
GithubAction::ChangeTitle { number, title } => {
write!(f, "update issue #{} title to \"{}\"", number, title)
}
GithubAction::Comment { number, body } => {
write!(f, "post comment on issue #{}: \"{}\"", number, body)
}
Expand Down Expand Up @@ -596,6 +606,11 @@ impl GithubAction<'_> {
Ok(())
}

GithubAction::ChangeTitle { number, title } => {
change_title(repository, number, &title)?;
Ok(())
}

GithubAction::Comment { number, body } => {
create_comment(repository, number, &body)?;
Ok(())
Expand Down
12 changes: 0 additions & 12 deletions crates/rust-project-goals-llm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ pub struct UpdateArgs {
/// Milestone for which we generate tracking issue data (e.g., `2024h2`).
pub milestone: String,

/// Quick mode does not use an LLM to generate a summary.
#[arg(long)]
pub quick: bool,

/// Quick mode does not use an LLM to generate a summary.
#[arg(long)]
pub vscode: bool,
Expand All @@ -30,12 +26,4 @@ pub struct UpdateArgs {
/// End date for comments.
/// If not given, no end date.
pub end_date: Option<chrono::NaiveDate>,

/// Set a custom model id for the LLM.
#[arg(long)]
pub model_id: Option<String>,

/// Set a custom region.
#[arg(long)]
pub region: Option<String>,
}
Loading