feat: add scoped adaptive experience foundation
This commit is contained in:
@@ -101,10 +101,10 @@ func (r *EpistemicsRepository) RecentExperienceRecords(ctx context.Context, limi
|
||||
return nil, fmt.Errorf("invalid experience limit")
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT
|
||||
experience_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
|
||||
experience_id,principal_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
|
||||
capability_implementation_ref,capability_usage_observation_ref,capability_gap_ref,skill_ref,expectation_id,
|
||||
expected_outcome,actual_outcome,outcome_ref,outcome_detail,human_response_ref,correction_utterance_id,
|
||||
verification_evidence,confidence_millis,recorded_at_unix_millis
|
||||
verification_evidence,confidence_millis,recorded_at_unix_millis,context_scope_ref,problem_statement,applicability_conditions,why_succeeded_or_failed
|
||||
FROM epistemics.experience_records ORDER BY recorded_at_unix_millis DESC, experience_id DESC LIMIT $1`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -113,10 +113,10 @@ func (r *EpistemicsRepository) RecentExperienceRecords(ctx context.Context, limi
|
||||
var result []epistemics.ExperienceRecord
|
||||
for rows.Next() {
|
||||
var value epistemics.ExperienceRecord
|
||||
if err := rows.Scan(&value.ID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
|
||||
if err := rows.Scan(&value.ID, &value.PrincipalID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
|
||||
&value.CapabilityImplementationRef, &value.CapabilityUsageObservationRef, &value.CapabilityGapRef, &value.SkillRef, &value.ExpectationID,
|
||||
&value.ExpectedOutcome, &value.ActualOutcome, &value.OutcomeRef, &value.OutcomeDetail, &value.HumanResponseRef, &value.CorrectionUtteranceID,
|
||||
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis); err != nil {
|
||||
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis, &value.ContextScopeRef, &value.ProblemStatement, &value.ApplicabilityConditions, &value.WhySucceededOrFailed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, value)
|
||||
@@ -205,15 +205,15 @@ func (r *EpistemicsRepository) WaitForReflectionCandidate(ctx context.Context, i
|
||||
|
||||
func (r *EpistemicsRepository) SaveExperienceRecord(ctx context.Context, value epistemics.ExperienceRecord) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.experience_records (
|
||||
experience_id, source_kind, original_intent_id, source_utterance_id, context_refs, help_method, help_method_ref,
|
||||
experience_id, principal_id, source_kind, original_intent_id, source_utterance_id, context_refs, help_method, help_method_ref,
|
||||
capability_implementation_ref, capability_usage_observation_ref, capability_gap_ref, skill_ref, expectation_id,
|
||||
expected_outcome, actual_outcome, outcome_ref, outcome_detail, human_response_ref, correction_utterance_id,
|
||||
verification_evidence, confidence_millis, recorded_at_unix_millis)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21) ON CONFLICT DO NOTHING`,
|
||||
value.ID, value.SourceKind, value.OriginalIntentID, value.SourceUtteranceID, value.ContextRefs, value.HelpMethod, value.HelpMethodRef,
|
||||
verification_evidence, confidence_millis, recorded_at_unix_millis, context_scope_ref, problem_statement, applicability_conditions, why_succeeded_or_failed)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26) ON CONFLICT DO NOTHING`,
|
||||
value.ID, value.PrincipalID, value.SourceKind, value.OriginalIntentID, value.SourceUtteranceID, value.ContextRefs, value.HelpMethod, value.HelpMethodRef,
|
||||
value.CapabilityImplementationRef, value.CapabilityUsageObservationRef, value.CapabilityGapRef, value.SkillRef, value.ExpectationID,
|
||||
value.ExpectedOutcome, value.ActualOutcome, value.OutcomeRef, value.OutcomeDetail, value.HumanResponseRef, value.CorrectionUtteranceID,
|
||||
value.VerificationEvidence, value.ConfidenceMillis, value.RecordedAtUnixMillis)
|
||||
value.VerificationEvidence, value.ConfidenceMillis, value.RecordedAtUnixMillis, value.ContextScopeRef, value.ProblemStatement, value.ApplicabilityConditions, value.WhySucceededOrFailed)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ func (r learningRecorder) recordReminderResponse(ctx context.Context, input remi
|
||||
actual, confidence := outcomeForHumanResponse(input.Result.Response)
|
||||
experience := epistemics.ExperienceRecord{
|
||||
SourceKind: epistemics.ExperienceSourceContinuity,
|
||||
PrincipalID: string(input.Utterance.PrincipalID),
|
||||
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
|
||||
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
||||
ContextRefs: contextRefs(
|
||||
@@ -143,6 +144,7 @@ func (r learningRecorder) recordCorrection(ctx context.Context, input correction
|
||||
}
|
||||
experience := epistemics.ExperienceRecord{
|
||||
SourceKind: epistemics.ExperienceSourceInteraction,
|
||||
PrincipalID: string(input.Utterance.PrincipalID),
|
||||
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
|
||||
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
||||
HelpMethod: epistemics.HelpMethodClarification,
|
||||
@@ -175,6 +177,7 @@ func (r learningRecorder) recordFastPathDecline(ctx context.Context, input fastP
|
||||
if input.Result.Response.ID != "" {
|
||||
experience.ContextRefs = contextRefs("continuity:response-candidate:" + string(input.Result.Response.Operation))
|
||||
}
|
||||
experience.PrincipalID = string(input.Utterance.PrincipalID)
|
||||
saved, ok := r.saveExperienceAndReflection(ctx, experience)
|
||||
if ok {
|
||||
r.updateReminderSkill(ctx, saved, capability.UsageSkipped, false)
|
||||
@@ -196,6 +199,7 @@ func (r learningRecorder) recordCapabilityUse(ctx context.Context, input capabil
|
||||
}
|
||||
experience := epistemics.ExperienceRecord{
|
||||
SourceKind: epistemics.ExperienceSourceCapability,
|
||||
PrincipalID: string(input.Utterance.PrincipalID),
|
||||
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
|
||||
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
||||
HelpMethod: epistemics.HelpMethodCapabilityExecution,
|
||||
@@ -232,6 +236,7 @@ func (r learningRecorder) recordDelegationOutcome(ctx context.Context, input del
|
||||
}
|
||||
experience := epistemics.ExperienceRecord{
|
||||
SourceKind: epistemics.ExperienceSourceCapability,
|
||||
PrincipalID: string(input.Utterance.PrincipalID),
|
||||
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
|
||||
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
||||
ContextRefs: contextRefs("work:delegation:"+input.DelegationID, "work:run:"+input.RunID),
|
||||
|
||||
@@ -102,6 +102,7 @@ type SystemExpectation struct {
|
||||
// state or grants authority.
|
||||
type ExperienceRecord struct {
|
||||
ID ExperienceRecordID
|
||||
PrincipalID string
|
||||
SourceKind ExperienceSourceKind
|
||||
OriginalIntentID OriginalIntentRef
|
||||
SourceUtteranceID SourceUtteranceRef
|
||||
@@ -133,6 +134,15 @@ type ExperienceRecord struct {
|
||||
WhySucceededOrFailed string
|
||||
}
|
||||
|
||||
// ConsolidationCandidate is a rebuildable Epistemics projection. It preserves
|
||||
// source ExperienceRecord references and cannot activate a Skill by itself.
|
||||
type ConsolidationCandidate struct {
|
||||
EpisodeRef string
|
||||
ExperienceRefs []ExperienceRecordID
|
||||
PropositionText string
|
||||
SkillCandidate bool
|
||||
}
|
||||
|
||||
// ReflectionCandidate records that broader reflection may be warranted.
|
||||
// Cognitive reflection, if used, must go through Cognitive Gateway/Router and
|
||||
// produce candidates only; it cannot directly rewrite canonical state.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE epistemics_role;
|
||||
|
||||
ALTER TABLE epistemics.experience_records
|
||||
ADD COLUMN IF NOT EXISTS principal_id TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS context_scope_ref TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS problem_statement TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS applicability_conditions TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
ADD COLUMN IF NOT EXISTS why_succeeded_or_failed TEXT NOT NULL DEFAULT '';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS experience_records_principal_scope_recorded_idx
|
||||
ON epistemics.experience_records (principal_id, context_scope_ref, recorded_at_unix_millis DESC);
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,89 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
)
|
||||
|
||||
type ExperienceRetrieval struct {
|
||||
Positive []epistemics.ExperienceRecord
|
||||
Negative []epistemics.ExperienceRecord
|
||||
Contradictions []epistemics.ExperienceRecord
|
||||
}
|
||||
|
||||
// RetrieveExperience filters principal and scope before text relevance. It is
|
||||
// a derived view over append-only Epistemics records, not a Memory owner.
|
||||
func RetrieveExperience(records []epistemics.ExperienceRecord, principalID, scopeRef, query string, limit int) ExperienceRetrieval {
|
||||
if limit < 1 {
|
||||
limit = 1
|
||||
}
|
||||
queryTokens := retrievalTokens(query)
|
||||
eligible := make([]epistemics.ExperienceRecord, 0, len(records))
|
||||
for _, record := range records {
|
||||
if principalID != "" && record.PrincipalID != principalID {
|
||||
continue
|
||||
}
|
||||
if scopeRef != "" && record.ContextScopeRef != scopeRef {
|
||||
continue
|
||||
}
|
||||
if len(queryTokens) > 0 && overlap(queryTokens, retrievalTokens(record.ProblemStatement+" "+record.OutcomeDetail+" "+record.WhySucceededOrFailed)) == 0 {
|
||||
continue
|
||||
}
|
||||
eligible = append(eligible, record)
|
||||
}
|
||||
sort.SliceStable(eligible, func(i, j int) bool { return eligible[i].RecordedAtUnixMillis > eligible[j].RecordedAtUnixMillis })
|
||||
result := ExperienceRetrieval{}
|
||||
for _, record := range eligible {
|
||||
if len(result.Positive)+len(result.Negative) >= limit {
|
||||
break
|
||||
}
|
||||
switch record.ActualOutcome {
|
||||
case epistemics.OutcomeSuccess, epistemics.OutcomePartialSuccess:
|
||||
result.Positive = append(result.Positive, record)
|
||||
default:
|
||||
result.Negative = append(result.Negative, record)
|
||||
if record.ActualOutcome == epistemics.OutcomeConflict || record.ActualOutcome == epistemics.OutcomeUserCorrected || record.ActualOutcome == epistemics.OutcomeStale {
|
||||
result.Contradictions = append(result.Contradictions, record)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func BuildConsolidationCandidates(records []epistemics.ExperienceRecord) []epistemics.ConsolidationCandidate {
|
||||
byProblem := map[string][]epistemics.ExperienceRecord{}
|
||||
for _, record := range records {
|
||||
key := strings.TrimSpace(strings.ToLower(record.ProblemStatement))
|
||||
if key != "" {
|
||||
byProblem[key] = append(byProblem[key], record)
|
||||
}
|
||||
}
|
||||
result := make([]epistemics.ConsolidationCandidate, 0, len(byProblem))
|
||||
for problem, grouped := range byProblem {
|
||||
refs := make([]epistemics.ExperienceRecordID, 0, len(grouped))
|
||||
for _, record := range grouped {
|
||||
refs = append(refs, record.ID)
|
||||
}
|
||||
result = append(result, epistemics.ConsolidationCandidate{EpisodeRef: "derived:experience:" + problem, ExperienceRefs: refs, PropositionText: problem, SkillCandidate: len(grouped) > 1})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func retrievalTokens(value string) []string {
|
||||
return strings.FieldsFunc(strings.ToLower(value), func(r rune) bool { return r < '0' || r > 'z' && (r < 'а' || r > 'я') })
|
||||
}
|
||||
func overlap(left, right []string) int {
|
||||
seen := map[string]bool{}
|
||||
for _, v := range left {
|
||||
seen[v] = true
|
||||
}
|
||||
n := 0
|
||||
for _, v := range right {
|
||||
if seen[v] {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRetrieveExperienceFiltersScopeBeforeRelevanceAndKeepsFailures(t *testing.T) {
|
||||
records := []epistemics.ExperienceRecord{
|
||||
{ID: "success-a", PrincipalID: "p", ContextScopeRef: "company-a", ProblemStatement: "sync catalog", ActualOutcome: epistemics.OutcomeSuccess, RecordedAtUnixMillis: 2},
|
||||
{ID: "failed-a", PrincipalID: "p", ContextScopeRef: "company-a", ProblemStatement: "sync catalog", ActualOutcome: epistemics.OutcomeFailed, OutcomeDetail: "old connector failed", RecordedAtUnixMillis: 1},
|
||||
{ID: "success-b", PrincipalID: "p", ContextScopeRef: "company-b", ProblemStatement: "sync catalog", ActualOutcome: epistemics.OutcomeSuccess, RecordedAtUnixMillis: 3},
|
||||
}
|
||||
got := RetrieveExperience(records, "p", "company-a", "sync catalog", 4)
|
||||
if len(got.Positive) != 1 || got.Positive[0].ID != "success-a" || len(got.Negative) != 1 || got.Negative[0].ID != "failed-a" {
|
||||
t.Fatalf("retrieval=%#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsolidationPreservesEvidenceWithoutSkillActivation(t *testing.T) {
|
||||
got := BuildConsolidationCandidates([]epistemics.ExperienceRecord{{ID: "a", ProblemStatement: "sync catalog"}, {ID: "b", ProblemStatement: "sync catalog"}})
|
||||
if len(got) != 1 || len(got[0].ExperienceRefs) != 2 || !got[0].SkillCandidate {
|
||||
t.Fatalf("candidates=%#v", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user