6759ff6003
A parked clarify said what she heard (an intent) and not what she was about to do, so the resolver had to infer the action from conversational history instead of reading it off an object. PendingAction names the capability being assembled in the ecosystem's dotted form (reminder.create, fact.write, act.run), the slots it has, the slots it still wants, when it was asked, attempts and TTL. Gaps() computes the missing slots from the slots rather than trusting Missing, because Missing is what she asked and the slots are what she got. CapabilityFor maps every dialogue.Intent, so the mapping lives here and dialogue still does not import router (the cycle rule). Nothing reads it yet: this is the widening V-560 to V-562 build on.
102 lines
4.1 KiB
Go
102 lines
4.1 KiB
Go
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
|
|
}
|