internal/llmapp: refactor Overview to avoid unexported parameter
Change-Id: Id12e674c0ab81426f42e8ce48aec9c08b78746da
Reviewed-on: https://go-review.googlesource.com/c/oscar/+/622856
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/gaby/overview.go b/internal/gaby/overview.go
index 7f6d582..e2ae1ec 100644
--- a/internal/gaby/overview.go
+++ b/internal/gaby/overview.go
@@ -67,7 +67,7 @@
overviewForm: form,
Result: &overviewResult{
IssueOverviewResult: *overview,
- OverviewHTML: htmlutil.MarkdownToSafeHTML(overview.Overview),
+ OverviewHTML: htmlutil.MarkdownToSafeHTML(overview.Overview.Overview),
},
}
}
diff --git a/internal/gaby/templates_test.go b/internal/gaby/templates_test.go
index 12e2dd0..a396dcc 100644
--- a/internal/gaby/templates_test.go
+++ b/internal/gaby/templates_test.go
@@ -19,6 +19,7 @@
"golang.org/x/net/html/atom"
"golang.org/x/oscar/internal/actions"
"golang.org/x/oscar/internal/github"
+ "golang.org/x/oscar/internal/llmapp"
"golang.org/x/oscar/internal/search"
)
@@ -40,7 +41,10 @@
IssueOverviewResult: github.IssueOverviewResult{
URL: "https://example.com",
NumComments: 2,
- Overview: "an overview",
+ Overview: &llmapp.OverviewResult{
+ Overview: "an overview",
+ Prompt: []string{"a prompt"},
+ },
},
OverviewHTML: safehtml.HTMLEscaped("an overview"),
}}},
diff --git a/internal/gaby/tmpl/overviewpage.tmpl b/internal/gaby/tmpl/overviewpage.tmpl
index 9016298..800f46d 100644
--- a/internal/gaby/tmpl/overviewpage.tmpl
+++ b/internal/gaby/tmpl/overviewpage.tmpl
@@ -66,7 +66,7 @@
<div class="toggle" onclick="togglePrompt()">[show prompt]</div>
<div id="prompt">
<ul>
- {{- range .Prompt -}}
+ {{- range .Overview.Prompt -}}
<li>
<pre>{{.}}</pre>
</li>
diff --git a/internal/github/overview.go b/internal/github/overview.go
index b1e8bcd..a73a4ee 100644
--- a/internal/github/overview.go
+++ b/internal/github/overview.go
@@ -16,57 +16,45 @@
// It contains the generated overview and metadata about
// the issue.
type IssueOverviewResult struct {
- URL string // the issue's URL
- NumComments int // number of comments for this issue
- Overview string // the LLM-generated issue and comment summary
- Prompt []string // the prompt(s) used to generate the result
+ URL string // the issue's URL
+ NumComments 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, g llm.TextGenerator, db storage.DB, project string, issue int64) (*IssueOverviewResult, error) {
- var docs []*llmapp.Doc
- var m issueMetadata
+ var post *llmapp.Doc
+ var comments []*llmapp.Doc
for e := range events(db, project, issue, issue) {
- m.update(e)
- doc := e.toLLMDoc()
+ doc, isIssue := e.toLLMDoc()
if doc == nil {
continue
}
- docs = append(docs, doc)
+ if isIssue {
+ post = doc
+ continue
+ }
+ comments = append(comments, doc)
}
- overview, err := llmapp.Overview(ctx, g, llmapp.PostAndComments, docs...)
+ overview, err := llmapp.PostOverview(ctx, g, post, comments)
if err != nil {
return nil, err
}
return &IssueOverviewResult{
- URL: m.url,
- NumComments: m.numComments,
+ URL: post.URL,
+ NumComments: len(comments),
Overview: overview,
- Prompt: llmapp.OverviewPrompt(llmapp.PostAndComments, docs),
}, nil
}
-type issueMetadata struct {
- url string
- numComments int
-}
-
-// update updates the issueMetadata given the event.
-func (m *issueMetadata) update(e *Event) {
- switch v := e.Typed.(type) {
- case *Issue:
- m.url = v.HTMLURL
- case *IssueComment:
- m.numComments++
- }
-}
-
// toLLMDoc converts an Event to a format that can be used as
// an input to an LLM.
-// It returns nil if the Event cannot be converted to a document.
-func (e *Event) toLLMDoc() *llmapp.Doc {
+// isIssue is true if the event represents a GitHub issue (as opposed to
+// a comment or another event).
+// It returns (nil, false) if the Event cannot be converted to a document.
+func (e *Event) toLLMDoc() (_ *llmapp.Doc, isIssue bool) {
switch v := e.Typed.(type) {
case *Issue:
return &llmapp.Doc{
@@ -75,7 +63,7 @@
Author: v.User.Login,
Title: v.Title,
Text: v.Body,
- }
+ }, true
case *IssueComment:
return &llmapp.Doc{
Type: "issue comment",
@@ -83,7 +71,7 @@
Author: v.User.Login,
// no title
Text: v.Body,
- }
+ }, false
}
- return nil
+ return nil, false
}
diff --git a/internal/github/overview_test.go b/internal/github/overview_test.go
index 137a590..6dc8cd7 100644
--- a/internal/github/overview_test.go
+++ b/internal/github/overview_test.go
@@ -33,13 +33,17 @@
check(c.Add("robpike/ivy"))
check(c.Sync(ctx))
- got, err := IssueOverview(ctx, llm.EchoTextGenerator(), db, "robpike/ivy", 19)
+ echo := llm.EchoTextGenerator()
+
+ got, err := IssueOverview(ctx, echo, db, "robpike/ivy", 19)
if err != nil {
t.Fatal(err)
}
- prompt := llmapp.OverviewPrompt(llmapp.PostAndComments, []*llmapp.Doc{
- {
+ // This merely checks that the correct call to [llmapp.PostOverview] is made.
+ // The internals of [llmapp.PostOverview] are tested in the llmapp package.
+ wantOverview, err := llmapp.PostOverview(ctx, echo,
+ &llmapp.Doc{
Type: "issue",
URL: "https://github.com/robpike/ivy/issues/19",
Author: "xunshicheng",
@@ -50,25 +54,27 @@
could you get me a hand!
`,
},
- {
- Type: "issue comment",
- URL: "https://github.com/robpike/ivy/issues/19#issuecomment-169157303",
- Author: "robpike",
- Text: `See the import comment, or listen to the error message. Ivy uses a custom import.
+ []*llmapp.Doc{
+ {
+ Type: "issue comment",
+ URL: "https://github.com/robpike/ivy/issues/19#issuecomment-169157303",
+ Author: "robpike",
+ Text: `See the import comment, or listen to the error message. Ivy uses a custom import.
go get robpike.io/ivy
It is a fair point though that this should be explained in the README. I will fix that.
`,
- },
- })
- overview := llm.EchoResponse(prompt...)
+ },
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
want := &IssueOverviewResult{
URL: "https://github.com/robpike/ivy/issues/19",
- Overview: overview,
+ Overview: wantOverview,
NumComments: 1,
- Prompt: prompt,
}
if diff := cmp.Diff(got, want); diff != "" {
diff --git a/internal/llmapp/overview.go b/internal/llmapp/overview.go
index f4bc6e0..57e9075 100644
--- a/internal/llmapp/overview.go
+++ b/internal/llmapp/overview.go
@@ -33,24 +33,55 @@
Text string `json:"text"` // required
}
+// OverviewResult is the result of [Overview] or [PostOverview].
+type OverviewResult struct {
+ Overview string // the LLM-generated summary
+ Prompt []string // the prompt(s) used to generate the result
+}
+
// Overview returns an LLM-generated overview of the given documents,
// styled with markdown.
+// Overview returns an error if no documents are provided or the LLM is unable
+// to generate a response.
+func Overview(ctx context.Context, g llm.TextGenerator, docs ...*Doc) (*OverviewResult, error) {
+ return overview(ctx, g, documents, docs)
+}
+
+// PostOverview returns an LLM-generated overview of the given post and comments,
+// styled with markdown.
+// PostOverview returns an error if no post is provided or the LLM is unable to generate a response.
+func PostOverview(ctx context.Context, g llm.TextGenerator, post *Doc, comments []*Doc) (*OverviewResult, error) {
+ if post == nil {
+ return nil, errors.New("llmapp PostOverview: no post")
+ }
+ return overview(ctx, g, postAndComments, append([]*Doc{post}, comments...))
+}
+
+// overview returns an LLM-generated overview of the given documents,
+// styled with markdown.
// The kind argument is a descriptor for the given documents, used to add
// additional context to the LLM prompt.
// Overview returns an error if no documents are provided or the LLM is unable
// to generate a response.
-// TODO(tatianabradley): Chunk large prompts.
-func Overview(ctx context.Context, g llm.TextGenerator, kind docsKind, docs ...*Doc) (string, error) {
+func overview(ctx context.Context, g llm.TextGenerator, kind docsKind, docs []*Doc) (*OverviewResult, error) {
if len(docs) == 0 {
- return "", errors.New("llmapp.Overview: no documents")
+ return nil, errors.New("llmapp overview: no documents")
}
- return g.GenerateText(ctx, OverviewPrompt(kind, docs)...)
+ prompt := overviewPrompt(kind, docs)
+ overview, err := g.GenerateText(ctx, prompt...)
+ if err != nil {
+ return nil, err
+ }
+ return &OverviewResult{
+ Overview: overview,
+ Prompt: prompt,
+ }, nil
}
-// OverviewPrompt converts the given docs into a slice of
+// overviewPrompt converts the given docs into a slice of
// text prompts, followed by an instruction prompt based
// on the documents kind.
-func OverviewPrompt(kind docsKind, docs []*Doc) []string {
+func overviewPrompt(kind docsKind, docs []*Doc) []string {
var inputs = make([]string, len(docs))
for i, d := range docs {
inputs[i] = string(storage.JSON(d))
@@ -59,15 +90,14 @@
}
// docsKind is a descriptor for a group of documents.
-// unexported so that clients must use a predefined kind.
-type docsKind struct{ name string }
+type docsKind string
var (
// Represents a group of documents of an unspecified kind.
- Documents docsKind = docsKind{"documents"}
+ documents docsKind = "documents"
// The documents represent a post and comments/replies
// on that post. For example, a GitHub issue and its comments.
- PostAndComments docsKind = docsKind{"post_and_comments"}
+ postAndComments docsKind = "post_and_comments"
)
//go:embed prompts/*.tmpl
@@ -78,7 +108,7 @@
// document kind.
func (k docsKind) instructions() string {
w := &strings.Builder{}
- err := tmpls.ExecuteTemplate(w, k.name, nil)
+ err := tmpls.ExecuteTemplate(w, string(k), nil)
if err != nil {
// unreachable except bug in this package
panic(err)
diff --git a/internal/llmapp/overview_test.go b/internal/llmapp/overview_test.go
index 2280f0d..9034ecc 100644
--- a/internal/llmapp/overview_test.go
+++ b/internal/llmapp/overview_test.go
@@ -9,6 +9,7 @@
"strings"
"testing"
+ "github.com/google/go-cmp/cmp"
"golang.org/x/oscar/internal/llm"
)
@@ -17,19 +18,45 @@
g := llm.EchoTextGenerator()
d1 := &Doc{URL: "https://example.com", Author: "rsc", Title: "title", Text: "some text"}
d2 := &Doc{Text: "some text 2"}
- got, err := Overview(ctx, g, Documents, d1, d2)
+ got, err := Overview(ctx, g, d1, d2)
if err != nil {
t.Fatal(err)
}
- prompt := Documents.instructions()
- want := llm.EchoResponse(
+ promptParts := []string{
`{"url":"https://example.com","author":"rsc","title":"title","text":"some text"}`,
`{"text":"some text 2"}`,
- prompt)
- if got != want {
- t.Errorf("Overview() = %s, want %s", got, want)
+ documents.instructions(),
}
- t.Log(want)
+ want := &OverviewResult{
+ Overview: llm.EchoResponse(promptParts...),
+ Prompt: promptParts,
+ }
+ if diff := cmp.Diff(got, want); diff != "" {
+ t.Errorf("Overview() mismatch (-got +want):\n%s", diff)
+ }
+}
+
+func TestPostOverview(t *testing.T) {
+ ctx := context.Background()
+ g := llm.EchoTextGenerator()
+ d1 := &Doc{URL: "https://example.com", Author: "rsc", Title: "title", Text: "some text"}
+ d2 := &Doc{Text: "some text 2"}
+ got, err := PostOverview(ctx, g, d1, []*Doc{d2})
+ if err != nil {
+ t.Fatal(err)
+ }
+ promptParts := []string{
+ `{"url":"https://example.com","author":"rsc","title":"title","text":"some text"}`,
+ `{"text":"some text 2"}`,
+ postAndComments.instructions(),
+ }
+ want := &OverviewResult{
+ Overview: llm.EchoResponse(promptParts...),
+ Prompt: promptParts,
+ }
+ if diff := cmp.Diff(got, want); diff != "" {
+ t.Errorf("PostOverview() mismatch (-got +want):\n%s", diff)
+ }
}
func TestInstructions(t *testing.T) {
@@ -37,7 +64,7 @@
wantPost := "post" // only in PostAndComments
t.Run("Documents", func(t *testing.T) {
- di := Documents.instructions()
+ di := documents.instructions()
if !strings.Contains(di, wantAll) {
t.Errorf("Documents.instructions(): does not contain %q", wantAll)
}
@@ -47,7 +74,7 @@
})
t.Run("PostAndComments", func(t *testing.T) {
- pi := PostAndComments.instructions()
+ pi := postAndComments.instructions()
if !strings.Contains(pi, wantAll) {
t.Fatalf("PostAndComments.instructions(): does not contain %q", wantAll)
}