Ctrl K
ComunidadGuías…

internal/task/schedule_write.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package task

import (
	"errors"
	"strings"
	"time"
)

var (
	ErrScheduleRequesterRequired       = errors.New("requester is required")
	ErrScheduleConversationRequired    = errors.New("platform and conversationID are required")
	ErrScheduleReplyTargetRequired     = errors.New("replyTargetID is required")
	ErrScheduleTaskInstructionRequired = errors.New("taskInstruction is required")
	ErrScheduleUpdateFieldRequired     = errors.New("at least one field to change is required")
	ErrScheduleKindInvalid             = errors.New("kind must be once, interval or cron")
	ErrScheduleTimeZoneInvalid         = errors.New("timeZone must be a valid IANA time zone")
	ErrScheduleRunAtInvalid            = errors.New("runAt must be RFC3339")
	ErrScheduleExpiresAtInvalid        = errors.New("expiresAt must be a future RFC3339 timestamp")
	ErrScheduleRepeatPolicyRequired    = errors.New("interval and cron schedules require repeatPolicy finite or unbounded")
	ErrScheduleFiniteBoundRequired     = errors.New("finite interval and cron schedules require expiresAt or maxRunCount")
	ErrScheduleNoFutureRun             = errors.New("task schedule has no future run")
)

type ScheduleCreateInput struct {
	Description     string
	TaskInstruction string
	Kind            string
	RunAt           string
	ExpiresAt       string
	IntervalSecond  int
	CronExpression  string
	TimeZone        string
	MaxRunCount     int
	RepeatPolicy    string
}

type ScheduleUpdateInput struct {
	Description      *string
	TaskInstruction  *string
	AgentProfileName *string
	Kind             *string
	RunAt            *string
	ExpiresAt        *string
	IntervalSecond   *int
	CronExpression   *string
	TimeZone         *string
	MaxRunCount      *int
	RepeatPolicy     *string
}

type ScheduleDeliveryBinding struct {
	Platform       string
	ConversationID string
	ReplyTargetID  string
}

type ScheduleCreateContext struct {
	CreatorPersonID  string
	AgentProfileName string
	Delivery         ScheduleDeliveryBinding
	CompanyTimeZone  string
	IsApproved       bool
	ReferenceTime    time.Time
}

type ScheduleMutationResult struct {
	ScheduleID       string     `json:"scheduleID"`
	Description      string     `json:"description"`
	TaskInstruction  string     `json:"taskInstruction"`
	TimeZone         string     `json:"timeZone"`
	Kind             string     `json:"kind"`
	RunAt            *time.Time `json:"runAt,omitempty"`
	IntervalSecond   int        `json:"intervalSecond,omitempty"`
	CronExpression   string     `json:"cronExpression,omitempty"`
	MaxRunCount      int        `json:"maxRunCount,omitempty"`
	ExpiresAt        *time.Time `json:"expiresAt,omitempty"`
	NextRunAt        *time.Time `json:"nextRunAt,omitempty"`
	ConversationID   string     `json:"conversationID"`
	ReplyTargetID    string     `json:"replyTargetID"`
	AgentProfileName string     `json:"agentProfileName"`
}

func BuildScheduleCreate(input ScheduleCreateInput, createContext ScheduleCreateContext) (Schedule, error) {
	if errorValue := requireScheduleDelivery(createContext); errorValue != nil {
		return Schedule{}, errorValue
	}
	taskInstruction := strings.TrimSpace(input.TaskInstruction)
	if taskInstruction == "" {
		return Schedule{}, ErrScheduleTaskInstructionRequired
	}
	kind, errorValue := parseScheduleKind(input.Kind)
	if errorValue != nil {
		return Schedule{}, errorValue
	}
	timeZone, errorValue := normalizeScheduleTimeZone(input.TimeZone, createContext.CompanyTimeZone)
	if errorValue != nil {
		return Schedule{}, errorValue
	}
	runAt, errorValue := parseScheduleRunAt(input.RunAt)
	if errorValue != nil {
		return Schedule{}, errorValue
	}
	expiresAt, errorValue := parseScheduleExpiresAt(input.ExpiresAt, createContext.ReferenceTime)
	if errorValue != nil {
		return Schedule{}, errorValue
	}
	schedule := Schedule{
		ScheduleID:       NewIdentifier(),
		CreatorPersonID:  strings.TrimSpace(createContext.CreatorPersonID),
		Name:             firstNonBlankScheduleValue(input.Description, taskInstruction),
		Prompt:           taskInstruction,
		ExecutionMode:    ScheduleExecutionModeAgent,
		AgentProfileName: firstNonBlankScheduleValue(createContext.AgentProfileName, "default"),
		Platform:         strings.TrimSpace(createContext.Delivery.Platform),
		ConversationID:   strings.TrimSpace(createContext.Delivery.ConversationID),
		ReplyTargetID:    strings.TrimSpace(createContext.Delivery.ReplyTargetID),
		TimeZone:         timeZone,
		Kind:             kind,
		RunAt:            runAt,
		IntervalSecond:   input.IntervalSecond,
		CronExpression:   strings.TrimSpace(input.CronExpression),
		MaxRunCount:      input.MaxRunCount,
		ExpiresAt:        expiresAt,
		CreatedAt:        createContext.ReferenceTime,
		UpdatedAt:        createContext.ReferenceTime,
		NextAttemptAt:    &createContext.ReferenceTime,
	}
	if errorValue := validateScheduleRepeatPolicy(schedule, input.RepeatPolicy); errorValue != nil {
		return Schedule{}, errorValue
	}
	if createContext.IsApproved {
		schedule.ApprovedAt = &createContext.ReferenceTime
	}
	return schedule, nil
}

