package dialogue import "time" // Capability names the thing being assembled across a clarify exchange — // "reminder.create", not "reminder". A router intent says what she heard; a // capability says what she is about to do, and those are not the same word: // three intents currently reach exactly one capability each, but a fact key // that turns out to be a Hexis target does not. Named in the ecosystem's // dotted form because that is what a confirmation binds (cmd/mavend/confirm.go) // and what Hexis registers. // // This package must stay free of internal/router (the cycle rule that makes // Slots a hand-kept copy), so the mapping from an intent lives here and reads // off dialogue.Intent only. type Capability string const ( CapReminderCreate Capability = "reminder.create" CapFactWrite Capability = "fact.write" CapNoteWrite Capability = "note.write" CapActRun Capability = "act.run" CapQueryAnswer Capability = "query.answer" CapChatReply Capability = "chat.reply" CapSystemControl Capability = "system.control" ) // intentCapability — the one place an intent becomes a capability. Every intent // is listed, including the four that are never worth a clarifying question, so a // parked action always knows what it is even when nothing asks it. var intentCapability = map[Intent]Capability{ IntentReminder: CapReminderCreate, IntentFact: CapFactWrite, IntentNote: CapNoteWrite, IntentAct: CapActRun, IntentQuery: CapQueryAnswer, IntentChat: CapChatReply, IntentSystem: CapSystemControl, } // CapabilityFor maps a router intent (already narrowed to dialogue.Intent by // the caller) to the capability being assembled. "" for an intent she does not // recognise — an unknown intent must not silently become a real capability. func CapabilityFor(in Intent) Capability { return intentCapability[in] } // PendingAction is the action Maven is assembling, as an object rather than as // conversational history: which capability, the slots it already has, the slots // it is still missing, when she asked, how many questions that has cost and how // long the answer stays welcome. // // It exists because the resolver used to have to infer all of that from a // parked question plus the previous turn (Vikunja #558): "is this his answer or // a new request" is answerable against an object and guessy against a // transcript. PendingQuestion carries one of these and keeps its own flat // fields, so this is a widening — nothing reads the capability yet. type PendingAction struct { Capability Capability Slots Slots // what is filled so far Missing []Slot // what she is waiting for, in the order to ask about Utterance string // his original raw words, as the action's provenance Asked time.Time TTL time.Duration Attempts int // questions already asked about this action // MaxAttempts caps Attempts. 0 ⇒ DefaultMaxAttempts. MaxAttempts int } // maxAttempts is MaxAttempts with the default filled in. func (a *PendingAction) maxAttempts() int { if a.MaxAttempts <= 0 { return DefaultMaxAttempts } return a.MaxAttempts } // IsExpired — the answer came too late for this action to still be his answer. func (a *PendingAction) IsExpired(now time.Time) bool { return now.After(a.Asked.Add(a.TTL)) } // CanAsk reports whether she may ask another question about this action. func (a *PendingAction) CanAsk() bool { return a.Attempts < a.maxAttempts() } // Gaps lists the slots this action asked for and still does not have. Computed // from the slots rather than trusted from Missing, because Missing is what she // asked about and the slots are what she got — an answer can fill a gap she // never asked about, and a re-park must not ask again for something now filled. func (a *PendingAction) Gaps() []Slot { return StillMissing(a.Missing, a.Slots) } // Complete reports whether every slot this action was waiting for is filled, so // it can run. Note that this is completeness against what she ASKED, not // against the capability's whole schema — validating that is V-562. func (a *PendingAction) Complete() bool { return len(a.Gaps()) == 0 }