This commit is contained in:
@@ -100,6 +100,22 @@ func (r *InteractionRepository) Utterances(ctx context.Context, id interaction.C
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// UtterancesWithProjectReferences returns canonical utterances together with
|
||||
// their already-persisted Interaction-owned project references. The caller
|
||||
// uses the pair only as a read projection; this method does not infer or write
|
||||
// scope membership.
|
||||
func (r *InteractionRepository) UtterancesWithProjectReferences(ctx context.Context, id interaction.ConversationID) ([]interaction.HumanUtterance, []interaction.ProjectReference, error) {
|
||||
utterances, err := r.Utterances(ctx, id)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
references, err := r.projectReferencesForUtterances(ctx, utterances)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return utterances, references, nil
|
||||
}
|
||||
|
||||
// RecentUtterancesForPrincipal provides durable conversational context when a
|
||||
// client starts a fresh conversation instead of carrying the previous ID.
|
||||
func (r *InteractionRepository) RecentUtterancesForPrincipal(ctx context.Context, principalID identity.PrincipalID, limit int) ([]interaction.HumanUtterance, error) {
|
||||
@@ -118,3 +134,52 @@ func (r *InteractionRepository) RecentUtterancesForPrincipal(ctx context.Context
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// RecentUtterancesForPrincipalWithProjectReferences keeps a new conversation
|
||||
// within the same durable Interaction read boundary as its scoped context.
|
||||
func (r *InteractionRepository) RecentUtterancesForPrincipalWithProjectReferences(ctx context.Context, principalID identity.PrincipalID, limit int) ([]interaction.HumanUtterance, []interaction.ProjectReference, error) {
|
||||
utterances, err := r.RecentUtterancesForPrincipal(ctx, principalID, limit)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
references, err := r.projectReferencesForUtterances(ctx, utterances)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return utterances, references, nil
|
||||
}
|
||||
|
||||
func (r *InteractionRepository) projectReferencesForUtterances(ctx context.Context, utterances []interaction.HumanUtterance) ([]interaction.ProjectReference, error) {
|
||||
if len(utterances) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
ids := make([]string, 0, len(utterances))
|
||||
seen := make(map[interaction.HumanUtteranceID]struct{}, len(utterances))
|
||||
for _, utterance := range utterances {
|
||||
if utterance.ID == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[utterance.ID]; exists {
|
||||
continue
|
||||
}
|
||||
seen[utterance.ID] = struct{}{}
|
||||
ids = append(ids, string(utterance.ID))
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT project_reference_id,utterance_id,project_id,project_display_name,role,provenance,confidence_millis,recorded_at_unix_millis FROM interaction.project_references WHERE utterance_id = ANY($1::text[]) ORDER BY recorded_at_unix_millis,project_reference_id`, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
refs := make([]interaction.ProjectReference, 0)
|
||||
for rows.Next() {
|
||||
var ref interaction.ProjectReference
|
||||
if err := rows.Scan(&ref.ID, &ref.UtteranceID, &ref.ProjectID, &ref.ProjectDisplayName, &ref.Role, &ref.Provenance, &ref.ConfidenceMillis, &ref.RecordedAtUnixMillis); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refs = append(refs, ref)
|
||||
}
|
||||
return refs, rows.Err()
|
||||
}
|
||||
|
||||
@@ -28,6 +28,35 @@ type cognitiveInterpretationClient struct {
|
||||
media *http.Client
|
||||
}
|
||||
|
||||
// compileInteractionContext is the Core-side handoff from the existing
|
||||
// Interaction retrieval projection to the existing Context Compiler. The
|
||||
// compiler receives only already-authorized, typed fragments and applies its
|
||||
// scope and byte-budget guards before the cognitive gateway sees them.
|
||||
func compileInteractionContext(projection interactiondomain.ContextProjection, focusProjectID string) (string, error) {
|
||||
if len(projection.Fragments) == 0 {
|
||||
return projection.Text, nil
|
||||
}
|
||||
candidates := make([]cognitive.ContextCandidate, 0, len(projection.Fragments))
|
||||
for _, fragment := range projection.Fragments {
|
||||
candidates = append(candidates, cognitive.ContextCandidate{
|
||||
Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("interaction:" + string(fragment.UtteranceID)), SourceKind: "interaction-gen2-retrieval", SourceID: "interaction:utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: fragment.Text},
|
||||
Relevance: fragment.Relevance,
|
||||
Lane: "interaction",
|
||||
ContextScopeRef: fragment.ScopeRef,
|
||||
FreshnessState: "CURRENT",
|
||||
})
|
||||
}
|
||||
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{Candidates: candidates, DataPolicy: capability.DataPolicyLocalOnly, MaxBytes: 2048, ContextScopeRef: focusProjectID, FreshnessRequirement: "conversation-current", RiskClass: "low"})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parts := make([]string, 0, len(bundle.Excerpts))
|
||||
for _, excerpt := range bundle.Excerpts {
|
||||
parts = append(parts, excerpt.Text)
|
||||
}
|
||||
return strings.Join(parts, "\n"), nil
|
||||
}
|
||||
|
||||
const (
|
||||
// Household interpretation and response review are intentionally compact
|
||||
// calls. Keeping their output bounded prevents a loaded local model from
|
||||
|
||||
@@ -70,6 +70,16 @@ func TestAnswerStableKnowledgeUsesLocalOnlyCognitiveRoute(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileInteractionContextAppliesTypedScopeBeforeGateway(t *testing.T) {
|
||||
context, err := compileInteractionContext(interactiondomain.ContextProjection{Fragments: []interactiondomain.ContextFragment{
|
||||
{UtteranceID: "a", Text: "контрольный код = ALPHA", ScopeRef: "company-a", Relevance: 2},
|
||||
{UtteranceID: "b", Text: "контрольный код = BETA", ScopeRef: "company-b", Relevance: 3},
|
||||
}}, "company-a")
|
||||
if err != nil || context != "контрольный код = ALPHA" {
|
||||
t.Fatalf("context=%q err=%v", context, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateImageUsesDedicatedCognitiveMediaRoute(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/v1/multimodal/generate-image" {
|
||||
|
||||
@@ -571,9 +571,10 @@ func main() {
|
||||
travelContinuation := false
|
||||
var previousTask *interaction.HumanUtterance
|
||||
var conversationHistory []interaction.HumanUtterance
|
||||
var contextProjectReferences []interaction.ProjectReference
|
||||
if request.ConversationID != "" {
|
||||
var historyErr error
|
||||
conversationHistory, historyErr = interactionRepository.Utterances(r.Context(), conversation.ID)
|
||||
conversationHistory, contextProjectReferences, historyErr = interactionRepository.UtterancesWithProjectReferences(r.Context(), conversation.ID)
|
||||
if historyErr != nil {
|
||||
log.Printf("interaction conversation history unavailable: %v", historyErr)
|
||||
}
|
||||
@@ -581,14 +582,24 @@ func main() {
|
||||
// Keep durable user context available even when the web client carries a
|
||||
// conversation ID; a new or rotated conversation must not erase learned
|
||||
// preferences.
|
||||
recentUserHistory, historyErr := interactionRepository.RecentUtterancesForPrincipal(r.Context(), session.PrincipalID, 64)
|
||||
recentUserHistory, recentProjectReferences, historyErr := interactionRepository.RecentUtterancesForPrincipalWithProjectReferences(r.Context(), session.PrincipalID, 64)
|
||||
if historyErr != nil {
|
||||
log.Printf("interaction principal history unavailable: %v", historyErr)
|
||||
} else {
|
||||
log.Printf("interaction principal history loaded: %d utterances", len(recentUserHistory))
|
||||
}
|
||||
conversationHistory = append(conversationHistory, recentUserHistory...)
|
||||
gen2Context := interactiondomain.BuildScopedGen2Context(conversationHistory, string(session.PrincipalID))
|
||||
contextProjectReferences = append(contextProjectReferences, recentProjectReferences...)
|
||||
focusProjectID := ""
|
||||
if previousFocus != nil {
|
||||
focusProjectID = string(previousFocus.ProjectID)
|
||||
}
|
||||
gen2Context := interactiondomain.BuildScopedGen2ContextForQuery(conversationHistory, contextProjectReferences, string(session.PrincipalID), focusProjectID, effectiveTaskMessage)
|
||||
if compiledContext, contextErr := compileInteractionContext(gen2Context, focusProjectID); contextErr != nil {
|
||||
log.Printf("interaction context compilation unavailable: %v", contextErr)
|
||||
} else {
|
||||
gen2Context.Text = compiledContext
|
||||
}
|
||||
if gen2Context.UsedPersonalMemoryFallback {
|
||||
telemetry.RecordCompatibility("personal-memory-fallback", "core-runtime", "read", "gen2-context-empty", true, true, false, "USED", utterance.CorrelationID, string(session.PrincipalID), now)
|
||||
}
|
||||
@@ -887,7 +898,7 @@ func main() {
|
||||
planningReply = retrievedMemoryReply
|
||||
needsCognition = false
|
||||
}
|
||||
if hasChatRoute(routePlan, chatRouteResearch) && semantic.Interpretation.Clarification != interaction.ClarificationRequired && shouldRunGeneralResearch(semantic.InformationRequestKind) {
|
||||
if hasChatRoute(routePlan, chatRouteResearch) && len(gen2Context.Fragments) == 0 && semantic.Interpretation.Clarification != interaction.ClarificationRequired && shouldRunGeneralResearch(semantic.InformationRequestKind) {
|
||||
researchAttempted = true
|
||||
if planID, idErr := ids.New(); idErr == nil {
|
||||
parts := interactiondomain.IndependentRequestParts(effectiveTaskMessage)
|
||||
@@ -954,7 +965,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
canGenerateTextReply := shouldGenerateNonMutatingTextDirective(semantic, routePlan, helpPath, creativeRequest) && planningReply == deterministicReply
|
||||
if (planningReply == "" && needsCognition && semantic.Interpretation.SpeechAct == interaction.SpeechActQuestion || canGenerateTextReply) && !hasChatRoute(routePlan, chatRouteResearch) && !creativeRequest {
|
||||
if (planningReply == "" && needsCognition && semantic.Interpretation.SpeechAct == interaction.SpeechActQuestion || canGenerateTextReply) && (!hasChatRoute(routePlan, chatRouteResearch) || len(gen2Context.Fragments) > 0) && !creativeRequest {
|
||||
answerCtx, cancelAnswer := context.WithTimeout(r.Context(), 12*time.Second)
|
||||
answer, answerErr := cognitiveClient.AnswerStableKnowledge(answerCtx, session, effectiveTaskMessage, gen2Context.Text, cognitive.TaskMeaning{
|
||||
SpeechAct: string(semantic.Interpretation.SpeechAct), CapabilityHints: toCapabilityKinds(semantic.CapabilityHints), RequestedOutcome: semantic.Interpretation.RequestedOutcome, InformationRequestKind: semantic.InformationRequestKind, ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"task_file":"dev/tasks/GEN2-COGNITIVE-002-restore-production-cognitive-route.json","task_id":"GEN2-COGNITIVE-002","task_revision":2}
|
||||
{"task_file":"dev/tasks/GEN2-COGNITIVE-002-restore-production-cognitive-route.json","task_id":"GEN2-COGNITIVE-002","task_revision":3}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"task_id": "GEN2-COGNITIVE-002",
|
||||
"task_revision": 2,
|
||||
"task_revision": 3,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
@@ -13,6 +13,7 @@
|
||||
"cmd/core-runtime/**",
|
||||
"modules/interaction/domain/gen2_context.go",
|
||||
"modules/interaction/domain/gen2_context_test.go",
|
||||
"adapters/postgres/interaction_repository.go",
|
||||
"contracts/capability/**",
|
||||
"contracts/cognitive/**",
|
||||
"modules/capability/**",
|
||||
@@ -36,9 +37,9 @@
|
||||
"may_change": ["typed capability/provider selection and invocation wiring only within declared paths", "the existing Interaction Gen2 retrieval projection for bounded, scoped human assertions; no new Memory owner or reader"]
|
||||
},
|
||||
"semantic_owner": "Capability remains the sole owner of provider/model/binding lifecycle; cognitive-worker invokes providers through adapters; Core consumes the governed gateway; Supervisor alone controls traffic promotion and rollback.",
|
||||
"canonical_state": ["existing Capability provider/model/binding/evaluation evidence", "canonical Interaction human utterance history read through the existing Gen2 retrieval projection", "no new canonical writer, owner, schema, role, or migration"],
|
||||
"canonical_state": ["existing Capability provider/model/binding/evaluation evidence", "canonical Interaction human utterance history and existing typed ProjectReference read through the existing Gen2 retrieval projection", "no new canonical writer, owner, schema, role, or migration"],
|
||||
"state_transitions": ["trace stable and candidate loaded binding heads -> correct stale/divergent lifecycle or invocation defect -> governed repair/qualification -> scoped Interaction projection selects bounded current human assertions -> isolated candidate validation -> Supervisor canary -> promote or rollback"],
|
||||
"side_effects_and_privileges": ["production candidate deployment and Supervisor canary/promotion are explicitly authorized", "adapters/models/** may change only after trace proves a model adapter, provider endpoint, injection, timeout, rate-limit, context budget, or invocation defect there", "cmd/core-runtime/** may change only after trace proves Core-to-worker request construction, route selection, or response handling is causal", "modules/interaction/domain/gen2_context.go may change only to extend the existing bounded Gen2 read projection over canonical Interaction utterances with Principal hard filtering, current applicability and relevance selection; it must not write state, become a Memory owner, or promote PersonalMemory V1 to primary", "no direct SQL semantic mutation", "no credential, cookie, token, prompt, or user content read/emitted", "Lifeboat may be observed as fallback but cannot mask Local MEDIUM primary failure"],
|
||||
"side_effects_and_privileges": ["production candidate deployment and Supervisor canary/promotion are explicitly authorized", "adapters/models/** may change only after trace proves a model adapter, provider endpoint, injection, timeout, rate-limit, context budget, or invocation defect there", "cmd/core-runtime/** may change only after trace proves Core-to-worker request construction, route selection, or response handling is causal", "modules/interaction/domain/gen2_context.go may change only to extend the existing bounded Gen2 read projection over canonical Interaction utterances with Principal hard filtering, current applicability and relevance selection; it must not write state, become a Memory owner, or promote PersonalMemory V1 to primary", "adapters/postgres/interaction_repository.go may only read the already-persisted typed interaction.ProjectReference records for the selected canonical Interaction utterances; it may not create schema, write state, or bypass Interaction ownership", "no direct SQL semantic mutation", "no credential, cookie, token, prompt, or user content read/emitted", "Lifeboat may be observed as fallback but cannot mask Local MEDIUM primary failure"],
|
||||
"persistence_and_migrations": ["no schema/migration change is authorized", "adapters/postgres/capability_repository.go may change only after trace proves durable Capability head/state loading diverges from the correct persisted state; reuse its existing repository boundary"],
|
||||
"failure_retry_restart": ["discovery health, invocation health, and availability remain distinct", "CPU/GPU gaps must not suppress a valid Local MEDIUM", "restart reloads a durable active/validated binding head", "failed candidate rolls traffic to Supervisor LKG without stable recreation"],
|
||||
"assumptions": ["isolated candidate slot B and Supervisor controls from GEN2-DEPLOY-003 are available", "an existing authenticated production session may be used only through ordinary runtime interaction", "GEN2-COMPAT-OBS-001 remains paused and V1 paths remain unchanged"],
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
package domain
|
||||
|
||||
import "git.cin.su/m/Assistant/contracts/interaction"
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
)
|
||||
|
||||
const (
|
||||
gen2ContextMaxCandidates = 4
|
||||
gen2ContextMaxBytes = 2048
|
||||
)
|
||||
|
||||
type ContextFragment struct {
|
||||
UtteranceID interaction.HumanUtteranceID
|
||||
Text string
|
||||
ScopeRef string
|
||||
Relevance int
|
||||
Historical bool
|
||||
}
|
||||
|
||||
type ContextProjection struct {
|
||||
Text string
|
||||
Source string
|
||||
UsedPersonalMemoryFallback bool
|
||||
Fragments []ContextFragment
|
||||
Historical []ContextFragment
|
||||
}
|
||||
|
||||
// BuildGen2Context is a read-side projection over canonical utterance history.
|
||||
@@ -19,6 +39,14 @@ func BuildGen2Context(history []interaction.HumanUtterance) ContextProjection {
|
||||
// filter prevents accidental cross-principal projection when a mixed source
|
||||
// is passed during degraded operation.
|
||||
func BuildScopedGen2Context(history []interaction.HumanUtterance, principalID string) ContextProjection {
|
||||
return BuildScopedGen2ContextForQuery(history, nil, principalID, "", "")
|
||||
}
|
||||
|
||||
// BuildScopedGen2ContextForQuery projects bounded, explicit human assertions
|
||||
// from the canonical Interaction history. Principal and typed project refs are
|
||||
// hard filters before relevance ranking. Older values remain lineage in
|
||||
// Historical and are never represented as the current value.
|
||||
func BuildScopedGen2ContextForQuery(history []interaction.HumanUtterance, references []interaction.ProjectReference, principalID, focusProjectID, query string) ContextProjection {
|
||||
if principalID != "" {
|
||||
scoped := make([]interaction.HumanUtterance, 0, len(history))
|
||||
for _, item := range history {
|
||||
@@ -28,9 +56,182 @@ func BuildScopedGen2Context(history []interaction.HumanUtterance, principalID st
|
||||
}
|
||||
history = scoped
|
||||
}
|
||||
projectRefs := make(map[interaction.HumanUtteranceID][]interaction.ProjectReference, len(references))
|
||||
for _, ref := range references {
|
||||
projectRefs[ref.UtteranceID] = append(projectRefs[ref.UtteranceID], ref)
|
||||
}
|
||||
assertions := explicitAssertionCandidates(history, projectRefs, focusProjectID, query)
|
||||
if len(assertions) > 0 {
|
||||
current, historical := selectCurrentAssertions(assertions)
|
||||
return contextProjectionFromAssertions(current, historical)
|
||||
}
|
||||
text := CompilePersonalMemory(history).ContextText()
|
||||
if text != "" {
|
||||
return ContextProjection{Text: text, Source: "retrieval:interaction-utterances"}
|
||||
}
|
||||
return ContextProjection{Text: text, Source: "personal-memory-v1-fallback", UsedPersonalMemoryFallback: true}
|
||||
}
|
||||
|
||||
type assertionCandidate struct {
|
||||
fragment ContextFragment
|
||||
subject string
|
||||
observed int64
|
||||
}
|
||||
|
||||
func explicitAssertionCandidates(history []interaction.HumanUtterance, projectRefs map[interaction.HumanUtteranceID][]interaction.ProjectReference, focusProjectID, query string) []assertionCandidate {
|
||||
queryTokens := significantContextTokens(query)
|
||||
result := make([]assertionCandidate, 0)
|
||||
for _, utterance := range history {
|
||||
subject, value, ok := explicitAssertion(utterance.Content)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
refs := projectRefs[utterance.ID]
|
||||
scopeRef := ""
|
||||
if focusProjectID != "" {
|
||||
matched := false
|
||||
for _, ref := range refs {
|
||||
if string(ref.ProjectID) == focusProjectID {
|
||||
matched = true
|
||||
scopeRef = focusProjectID
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
} else if len(refs) == 1 {
|
||||
scopeRef = string(refs[0].ProjectID)
|
||||
}
|
||||
relevance := contextRelevance(subject, queryTokens)
|
||||
if len(queryTokens) > 0 && relevance == 0 {
|
||||
continue
|
||||
}
|
||||
result = append(result, assertionCandidate{
|
||||
fragment: ContextFragment{UtteranceID: utterance.ID, Text: subject + " = " + value, ScopeRef: scopeRef, Relevance: relevance},
|
||||
subject: normalizedContextSubject(subject),
|
||||
observed: utterance.ReceivedAtUnixMillis,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func selectCurrentAssertions(candidates []assertionCandidate) ([]ContextFragment, []ContextFragment) {
|
||||
latest := make(map[string]assertionCandidate)
|
||||
for _, candidate := range candidates {
|
||||
current, exists := latest[candidate.subject]
|
||||
if !exists || candidate.observed > current.observed || candidate.observed == current.observed && candidate.fragment.UtteranceID > current.fragment.UtteranceID {
|
||||
latest[candidate.subject] = candidate
|
||||
}
|
||||
}
|
||||
current := make([]ContextFragment, 0, len(latest))
|
||||
historical := make([]ContextFragment, 0, len(candidates)-len(latest))
|
||||
for _, candidate := range candidates {
|
||||
selected := latest[candidate.subject]
|
||||
if candidate.fragment.UtteranceID == selected.fragment.UtteranceID {
|
||||
current = append(current, candidate.fragment)
|
||||
continue
|
||||
}
|
||||
candidate.fragment.Historical = true
|
||||
historical = append(historical, candidate.fragment)
|
||||
}
|
||||
sort.SliceStable(current, func(i, j int) bool {
|
||||
if current[i].Relevance != current[j].Relevance {
|
||||
return current[i].Relevance > current[j].Relevance
|
||||
}
|
||||
return current[i].UtteranceID > current[j].UtteranceID
|
||||
})
|
||||
return current, historical
|
||||
}
|
||||
|
||||
func contextProjectionFromAssertions(current, historical []ContextFragment) ContextProjection {
|
||||
selected := make([]ContextFragment, 0, len(current))
|
||||
text := make([]string, 0, len(current))
|
||||
usedBytes := 0
|
||||
for _, candidate := range current {
|
||||
if len(selected) == gen2ContextMaxCandidates {
|
||||
break
|
||||
}
|
||||
bytes := len([]byte(candidate.Text))
|
||||
if bytes == 0 || usedBytes+bytes > gen2ContextMaxBytes {
|
||||
continue
|
||||
}
|
||||
usedBytes += bytes
|
||||
selected = append(selected, candidate)
|
||||
text = append(text, candidate.Text)
|
||||
}
|
||||
if len(selected) == 0 {
|
||||
return ContextProjection{Source: "retrieval:interaction-assertions", Historical: historical}
|
||||
}
|
||||
return ContextProjection{Text: strings.Join(text, "\n"), Source: "retrieval:interaction-assertions", Fragments: selected, Historical: historical}
|
||||
}
|
||||
|
||||
func explicitAssertion(message string) (string, string, bool) {
|
||||
text := strings.TrimSpace(StripAssistantVocative(message))
|
||||
if text == "" || strings.Contains(text, "?") {
|
||||
return "", "", false
|
||||
}
|
||||
lower := strings.ToLower(text)
|
||||
if index := strings.Index(lower, "запомни:"); index >= 0 {
|
||||
text = strings.TrimSpace(text[index+len("запомни:"):])
|
||||
lower = strings.ToLower(text)
|
||||
}
|
||||
for _, prefix := range []string{"запомни:", "запомни", "помни:"} {
|
||||
if strings.HasPrefix(lower, prefix) {
|
||||
text = strings.TrimSpace(text[len(prefix):])
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, separator := range []string{"=", "—"} {
|
||||
subject, value, found := strings.Cut(text, separator)
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
subject, value = strings.TrimSpace(subject), strings.TrimSpace(value)
|
||||
if subject != "" && value != "" && len([]rune(subject)) <= 160 && len([]rune(value)) <= 512 {
|
||||
return subject, value, true
|
||||
}
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
func normalizedContextSubject(subject string) string {
|
||||
return strings.Join(significantContextTokens(subject), " ")
|
||||
}
|
||||
|
||||
func significantContextTokens(value string) []string {
|
||||
ignored := map[string]struct{}{"какой": {}, "какая": {}, "какое": {}, "какие": {}, "текущий": {}, "текущая": {}, "текущее": {}, "я": {}, "ты": {}, "назвал": {}, "назвала": {}, "сообщил": {}, "сообщила": {}}
|
||||
result := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
for _, token := range semanticTokens(value) {
|
||||
if len([]rune(token)) < 3 {
|
||||
continue
|
||||
}
|
||||
if _, skip := ignored[token]; skip {
|
||||
continue
|
||||
}
|
||||
if _, duplicate := seen[token]; duplicate {
|
||||
continue
|
||||
}
|
||||
seen[token] = struct{}{}
|
||||
result = append(result, token)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func contextRelevance(subject string, queryTokens []string) int {
|
||||
if len(queryTokens) == 0 {
|
||||
return 1
|
||||
}
|
||||
subjectTokens := significantContextTokens(subject)
|
||||
matched := 0
|
||||
for _, query := range queryTokens {
|
||||
for _, token := range subjectTokens {
|
||||
if query == token {
|
||||
matched++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
@@ -32,7 +32,61 @@ func TestGen2ContextProjectsExplicitRememberedAssertion(t *testing.T) {
|
||||
got := domain.BuildScopedGen2Context([]interaction.HumanUtterance{
|
||||
{ID: "assertion", PrincipalID: "principal-1", Content: "Для этого разговора запомни: мой тестовый цвет — синий.", ReceivedAtUnixMillis: 1},
|
||||
}, "principal-1")
|
||||
if got.Source != "retrieval:interaction-utterances" || got.UsedPersonalMemoryFallback || !strings.Contains(got.Text, "тестовый цвет — синий") {
|
||||
if got.Source != "retrieval:interaction-assertions" || got.UsedPersonalMemoryFallback || !strings.Contains(got.Text, "тестовый цвет = синий") {
|
||||
t.Fatalf("remembered assertion was not projected: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGen2ContextSelectsArbitraryRelevantCurrentAssertion(t *testing.T) {
|
||||
history := []interaction.HumanUtterance{
|
||||
{ID: "assertion", PrincipalID: "principal-1", Content: "контрольный код = ORBIT-731", ReceivedAtUnixMillis: 1},
|
||||
{ID: "turn-1", PrincipalID: "principal-1", Content: "Спасибо", ReceivedAtUnixMillis: 2},
|
||||
{ID: "turn-2", PrincipalID: "principal-1", Content: "Продолжим", ReceivedAtUnixMillis: 3},
|
||||
}
|
||||
got := domain.BuildScopedGen2ContextForQuery(history, nil, "principal-1", "", "Какой контрольный код я назвал?")
|
||||
if got.Source != "retrieval:interaction-assertions" || !strings.Contains(got.Text, "ORBIT-731") || len(got.Fragments) != 1 {
|
||||
t.Fatalf("arbitrary assertion was not selected: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGen2ContextKeepsHistoricalAssertionOutOfCurrentProjection(t *testing.T) {
|
||||
history := []interaction.HumanUtterance{
|
||||
{ID: "old", PrincipalID: "principal-1", Content: "контрольный код = OLD-111", ReceivedAtUnixMillis: 1},
|
||||
{ID: "new", PrincipalID: "principal-1", Content: "контрольный код = NEW-222", ReceivedAtUnixMillis: 2},
|
||||
}
|
||||
got := domain.BuildScopedGen2ContextForQuery(history, nil, "principal-1", "", "Какой контрольный код?")
|
||||
if strings.Contains(got.Text, "OLD-111") || !strings.Contains(got.Text, "NEW-222") || len(got.Historical) != 1 || !strings.Contains(got.Historical[0].Text, "OLD-111") {
|
||||
t.Fatalf("current/historical assertion separation failed: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGen2ContextHardFiltersProjectReferencesBeforeRanking(t *testing.T) {
|
||||
history := []interaction.HumanUtterance{
|
||||
{ID: "a", PrincipalID: "principal-1", Content: "контрольный код = ALPHA", ReceivedAtUnixMillis: 1},
|
||||
{ID: "b", PrincipalID: "principal-1", Content: "контрольный код = BETA", ReceivedAtUnixMillis: 2},
|
||||
}
|
||||
references := []interaction.ProjectReference{
|
||||
{ID: "ref-a", UtteranceID: "a", ProjectID: "company-a"},
|
||||
{ID: "ref-b", UtteranceID: "b", ProjectID: "company-b"},
|
||||
}
|
||||
for projectID, expected := range map[string]struct{ want, absent string }{
|
||||
"company-a": {want: "ALPHA", absent: "BETA"},
|
||||
"company-b": {want: "BETA", absent: "ALPHA"},
|
||||
} {
|
||||
got := domain.BuildScopedGen2ContextForQuery(history, references, "principal-1", projectID, "Какой контрольный код?")
|
||||
if !strings.Contains(got.Text, expected.want) || strings.Contains(got.Text, expected.absent) || len(got.Fragments) != 1 || got.Fragments[0].ScopeRef != projectID {
|
||||
t.Fatalf("scope %s leaked context: %#v", projectID, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGen2ContextExcludesIrrelevantAssertionsFromBoundedProjection(t *testing.T) {
|
||||
history := []interaction.HumanUtterance{
|
||||
{ID: "code", PrincipalID: "principal-1", Content: "контрольный код = ORBIT-731", ReceivedAtUnixMillis: 1},
|
||||
{ID: "city", PrincipalID: "principal-1", Content: "город встречи = Казань", ReceivedAtUnixMillis: 2},
|
||||
}
|
||||
got := domain.BuildScopedGen2ContextForQuery(history, nil, "principal-1", "", "Какой контрольный код?")
|
||||
if strings.Contains(got.Text, "Казань") || len(got.Fragments) != 1 {
|
||||
t.Fatalf("irrelevant assertion entered bounded projection: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user