blob: ff6b7d67fdd37494d35d6c15220e9521687ec32c [file] [log] [blame]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package github
import (
"context"
"fmt"
"iter"
"golang.org/x/oscar/internal/llmapp"
"golang.org/x/oscar/internal/storage"
"golang.org/x/oscar/internal/storage/timed"
"rsc.io/ordered"
)
// IssueOverviewResult is the result of [IssueOverview] or [UpdateOverview].
// It contains the generated overview and metadata about the issue.
type IssueOverviewResult struct {
*Issue // the issue itself
NewComments int // number of new comments for this issue (for [CommentsAfterOverview])
TotalComments int // number of comments for this issue
Overview *llmapp.OverviewResult // the LLM-generated issue and comment summary
}
// IssueOverview returns an LLM-generated overview of the issue and its comments.
// It does not make any requests to GitHub; the issue and comment data must already
// be stored in db.
func IssueOverview(ctx context.Context, lc *llmapp.Client, db storage.DB, project string, issue int64) (*IssueOverviewResult, error) {
iss, err := LookupIssue(db, project, issue)
if err != nil {
return nil, err
}
post := iss.toLLMDoc()
var cs []*llmapp.Doc
for c := range comments(db, project, issue) {
cs = append(cs, c.toLLMDoc())
}
overview, err := lc.PostOverview(ctx, post, cs)
if err != nil {
return nil, err
}
return &IssueOverviewResult{
Issue: iss,
TotalComments: len(cs),
Overview: overview,
}, nil
}
// UpdateOverview 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).
// UpdateOverview does not make any requests to GitHub; the issue and comment data must already
// be stored in db.
func UpdateOverview(ctx context.Context, lc *llmapp.Client, db storage.DB,
project string, issue int64, lastRead int64) (*IssueOverviewResult, error) {
iss, err := LookupIssue(db, project, issue)
if err != nil {
return nil, err
}
post := iss.toLLMDoc()
var oldComments, newComments []*llmapp.Doc
foundTarget := false
for c := range comments(db, project, issue) {
// New comment.
if c.CommentID() > lastRead {
newComments = append(newComments, c.toLLMDoc())
continue
}
if c.CommentID() == lastRead {
foundTarget = true
}
oldComments = append(oldComments, c.toLLMDoc())
}
if !foundTarget {
return nil, fmt.Errorf("issue %d comment %d not found in database", issue, lastRead)
}
overview, err := lc.UpdatedPostOverview(ctx, post, oldComments, newComments)
if err != nil {
return nil, err
}
return &IssueOverviewResult{
Issue: iss,
NewComments: len(newComments),
TotalComments: len(oldComments) + len(newComments),
Overview: overview,
}, nil
}
// toLLMDoc converts an Issue to a format that can be used as
// an input to an LLM.
func (i *Issue) toLLMDoc() *llmapp.Doc {
return &llmapp.Doc{
Type: "issue",
URL: i.HTMLURL,
Author: i.User.Login,
Title: i.Title,
Text: i.Body,
}
}
// toLLMDoc converts an IssueComment to a format that can be used as
// an input to an LLM.
func (ic *IssueComment) toLLMDoc() *llmapp.Doc {
return &llmapp.Doc{
Type: "issue comment",
URL: ic.HTMLURL,
Author: ic.User.Login,
// no title
Text: ic.Body,
}
}
// comments returns an iterator over the comments for the issue in the db.
func comments(db storage.DB, project string, issue int64) iter.Seq[*IssueComment] {
return func(yield func(*IssueComment) bool) {
for e := range eventsByAPI(db, project, issue, "/issues/comments") {
if !yield(e.Typed.(*IssueComment)) {
return
}
}
}
}
// eventsByAPI returns an iterator over the events for the issue in the db
// with the given API.
func eventsByAPI(db storage.DB, project string, issue int64, api string) iter.Seq[*Event] {
return func(yield func(*Event) bool) {
start := o(project, issue, api)
end := o(project, issue, api, ordered.Inf)
for t := range timed.Scan(db, eventKind, start, end) {
if !yield(decodeEvent(db, t)) {
return
}
}
}
}