func InitializeScheduleCreate(input ScheduleCreateInput, createContext ScheduleCreateContext) (Schedule, error) {
	schedule, errorValue := BuildScheduleCreate(input, createContext)
	if errorValue != nil {
		return Schedule{}, errorValue
	}
	return initializedScheduleWithFutureRun(schedule, createContext.ReferenceTime)
}

func ApplyScheduleUpdate(schedule Schedule, input ScheduleUpdateInput, companyTimeZone string, referenceTime time.Time) (Schedule, error) {
	if schedule.CarriesAnApprovedCall() {
		return Schedule{}, ErrScheduleCarriesAnApproval
	}
	if input.Description != nil {
		schedule.Name = strings.TrimSpace(*input.Description)
	}
	if input.TaskInstruction != nil {
		taskInstruction := strings.TrimSpace(*input.TaskInstruction)
		if taskInstruction == "" {
			return Schedule{}, ErrScheduleTaskInstructionRequired
		}
		schedule.Prompt = taskInstruction
	}
	if input.AgentProfileName != nil {
		schedule.AgentProfileName = strings.TrimSpace(*input.AgentProfileName)
	}
	if input.TimeZone != nil {
		timeZone, errorValue := normalizeScheduleTimeZone(*input.TimeZone, companyTimeZone)
		if errorValue != nil {
			return Schedule{}, errorValue
		}
		schedule.TimeZone = timeZone
	}
	updatedSchedule, errorValue := applyScheduleUpdateCadence(schedule, input, referenceTime)
	if errorValue != nil {
		return Schedule{}, errorValue
	}
	updatedSchedule.UpdatedAt = referenceTime
	updatedSchedule.NextAttemptAt = &referenceTime
	return initializedScheduleWithFutureRun(updatedSchedule, referenceTime)
}

func ScheduleUpdateChangesNothing(input ScheduleUpdateInput) bool {
	return input.Description == nil && input.TaskInstruction == nil && input.AgentProfileName == nil &&
		input.Kind == nil && input.RunAt == nil && input.ExpiresAt == nil &&
		input.IntervalSecond == nil && input.CronExpression == nil &&
		input.TimeZone == nil && input.MaxRunCount == nil && input.RepeatPolicy == nil
}

func ProjectScheduleMutation(schedule Schedule) ScheduleMutationResult {
	return ScheduleMutationResult{
		ScheduleID:       schedule.ScheduleID,
		Description:      schedule.Name,
		TaskInstruction:  schedule.Prompt,
		TimeZone:         schedule.TimeZone,
		Kind:             string(schedule.Kind),
		RunAt:            schedule.RunAt,
		IntervalSecond:   schedule.IntervalSecond,
		CronExpression:   schedule.CronExpression,
		MaxRunCount:      schedule.MaxRunCount,
		ExpiresAt:        schedule.ExpiresAt,
		NextRunAt:        schedule.NextRunAt,
		ConversationID:   schedule.ConversationID,
		ReplyTargetID:    schedule.ReplyTargetID,
		AgentProfileName: schedule.AgentProfileName,
	}
}

func requireScheduleDelivery(createContext ScheduleCreateContext) error {
	if strings.TrimSpace(createContext.CreatorPersonID) == "" {
		return ErrScheduleRequesterRequired
	}
	if strings.TrimSpace(createContext.Delivery.Platform) == "" || strings.TrimSpace(createContext.Delivery.ConversationID) == "" {
		return ErrScheduleConversationRequired
	}
	if strings.TrimSpace(createContext.Delivery.ReplyTargetID) == "" {
		return ErrScheduleReplyTargetRequired
	}
	return nil
}

func normalizeScheduleTimeZone(value string, companyTimeZone string) (string, error) {
	timeZone := ScheduleTimeZoneName(firstNonBlankScheduleValue(value, companyTimeZone))
	if _, errorValue := time.LoadLocation(timeZone); errorValue != nil {
		return "", ErrScheduleTimeZoneInvalid
	}
	return timeZone, nil
}

