package agents import ( "context" "errors" "fmt" "log/slog" "slices" "strings" "time" "g.fc.run/ai/byre/internal/api" "g.fc.run/ai/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 } resolutions = append(resolutions, store.Resolution{ State: state, Response: newResponse(item.Id, user, create), }) 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 newResponse(itemID, user string, create api.InboxResponseCreate) api.InboxResponse { attachments := []api.Attachment{} if create.Attachments != nil { attachments = *create.Attachments } images := []string{} if create.Images != nil { images = *create.Images } return api.InboxResponse{ Id: store.NewID(), ItemId: itemID, User: user, Type: create.Type, Options: create.Options, Text: create.Text, Images: images, Attachments: attachments, CreatedAt: time.Now().UTC(), } } func (m *Manager) Dismiss(ctx context.Context, user, itemID string) (api.InboxItem, error) { item, err := m.store.GetInboxItem(ctx, itemID) if err != nil { return api.InboxItem{}, err } err = m.store.DismissItem(ctx, itemID, user) if errors.Is(err, store.ErrNotOpen) { return api.InboxItem{}, fmt.Errorf("%w: item %s is %s", ErrConflict, item.Id, item.State) } if err != nil { return api.InboxItem{}, err } slog.Info("inbox item dismissed", "item", itemID, "user", user) go func() { err := m.Deliver(context.Background(), item.AgentSessionId) if err != nil { slog.Error("deliver inbox responses", "session", item.AgentSessionId, "err", err) } }() return m.store.GetInboxItem(ctx, itemID) } 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) } images := c.Images != nil && len(*c.Images) > 0 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 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 } return api.Answered, nil case api.Other: if text == "" && !images { return bad("other needs text or images") } return api.Answered, 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 }