Skip to content

Commit

Permalink
internal/overview: skip comments by gabyhelp and stub out poster
Browse files Browse the repository at this point in the history
When generating overviews, skip comments made by gabyhelp.
Also, append an @ to GitHub usernames when used in overviews,
so that GitHub will auto-link to the user on display.

Also add stubs for the [Client.Run] function and the [poster] type
which will post overviews to GitHub.

Change-Id: I76142abd6970ccee10c4d4656b9b3d5f779d2991
Reviewed-on: https://go-review.googlesource.com/c/oscar/+/636857
Reviewed-by: Hyang-Ah Hana Kim <[email protected]>
LUCI-TryBot-Result: Go LUCI <[email protected]>
  • Loading branch information
tatianab committed Dec 20, 2024
1 parent 016c8c3 commit 94e82d5
Show file tree
Hide file tree
Showing 7 changed files with 294 additions and 50 deletions.
2 changes: 1 addition & 1 deletion internal/gaby/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ func main() {
g.embed = ai
g.llm = ai
g.llmapp = llmapp.NewWithChecker(g.slog, ai, g.policy, g.db)
g.overview = overview.New(g.llmapp, g.github)
g.overview = overview.New(g.slog, g.db, g.github, g.llmapp, "overview", "gabyhelp")

cr := crawl.New(g.slog, g.db, g.http)
cr.Add("https://go.dev/")
Expand Down
5 changes: 4 additions & 1 deletion internal/gaby/overview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func TestPopulateOverviewPage(t *testing.T) {
vector: storage.MemVectorDB(db, lg, "vector"),
github: github.New(lg, db, secret.Empty(), nil),
llmapp: lc,
overview: overview.New(lc, gh),
overview: overview.New(lg, db, gh, lc, "test", "test-bot"),
docs: docs.New(lg, db),
embed: llm.QuoteEmbedder(),
}
Expand Down Expand Up @@ -118,6 +118,7 @@ func TestPopulateOverviewPage(t *testing.T) {
Raw: wantIssueResult.Overview,
Typed: &overview.IssueResult{
TotalComments: 2,
LastComment: comment2.CommentID(),
Overview: wantIssueResult.Overview,
},
Issue: iss1,
Expand All @@ -143,6 +144,7 @@ func TestPopulateOverviewPage(t *testing.T) {
Raw: wantIssueResult.Overview,
Typed: &overview.IssueResult{
TotalComments: 2,
LastComment: comment2.CommentID(),
Overview: wantIssueResult.Overview,
},
Issue: iss1,
Expand Down Expand Up @@ -195,6 +197,7 @@ func TestPopulateOverviewPage(t *testing.T) {
Typed: &overview.IssueUpdateResult{
NewComments: 1,
TotalComments: 2,
LastComment: comment2.CommentID(),
Overview: wantUpdateResult.Overview,
},
Issue: iss1,
Expand Down
12 changes: 10 additions & 2 deletions internal/github/llm.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func (i *Issue) ToLLMDoc() *llmapp.Doc {
return &llmapp.Doc{
Type: "issue",
URL: i.HTMLURL,
Author: i.User.Login,
Author: i.User.ForDisplay(),
Title: i.Title,
Text: i.Body,
}
Expand All @@ -26,8 +26,16 @@ func (ic *IssueComment) ToLLMDoc() *llmapp.Doc {
return &llmapp.Doc{
Type: "issue comment",
URL: ic.HTMLURL,
Author: ic.User.Login,
Author: ic.User.ForDisplay(),
// no title
Text: ic.Body,
}
}

// ForDisplay returns the user's login username, prefixed with @.
func (u *User) ForDisplay() string {
if u.Login == "" {
return ""
}
return "@" + u.Login
}
141 changes: 133 additions & 8 deletions internal/overview/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,148 @@

// Package overview generates and posts overviews of discussions.
// For now, it only works with GitHub issues and their comments.
// TODO(tatianabradley): Implement posting logic.
// TODO(tatianabradley): Add comment explaining design.
package overview

import (
"context"
"encoding/json"
"log/slog"
"time"

"golang.org/x/oscar/internal/github"
"golang.org/x/oscar/internal/llmapp"
"golang.org/x/oscar/internal/storage"
"rsc.io/ordered"
)

// A Client is used to generate, post, and update AI-generated overviews
// of GitHub issues and their comments.
type Client struct {
lc *llmapp.Client
gh *github.Client
slog *slog.Logger
db storage.DB // the database to use to store state
minTimeBetweenUpdates time.Duration // the minimum time between calls to [poster.run]

g *generator // for generating overviews
p *poster // for modifying GitHub
}

// New returns a new Client used to generate and post overviews to GitHub.
// Name is a string used to identify the Client, and bot is the login of the
// GitHub user that will modify GitHub.
// Clients with the same name and bot use the same state.
func New(lg *slog.Logger, db storage.DB, gh *github.Client, lc *llmapp.Client, name, bot string) *Client {
c := &Client{
slog: lg,
db: db,
minTimeBetweenUpdates: defaultMinTimeBetweenUpdates,
g: newGenerator(gh, lc),
p: newPoster(name, bot),
}
c.g.skipCommentsBy(bot)
return c
}

var defaultMinTimeBetweenUpdates = 24 * time.Hour

// Run posts and updates AI-generated overviews of GitHub issues.
//
// TODO(tatianabradley): Detailed comment.
func (c *Client) Run(ctx context.Context) error {
c.slog.Info("overview.Run start")
defer func() {
c.slog.Info("overview.Run end")
}()

// Check if we should run or not.
c.db.Lock(string(c.runKey()))
defer c.db.Unlock(string(c.runKey()))

lr, err := c.lastRun()
if err != nil {
return err
}
if time.Since(lr) < c.minTimeBetweenUpdates {
c.slog.Info("overview.Run: skipped (last successful run happened too recently)", "last run", lr, "min time", c.minTimeBetweenUpdates)
return nil
}
if err := c.p.run(); err != nil {
return err
}

c.setLastRun(time.Now())
return nil
}

// ForIssue returns an LLM-generated overview of the issue and its comments.
// It does not make any requests to, or modify, GitHub; the issue and comment data must already
// be stored in the database.
func (c *Client) ForIssue(ctx context.Context, iss *github.Issue) (*IssueResult, error) {
return c.g.issue(ctx, iss)
}

// ForIssueUpdate returns an LLM-generated overview of the issue and its
// comments, separating the comments into "old" and "new" groups broken
// by the specifed lastRead comment id. (The lastRead comment itself is
// considered "old", and must be present in the database).
//
// ForIssueUpdate does not make any requests to, or modify, GitHub; the issue and comment data must already
// be stored in db.
func (c *Client) ForIssueUpdate(ctx context.Context, iss *github.Issue, lastRead int64) (*IssueUpdateResult, error) {
return c.g.issueUpdate(ctx, iss, lastRead)
}

// EnableProject enables the Client to post on and update issues in the given
// GitHub project (for example "golang/go").
func (c *Client) EnableProject(project string) {
c.p.projects[project] = true
}

// RequireApproval configures the poster to require approval for all actions.
func (c *Client) RequireApproval() {
c.p.requireApproval = true
}

// AutoApprove configures the Client to auto-approve all its actions.
func (c *Client) AutoApprove() {
c.p.requireApproval = false
}

// SetMinComments sets the minimum number of comments a post needs to get an
// overview comment.
func (c *Client) SetMinComments(n int) {
c.p.minComments = n
}

// New returns a new Client used to generate and post overviews.
func New(lc *llmapp.Client, gh *github.Client) *Client {
return &Client{
lc: lc,
gh: gh,
type state struct {
LastRun string // the time of the last sucessful (non-skipped) call to [Client.Run]
}

// lastRun returns the time of the last successful (non-skipped)
// call to [Client.Run].
func (c *Client) lastRun() (time.Time, error) {
b, ok := c.db.Get(c.runKey())
if !ok {
return time.Time{}, nil
}
var s state
if err := json.Unmarshal(b, &s); err != nil {
return time.Time{}, err
}
return time.Parse(time.RFC3339, s.LastRun)
}

// setLastRun sets the time of the last successful (non-skipped)
// call to [Client.Run].
func (c *Client) setLastRun(t time.Time) {
c.db.Set(c.runKey(), storage.JSON(&state{
LastRun: t.Format(time.RFC3339),
}))
}

// runKey returns the key to use to store the state for this Client.
func (c *Client) runKey() []byte {
return ordered.Encode(runKind, c.p.name, c.p.bot)
}

const runKind = "overview.Run"
Loading

0 comments on commit 94e82d5

Please sign in to comment.