func IsScheduleWriteInputError(errorValue error) bool {
	for _, inputError := range []error{
		ErrScheduleRequesterRequired,
		ErrScheduleConversationRequired,
		ErrScheduleReplyTargetRequired,
		ErrScheduleTaskInstructionRequired,
		ErrScheduleUpdateFieldRequired,
		ErrScheduleKindInvalid,
		ErrScheduleTimeZoneInvalid,
		ErrScheduleRunAtInvalid,
		ErrScheduleExpiresAtInvalid,
		ErrScheduleRepeatPolicyRequired,
		ErrScheduleFiniteBoundRequired,
		ErrScheduleNoFutureRun,
		ErrScheduleLimitReached,
		ErrScheduleCarriesAnApproval,
		ErrApprovedCallNeedsAFutureStart,
		errorInvalidSchedule,
		errorInvalidCronExpression,
		errorUnableToFindNextTaskRun,
	} {
		if errors.Is(errorValue, inputError) {
			return true
		}
	}
	return false
}

func applyScheduleUpdateCadence(schedule Schedule, input ScheduleUpdateInput, referenceTime time.Time) (Schedule, error) {
	if input.Kind != nil {
		kind, errorValue := parseScheduleKind(*input.Kind)
		if errorValue != nil {
			return Schedule{}, errorValue
		}
		schedule.Kind = kind
	}
	if input.RunAt != nil {
		runAt, errorValue := parseScheduleRunAt(*input.RunAt)
		if errorValue != nil {
			return Schedule{}, errorValue
		}
		schedule.RunAt = runAt
	}
	if input.ExpiresAt != nil {
		expiresAt, errorValue := parseScheduleExpiresAt(*input.ExpiresAt, referenceTime)
		if errorValue != nil {
			return Schedule{}, errorValue
		}
		schedule.ExpiresAt = expiresAt
	}
	if input.IntervalSecond != nil {
		schedule.IntervalSecond = *input.IntervalSecond
	}
	if input.CronExpression != nil {
		schedule.CronExpression = strings.TrimSpace(*input.CronExpression)
	}
	if input.MaxRunCount != nil {
		schedule.MaxRunCount = *input.MaxRunCount
	}
	normalizeScheduleCadenceFields(&schedule)
	if errorValue := validateScheduleRepeatPolicy(schedule, scheduleRepeatPolicyOf(input)); errorValue != nil {
		return Schedule{}, errorValue
	}
	return schedule, nil
}

func initializedScheduleWithFutureRun(schedule Schedule, referenceTime time.Time) (Schedule, error) {
	initializedSchedule, errorValue := (Scheduler{}).InitializeSchedule(schedule, referenceTime)
	if errorValue != nil {
		return Schedule{}, errorValue
	}
	if initializedSchedule.NextRunAt == nil {
		return Schedule{}, ErrScheduleNoFutureRun
	}
	return initializedSchedule, nil
}

func normalizeScheduleCadenceFields(schedule *Schedule) {
	switch schedule.Kind {
	case ScheduleKindOnce:
		schedule.IntervalSecond = 0
		schedule.CronExpression = ""
		schedule.MaxRunCount = 0
	case ScheduleKindInterval:
		schedule.CronExpression = ""
	case ScheduleKindCron:
		schedule.IntervalSecond = 0
	}
}

func validateScheduleRepeatPolicy(schedule Schedule, repeatPolicy string) error {
	if schedule.Kind != ScheduleKindInterval && schedule.Kind != ScheduleKindCron {
		return nil
	}
	if schedule.MaxRunCount > 0 || schedule.ExpiresAt != nil {
		return nil
	}
	switch strings.TrimSpace(repeatPolicy) {
	case "unbounded":
		return nil
	case "finite":
		return ErrScheduleFiniteBoundRequired
	default:
		return ErrScheduleRepeatPolicyRequired
	}
}

func scheduleRepeatPolicyOf(input ScheduleUpdateInput) string {
	if input.RepeatPolicy == nil {
		return ""
	}
	return *input.RepeatPolicy
}

func parseScheduleKind(value string) (ScheduleKind, error) {
	switch strings.TrimSpace(value) {
	case string(ScheduleKindOnce):
		return ScheduleKindOnce, nil
	case string(ScheduleKindInterval):
		return ScheduleKindInterval, nil
	case string(ScheduleKindCron):
		return ScheduleKindCron, nil
	default:
		return "", ErrScheduleKindInvalid
	}
}

func parseScheduleRunAt(value string) (*time.Time, error) {
	trimmedValue := strings.TrimSpace(value)
	if trimmedValue == "" {
		return nil, nil
	}
	runAt, errorValue := time.Parse(time.RFC3339, trimmedValue)
	if errorValue != nil {
		return nil, ErrScheduleRunAtInvalid
	}
	return &runAt, nil
}

func parseScheduleExpiresAt(value string, referenceTime time.Time) (*time.Time, error) {
	trimmedValue := strings.TrimSpace(value)
	if trimmedValue == "" {
		return nil, nil
	}
	expiresAt, errorValue := time.Parse(time.RFC3339, trimmedValue)
	if errorValue != nil {
		return nil, ErrScheduleExpiresAtInvalid
	}
	expiresAt = expiresAt.UTC()
	if !expiresAt.After(referenceTime) {
		return nil, ErrScheduleExpiresAtInvalid
	}
	return &expiresAt, nil
}

func firstNonBlankScheduleValue(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}