377 lines
15 KiB
Go
377 lines
15 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.cin.su/m/Assistant/contracts/capability"
|
|
"git.cin.su/m/Assistant/contracts/continuity"
|
|
"git.cin.su/m/Assistant/contracts/epistemics"
|
|
"git.cin.su/m/Assistant/contracts/interaction"
|
|
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
|
|
epistemicsdomain "git.cin.su/m/Assistant/modules/epistemics/domain"
|
|
)
|
|
|
|
const reminderResponseSkillID = capability.SkillProfileID("skill-obvious-reminder-response-v1")
|
|
|
|
type learningEpistemicsStore interface {
|
|
SaveSystemExpectation(context.Context, epistemics.SystemExpectation) error
|
|
SaveExperienceRecord(context.Context, epistemics.ExperienceRecord) error
|
|
SaveReflectionCandidate(context.Context, epistemics.ReflectionCandidate) error
|
|
}
|
|
|
|
type learningCapabilityStore interface {
|
|
LoadSkillProfile(context.Context, capability.SkillProfileID) (capability.SkillProfile, error)
|
|
SaveSkillProfile(context.Context, capability.SkillProfile) error
|
|
}
|
|
|
|
type learningRecorder struct {
|
|
epistemics learningEpistemicsStore
|
|
capability capabilityLearningStore
|
|
}
|
|
|
|
type capabilityLearningStore interface {
|
|
learningCapabilityStore
|
|
}
|
|
|
|
type reminderResponseLearningInput struct {
|
|
Utterance interaction.HumanUtterance
|
|
Intent interaction.OriginalHumanIntent
|
|
Result continuity.HumanResponseResult
|
|
At time.Time
|
|
}
|
|
|
|
type correctionLearningInput struct {
|
|
Utterance interaction.HumanUtterance
|
|
Intent interaction.OriginalHumanIntent
|
|
At time.Time
|
|
}
|
|
|
|
type fastPathDeclineLearningInput struct {
|
|
Utterance interaction.HumanUtterance
|
|
Intent interaction.OriginalHumanIntent
|
|
Result continuity.HumanResponseResult
|
|
Reason capability.FastPathDeclineReason
|
|
At time.Time
|
|
}
|
|
|
|
type capabilityLearningInput struct {
|
|
Utterance interaction.HumanUtterance
|
|
Intent interaction.OriginalHumanIntent
|
|
ImplementationID capability.CapabilityImplementationID
|
|
UsageObservation capability.CapabilityUsageObservationID
|
|
GapID capability.CapabilityGapID
|
|
Outcome capability.UsageOutcomeClass
|
|
OutcomeRef string
|
|
At time.Time
|
|
}
|
|
|
|
type delegationLearningInput struct {
|
|
Intent interaction.OriginalHumanIntent
|
|
Utterance interaction.HumanUtterance
|
|
DelegationID string
|
|
RunID string
|
|
ImplementationID capability.CapabilityImplementationID
|
|
OutcomeRef string
|
|
VerificationRefs []epistemics.EvidenceRef
|
|
Verified bool
|
|
ExecutionWindows int
|
|
HumanBlocked bool
|
|
At time.Time
|
|
}
|
|
|
|
func (r learningRecorder) recordReminderResponse(ctx context.Context, input reminderResponseLearningInput) {
|
|
if r.epistemics == nil || r.capability == nil || input.Intent.ID == "" || input.Utterance.ID == "" || input.Result.Response.ID == "" {
|
|
return
|
|
}
|
|
expectedEffect := "resolve obvious reminder continuation using deterministic target resolver"
|
|
if input.Result.Response.TargetProspectiveID != "" {
|
|
expectedEffect += " for prospective item " + string(input.Result.Response.TargetProspectiveID)
|
|
}
|
|
expectation := epistemics.SystemExpectation{
|
|
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
|
|
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
|
ExpectedOutcome: epistemics.OutcomeSuccess,
|
|
ExpectedEffect: expectedEffect,
|
|
ConfidenceMillis: 850,
|
|
EvidenceBasis: []epistemics.EvidenceRef{"continuity:response-target-resolution"},
|
|
RecordedAtUnixMillis: input.At.UnixMilli(),
|
|
}
|
|
expectation, err := epistemicsdomain.NewSystemExpectation(expectation)
|
|
if err != nil {
|
|
log.Printf("learning reminder expectation invalid: %v", err)
|
|
return
|
|
}
|
|
if err := r.epistemics.SaveSystemExpectation(ctx, expectation); err != nil {
|
|
log.Printf("learning reminder expectation unavailable: %v", err)
|
|
return
|
|
}
|
|
actual, confidence := outcomeForHumanResponse(input.Result.Response)
|
|
experience := epistemics.ExperienceRecord{
|
|
SourceKind: epistemics.ExperienceSourceContinuity,
|
|
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
|
|
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
|
ContextRefs: contextRefs(
|
|
"continuity:human-response:"+string(input.Result.Response.ID),
|
|
"continuity:prospective:"+string(input.Result.Response.TargetProspectiveID),
|
|
"continuity:attention:"+string(input.Result.Response.TargetAttentionID),
|
|
),
|
|
HelpMethod: epistemics.HelpMethodDeterministicFastPath,
|
|
HelpMethodRef: "continuity:human-response-target-resolver-v1",
|
|
SkillRef: epistemics.SkillRef(reminderResponseSkillID),
|
|
ExpectationID: expectation.ID,
|
|
ExpectedOutcome: expectation.ExpectedOutcome,
|
|
ActualOutcome: actual,
|
|
OutcomeRef: "continuity:human-response:" + string(input.Result.Response.ID),
|
|
OutcomeDetail: string(input.Result.Response.Operation) + " -> " + string(input.Result.Response.Outcome),
|
|
HumanResponseRef: epistemics.HumanResponseRef(input.Result.Response.ID),
|
|
VerificationEvidence: []epistemics.EvidenceRef{epistemics.EvidenceRef("human-response:" + string(input.Result.Response.Outcome))},
|
|
ConfidenceMillis: confidence,
|
|
RecordedAtUnixMillis: input.At.UnixMilli(),
|
|
}
|
|
r.saveExperienceAndReflection(ctx, experience)
|
|
r.updateReminderSkill(ctx, experience, skillOutcomeForExperience(experience), false)
|
|
}
|
|
|
|
func (r learningRecorder) recordCorrection(ctx context.Context, input correctionLearningInput) {
|
|
if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" {
|
|
return
|
|
}
|
|
experience := epistemics.ExperienceRecord{
|
|
SourceKind: epistemics.ExperienceSourceInteraction,
|
|
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
|
|
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
|
HelpMethod: epistemics.HelpMethodClarification,
|
|
ExpectedOutcome: epistemics.OutcomeSuccess,
|
|
ActualOutcome: epistemics.OutcomeUserCorrected,
|
|
OutcomeDetail: "human correction indicates prior interpretation or target resolution may be wrong",
|
|
CorrectionUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
|
VerificationEvidence: []epistemics.EvidenceRef{"interaction:human-correction"},
|
|
ConfidenceMillis: 1000,
|
|
RecordedAtUnixMillis: input.At.UnixMilli(),
|
|
}
|
|
r.saveExperienceAndReflection(ctx, experience)
|
|
}
|
|
|
|
func (r learningRecorder) recordFastPathDecline(ctx context.Context, input fastPathDeclineLearningInput) {
|
|
if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" {
|
|
return
|
|
}
|
|
experience, err := epistemicsdomain.FastPathDeclineExperience(
|
|
epistemics.OriginalIntentRef(input.Intent.ID),
|
|
epistemics.SourceUtteranceRef(input.Utterance.ID),
|
|
epistemics.SkillRef(reminderResponseSkillID),
|
|
string(outcomeForDecline(input.Reason)),
|
|
input.At.UnixMilli(),
|
|
)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if input.Result.Response.ID != "" {
|
|
experience.ContextRefs = contextRefs("continuity:response-candidate:" + string(input.Result.Response.Operation))
|
|
}
|
|
saved, ok := r.saveExperienceAndReflection(ctx, experience)
|
|
if ok {
|
|
r.updateReminderSkill(ctx, saved, capability.UsageSkipped, false)
|
|
}
|
|
}
|
|
|
|
func (r learningRecorder) recordCapabilityUse(ctx context.Context, input capabilityLearningInput) {
|
|
if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" {
|
|
return
|
|
}
|
|
actual := epistemics.OutcomeSuccess
|
|
switch input.Outcome {
|
|
case capability.UsageFailed:
|
|
actual = epistemics.OutcomeFailed
|
|
case capability.UsageSkipped:
|
|
actual = epistemics.OutcomeNotApplicable
|
|
case capability.UsageUnknown:
|
|
actual = epistemics.OutcomeUnknown
|
|
}
|
|
experience := epistemics.ExperienceRecord{
|
|
SourceKind: epistemics.ExperienceSourceCapability,
|
|
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
|
|
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
|
HelpMethod: epistemics.HelpMethodCapabilityExecution,
|
|
CapabilityImplementationRef: epistemics.CapabilityImplementationRef(input.ImplementationID),
|
|
CapabilityUsageObservationRef: epistemics.CapabilityUsageObservationRef(input.UsageObservation),
|
|
CapabilityGapRef: epistemics.CapabilityGapRef(input.GapID),
|
|
ExpectedOutcome: epistemics.OutcomeSuccess,
|
|
ActualOutcome: actual,
|
|
OutcomeRef: input.OutcomeRef,
|
|
OutcomeDetail: "capability outcome " + string(input.Outcome),
|
|
VerificationEvidence: []epistemics.EvidenceRef{"capability:usage-observation"},
|
|
ConfidenceMillis: 900,
|
|
RecordedAtUnixMillis: input.At.UnixMilli(),
|
|
}
|
|
r.saveExperienceAndReflection(ctx, experience)
|
|
}
|
|
|
|
func (r learningRecorder) recordDelegationOutcome(ctx context.Context, input delegationLearningInput) {
|
|
if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" || input.ImplementationID == "" {
|
|
return
|
|
}
|
|
actual := epistemics.OutcomeSuccess
|
|
confidence := int64(950)
|
|
if !input.Verified {
|
|
actual = epistemics.OutcomeFailed
|
|
confidence = 850
|
|
}
|
|
detailParts := []string{"delegation outcome recorded"}
|
|
if input.ExecutionWindows > 1 {
|
|
detailParts = append(detailParts, "execution-window-continuations="+strconv.Itoa(input.ExecutionWindows-1))
|
|
}
|
|
if input.HumanBlocked {
|
|
detailParts = append(detailParts, "human-blocker-occurred")
|
|
}
|
|
experience := epistemics.ExperienceRecord{
|
|
SourceKind: epistemics.ExperienceSourceCapability,
|
|
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
|
|
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
|
ContextRefs: contextRefs("work:delegation:"+input.DelegationID, "work:run:"+input.RunID),
|
|
HelpMethod: epistemics.HelpMethodCapabilityExecution,
|
|
HelpMethodRef: "work:agent-delegation-v1",
|
|
CapabilityImplementationRef: epistemics.CapabilityImplementationRef(input.ImplementationID),
|
|
ExpectedOutcome: epistemics.OutcomeSuccess,
|
|
ActualOutcome: actual,
|
|
OutcomeRef: input.OutcomeRef,
|
|
OutcomeDetail: strings.Join(detailParts, "; "),
|
|
VerificationEvidence: input.VerificationRefs,
|
|
ConfidenceMillis: confidence,
|
|
RecordedAtUnixMillis: input.At.UnixMilli(),
|
|
}
|
|
r.saveExperienceAndReflection(ctx, experience)
|
|
}
|
|
|
|
func (r learningRecorder) saveExperienceAndReflection(ctx context.Context, value epistemics.ExperienceRecord) (epistemics.ExperienceRecord, bool) {
|
|
value, err := epistemicsdomain.NewExperienceRecord(value)
|
|
if err != nil {
|
|
log.Printf("learning experience invalid: %v", err)
|
|
return epistemics.ExperienceRecord{}, false
|
|
}
|
|
if err := r.epistemics.SaveExperienceRecord(ctx, value); err != nil {
|
|
log.Printf("learning experience unavailable: %v", err)
|
|
return epistemics.ExperienceRecord{}, false
|
|
}
|
|
if candidate, ok, err := epistemicsdomain.ReflectionForExperience(value); err == nil && ok {
|
|
if err := r.epistemics.SaveReflectionCandidate(ctx, candidate); err != nil {
|
|
log.Printf("learning reflection unavailable: %v", err)
|
|
}
|
|
} else if err != nil {
|
|
log.Printf("learning reflection invalid: %v", err)
|
|
}
|
|
return value, true
|
|
}
|
|
|
|
func (r learningRecorder) updateReminderSkill(ctx context.Context, experience epistemics.ExperienceRecord, outcome capability.UsageOutcomeClass, corrected bool) {
|
|
if r.capability == nil || experience.ID == "" {
|
|
return
|
|
}
|
|
profile, err := r.capability.LoadSkillProfile(ctx, reminderResponseSkillID)
|
|
if err != nil {
|
|
log.Printf("learning skill profile unavailable: %v", err)
|
|
return
|
|
}
|
|
next, err := capabilitydomain.ObserveSkillOutcome(profile, capability.SkillOutcomeObservation{
|
|
ExperienceRef: capability.SkillEvidenceRef("epistemics:experience:" + string(experience.ID)),
|
|
Outcome: outcome,
|
|
UserCorrected: corrected,
|
|
ObservedAtUnixMillis: experience.RecordedAtUnixMillis,
|
|
})
|
|
if err != nil {
|
|
log.Printf("learning skill observation invalid: %v", err)
|
|
return
|
|
}
|
|
if err := r.capability.SaveSkillProfile(ctx, next); err != nil {
|
|
log.Printf("learning skill profile save unavailable: %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func outcomeForHumanResponse(response continuity.HumanResponse) (epistemics.OutcomeClass, int64) {
|
|
if response.ResolutionState != continuity.ResponseResolved {
|
|
switch response.ResolutionState {
|
|
case continuity.ResponseAmbiguous:
|
|
return epistemics.OutcomeConflict, 850
|
|
case continuity.ResponseNoTarget:
|
|
return epistemics.OutcomeOutOfDistribution, 850
|
|
default:
|
|
return epistemics.OutcomeUnexpected, 700
|
|
}
|
|
}
|
|
switch response.Outcome {
|
|
case continuity.ResponseOutcomeUserReportedComplete, continuity.ResponseOutcomeSnoozed, continuity.ResponseOutcomeCancelled, continuity.ResponseOutcomeDismissed:
|
|
return epistemics.OutcomeSuccess, 950
|
|
case continuity.ResponseOutcomeClarificationNeeded:
|
|
return epistemics.OutcomeConflict, 850
|
|
default:
|
|
return epistemics.OutcomeUnknown, 500
|
|
}
|
|
}
|
|
|
|
func skillOutcomeForExperience(experience epistemics.ExperienceRecord) capability.UsageOutcomeClass {
|
|
switch experience.ActualOutcome {
|
|
case epistemics.OutcomeSuccess, epistemics.OutcomePartialSuccess:
|
|
return capability.UsageSucceeded
|
|
case epistemics.OutcomeFailed, epistemics.OutcomeUnexpected, epistemics.OutcomeUserCorrected:
|
|
return capability.UsageFailed
|
|
case epistemics.OutcomeUnknown:
|
|
return capability.UsageUnknown
|
|
default:
|
|
return capability.UsageSkipped
|
|
}
|
|
}
|
|
|
|
func outcomeForDecline(reason capability.FastPathDeclineReason) epistemics.OutcomeClass {
|
|
switch reason {
|
|
case capability.FastPathLowConfidence:
|
|
return epistemics.OutcomeLowConfidence
|
|
case capability.FastPathOutOfDistribution:
|
|
return epistemics.OutcomeOutOfDistribution
|
|
case capability.FastPathConflict:
|
|
return epistemics.OutcomeConflict
|
|
case capability.FastPathStale:
|
|
return epistemics.OutcomeStale
|
|
default:
|
|
return epistemics.OutcomeNotApplicable
|
|
}
|
|
}
|
|
|
|
func contextRefs(values ...string) []epistemics.ContextRef {
|
|
refs := make([]epistemics.ContextRef, 0, len(values))
|
|
for _, value := range values {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" || strings.HasSuffix(value, ":") {
|
|
continue
|
|
}
|
|
refs = append(refs, epistemics.ContextRef(value))
|
|
}
|
|
return refs
|
|
}
|
|
|
|
func looksLikeUnsupportedContinuation(message string, result continuity.HumanResponseResult) bool {
|
|
lower := strings.ToLower(strings.TrimSpace(message))
|
|
if result.Handled {
|
|
return false
|
|
}
|
|
if result.Response.Operation == continuity.ResponseContinue || result.Response.Operation == continuity.ResponseReject {
|
|
return true
|
|
}
|
|
return strings.Contains(lower, "это") && (strings.Contains(lower, "напомин") || strings.Contains(lower, "перенеси") || strings.Contains(lower, "закрой") || strings.Contains(lower, "передвинь"))
|
|
}
|
|
|
|
func declineReasonForResponse(result continuity.HumanResponseResult) capability.FastPathDeclineReason {
|
|
switch result.Response.Operation {
|
|
case continuity.ResponseReject:
|
|
return capability.FastPathConflict
|
|
case continuity.ResponseContinue:
|
|
return capability.FastPathNotApplicable
|
|
default:
|
|
return capability.FastPathOutOfDistribution
|
|
}
|
|
}
|