fix: compile scoped interaction assertions
CI / test (push) Failing after 34s

This commit is contained in:
m
2026-08-30 19:12:52 +03:00
parent ce7000e78e
commit 00af176a20
8 changed files with 382 additions and 11 deletions
@@ -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" {
+16 -5
View File
@@ -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
View File
@@ -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"],
+202 -1
View File
@@ -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)
}
}