internal/llmapp: add Overview

Add function Overview which uses an LLM to summarize documents.

Change-Id: Ie22afddde2ea59b81744b7c264724eccbc68f4a7
Reviewed-on: https://go-review.googlesource.com/c/oscar/+/621819
Reviewed-by: Jonathan Amsterdam <jba@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/internal/llmapp/overview.go b/internal/llmapp/overview.go
new file mode 100644
index 0000000..b4d2c14
--- /dev/null
+++ b/internal/llmapp/overview.go
@@ -0,0 +1,57 @@
+// 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 llmapp provides applications for LLM content generation
+// to complete higher-level tasks.
+package llmapp
+
+import (
+	"context"
+	"errors"
+
+	"golang.org/x/oscar/internal/llm"
+	"golang.org/x/oscar/internal/storage"
+)
+
+// A Doc is a document to provide to an LLM as part of a prompt.
+type Doc struct {
+	// Freeform text describing the type of document.
+	// Used to help the LLM distinguish between kinds of
+	// documents, or understand their relative importance.
+	Type string `json:"type,omitempty"`
+	// The URL of the document, if one exists.
+	URL string `json:"url,omitempty"`
+	// The author of the document, if known.
+	Author string `json:"author,omitempty"`
+	// The title of the document, if known.
+	Title string `json:"title,omitempty"`
+	Text  string `json:"text"` // required
+}
+
+// Overview returns an LLM-generated overview of the given documents,
+// styled with markdown.
+// It 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) (string, error) {
+	if len(docs) == 0 {
+		return "", errors.New("llmapp.Overview: no documents")
+	}
+	return g.GenerateText(ctx, OverviewPrompt(docs)...)
+}
+
+// OverviewPrompt converts the given docs into a slice of
+// text prompts, followed by a hard-coded instruction prompt.
+func OverviewPrompt(docs []*Doc) []string {
+	var inputs = make([]string, len(docs))
+	for i, d := range docs {
+		inputs[i] = string(storage.JSON(d))
+	}
+	return append(inputs, instruction)
+}
+
+// The hard-coded instruction to use when generating an overview.
+// In the future this might be a client-provided input to [Overview]
+// instead.
+const instruction = `Write a detailed summary of the documents.
+Use markdown formatting with headings and lists.`
diff --git a/internal/llmapp/overview_test.go b/internal/llmapp/overview_test.go
new file mode 100644
index 0000000..3c27c7d
--- /dev/null
+++ b/internal/llmapp/overview_test.go
@@ -0,0 +1,31 @@
+// 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 llmapp
+
+import (
+	"context"
+	"testing"
+
+	"golang.org/x/oscar/internal/llm"
+)
+
+func TestOverview(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 := Overview(ctx, g, d1, d2)
+	if err != nil {
+		t.Fatal(err)
+	}
+	want := llm.EchoResponse(
+		`{"url":"https://example.com","author":"rsc","title":"title","text":"some text"}`,
+		`{"text":"some text 2"}`,
+		instruction)
+	if got != want {
+		t.Errorf("Overview() = %s, want %s", got, want)
+	}
+	t.Log(want)
+}