Files
Assistant/cmd/core-runtime/learning.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
}
}