feat: add scoped adaptive experience foundation
This commit is contained in:
@@ -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