diff --git a/addons/internal/connector/route.go b/addons/internal/connector/route.go index de4ba0d347..9608b0d735 100644 --- a/addons/internal/connector/route.go +++ b/addons/internal/connector/route.go @@ -93,6 +93,14 @@ type Drafter interface { Draft(ctx context.Context, req contract.ConnectorRequest) error } +// HistoryReader is a Platform that reads the earlier messages of a thread for +// the op history. It returns the messages oldest first, the newest of those +// before req.Before, at most req.Limit of them, and whether older messages +// exist. Serve checks the request and bounds the answer. +type HistoryReader interface { + History(ctx context.Context, req contract.ConnectorRequest) ([]contract.ConnectorHistoryMessage, bool, error) +} + // Splitter is a Platform that cuts its texts itself; others use Split. type Splitter interface { Split(text string) []string @@ -159,6 +167,24 @@ func Serve(ctx context.Context, p Platform, req contract.ConnectorRequest) (cont if f, err = m.Media(ctx, req.Token, req.Media); err == nil { out.File = &f } + case contract.ConnectorOpHistory: + h, ok := p.(HistoryReader) + if !ok || req.Chat == "" || req.Thread == "" { + return contract.ConnectorResponse{}, fmt.Errorf("%w: no thread, or a platform that cannot read one", contract.ErrRefused) + } + if req.Limit < 1 || req.Limit > contract.MaxHistoryMessages { + req.Limit = contract.DefaultHistoryMessages + } + // Not retried after a wait: the history is context, and the host + // would rather answer without it than hold the answer behind it. + var msgs []contract.ConnectorHistoryMessage + var more bool + if msgs, more, err = h.History(ctx, req); err == nil { + if len(msgs) > req.Limit { + msgs, more = msgs[len(msgs)-req.Limit:], true + } + out.Messages, out.Truncated = msgs, more + } default: return contract.ConnectorResponse{}, fmt.Errorf("%w: unknown op", contract.ErrRefused) } diff --git a/addons/internal/connector/route_test.go b/addons/internal/connector/route_test.go index c2e6fa4d01..22a0bf104a 100644 --- a/addons/internal/connector/route_test.go +++ b/addons/internal/connector/route_test.go @@ -94,6 +94,19 @@ func (r *rich) Draft(_ context.Context, req contract.ConnectorRequest) error { return r.err } +// reading also reads the history of a thread. +type reading struct { + script + msgs []contract.ConnectorHistoryMessage + more bool + got []contract.ConnectorRequest +} + +func (r *reading) History(_ context.Context, req contract.ConnectorRequest) ([]contract.ConnectorHistoryMessage, bool, error) { + r.got = append(r.got, req) + return r.msgs, r.more, r.err +} + var _ = Describe("Serve", func() { ctx := context.Background() req := func(op string) contract.ConnectorRequest { @@ -287,4 +300,44 @@ var _ = Describe("Serve", func() { } Expect(f.files).To(HaveLen(1)) }) + + Describe("the op history", func() { + hist := func() contract.ConnectorRequest { + r := req("history") + r.Thread, r.Limit, r.Before = "t1", 2, "9" + return r + } + It("is refused on a platform that cannot read a thread, and without a thread", func() { + _, err := connector.Serve(ctx, &script{}, hist()) + Expect(err).To(MatchError(contract.ErrRefused)) + r := hist() + r.Thread = "" + _, err = connector.Serve(ctx, &reading{}, r) + Expect(err).To(MatchError(contract.ErrRefused)) + }) + It("returns the newest messages within the limit and says that older ones exist", func() { + s := &reading{msgs: []contract.ConnectorHistoryMessage{{Text: "a"}, {Text: "b"}, {Text: "c"}}} + out, err := connector.Serve(ctx, s, hist()) + Expect(err).NotTo(HaveOccurred()) + Expect(out.Messages).To(HaveLen(2)) + Expect(out.Messages[0].Text).To(Equal("b")) + Expect(out.Truncated).To(BeTrue()) + Expect(s.got[0].Before).To(Equal("9")) + }) + It("uses the default limit when the host asks for none or too many", func() { + s := &reading{} + r := hist() + r.Limit = contract.MaxHistoryMessages + 1 + _, err := connector.Serve(ctx, s, r) + Expect(err).NotTo(HaveOccurred()) + Expect(s.got[0].Limit).To(Equal(contract.DefaultHistoryMessages)) + }) + It("types the error of the platform and does not wait for a rate limit", func() { + s := &reading{script: script{err: &connector.RetryAfter{Seconds: 30}}} + out, err := connector.Serve(ctx, s, hist()) + Expect(err).NotTo(HaveOccurred()) + Expect(out.Error.Code).To(Equal(contract.ConnectorRateLimited)) + Expect(s.got).To(HaveLen(1)) + }) + }) })