164 lines
3.9 KiB
Go
164 lines
3.9 KiB
Go
package agents
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"g.fc.run/misc/byre/internal/api"
|
|
"g.fc.run/misc/byre/internal/store"
|
|
)
|
|
|
|
func (m *Manager) Respond(ctx context.Context, user string, creates []api.InboxResponseCreate) ([]api.InboxItem, error) {
|
|
if len(creates) == 0 {
|
|
return nil, fmt.Errorf("%w: responses is empty", ErrInvalid)
|
|
}
|
|
|
|
resolutions := []store.Resolution{}
|
|
sessions := []string{}
|
|
seen := map[string]bool{}
|
|
|
|
for _, create := range creates {
|
|
if seen[create.ItemId] {
|
|
return nil, fmt.Errorf("%w: item %s answered twice in one submission", ErrInvalid, create.ItemId)
|
|
}
|
|
seen[create.ItemId] = true
|
|
|
|
item, err := m.store.GetInboxItem(ctx, create.ItemId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
state, err := CheckResponse(item, create)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
attachments := []api.Attachment{}
|
|
if create.Attachments != nil {
|
|
attachments = *create.Attachments
|
|
}
|
|
|
|
resolutions = append(resolutions, store.Resolution{
|
|
State: state,
|
|
Response: api.InboxResponse{
|
|
Id: store.NewID(),
|
|
ItemId: item.Id,
|
|
User: user,
|
|
Type: create.Type,
|
|
Options: create.Options,
|
|
Text: create.Text,
|
|
Attachments: attachments,
|
|
CreatedAt: time.Now().UTC(),
|
|
},
|
|
})
|
|
if !slices.Contains(sessions, item.AgentSessionId) {
|
|
sessions = append(sessions, item.AgentSessionId)
|
|
}
|
|
}
|
|
|
|
err := m.store.Respond(ctx, user, resolutions)
|
|
if errors.Is(err, store.ErrNotOpen) {
|
|
return nil, fmt.Errorf("%w: %w", ErrConflict, err)
|
|
}
|
|
if err != nil {
|
|
slog.Error("record inbox responses", "user", user, "err", err)
|
|
return nil, err
|
|
}
|
|
for _, res := range resolutions {
|
|
slog.Info("inbox response recorded", "item", res.Response.ItemId, "response", res.Response.Id, "type", res.Response.Type, "user", user)
|
|
}
|
|
|
|
for _, id := range sessions {
|
|
go func() {
|
|
err := m.Deliver(context.Background(), id)
|
|
if err != nil {
|
|
slog.Error("deliver inbox responses", "session", id, "err", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
items := []api.InboxItem{}
|
|
for _, res := range resolutions {
|
|
item, err := m.store.GetInboxItem(ctx, res.Response.ItemId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func CheckResponse(item api.InboxItem, c api.InboxResponseCreate) (api.InboxState, error) {
|
|
if item.State != api.Open {
|
|
return "", fmt.Errorf("%w: item %s is %s", ErrConflict, item.Id, item.State)
|
|
}
|
|
|
|
text := ""
|
|
if c.Text != nil {
|
|
text = strings.TrimSpace(*c.Text)
|
|
}
|
|
question := item.Kind == api.Question
|
|
bad := func(format string, args ...any) (api.InboxState, error) {
|
|
return "", fmt.Errorf("%w: item %s: "+format, append([]any{ErrInvalid, item.Id}, args...)...)
|
|
}
|
|
|
|
switch c.Type {
|
|
case api.Option:
|
|
if !question {
|
|
return bad("only questions have options")
|
|
}
|
|
if c.Options == nil || len(*c.Options) == 0 {
|
|
return bad("option needs the chosen options")
|
|
}
|
|
if item.Payload["multiple"] != true && len(*c.Options) != 1 {
|
|
return bad("a pick-one question takes exactly one option")
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, o := range *c.Options {
|
|
if !hasOption(item, o) {
|
|
return bad("%q is not one of the question's options", o)
|
|
}
|
|
if seen[o] {
|
|
return bad("%q chosen twice", o)
|
|
}
|
|
seen[o] = true
|
|
}
|
|
if text != "" && item.Payload["multiple"] != true {
|
|
return bad("a pick-one option takes no text; use other")
|
|
}
|
|
return api.Answered, nil
|
|
|
|
case api.Other:
|
|
if text == "" {
|
|
return bad("other needs text")
|
|
}
|
|
return api.Answered, nil
|
|
|
|
case api.Dismiss:
|
|
if question {
|
|
return bad("questions need an option or other, not a dismissal")
|
|
}
|
|
return api.Dismissed, nil
|
|
}
|
|
|
|
return bad("unknown response type %q", c.Type)
|
|
}
|
|
|
|
func hasOption(item api.InboxItem, option string) bool {
|
|
options, ok := item.Payload["options"].([]any)
|
|
if !ok {
|
|
return false
|
|
}
|
|
for _, o := range options {
|
|
if o == option {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|