Compare commits

...
Author SHA1 Message Date
m 29b20bc5b0 DEV-000066 record production acceptance status
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 15s
2026-08-17 17:43:43 +03:00
m 071adb5ecb Promote DEV-000066 response candidate ordering
CI / test (push) Successful in 16s
DEV-000066 response candidate ordering follow-up
2026-08-17 14:38:15 +00:00
m 465a90b25d DEV-000066 prefer recent response candidates
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 16s
2026-08-17 17:37:20 +03:00
m 95d6ad2d6a Promote DEV-000066 response candidate window
CI / test (push) Successful in 19s
DEV-000066 response candidate window follow-up
2026-08-17 14:33:57 +00:00
m 1e554f6758 DEV-000066 widen response candidate resolution
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 16s
2026-08-17 17:33:05 +03:00
m 1c7d512710 Promote DEV-000066 prospective response summary resolution
CI / test (push) Successful in 16s
DEV-000066 prospective response summary resolution follow-up
2026-08-17 14:21:41 +00:00
m 8c6a7bebbd DEV-000066 resolve prospective responses by summary
CI / test (push) Successful in 17s
CI / test (pull_request) Successful in 14s
2026-08-17 17:20:44 +03:00
m 36774b37ae Promote DEV-000066 pre-delivery reminder responses
CI / test (push) Successful in 15s
DEV-000066 pre-delivery reminder response follow-up
2026-08-17 14:17:39 +00:00
m c79de1c757 DEV-000066 support pre-delivery reminder responses
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 19s
2026-08-17 17:16:29 +03:00
m acf012720f Promote DEV-000066 natural target resolution
CI / test (push) Successful in 15s
DEV-000066 natural target resolution follow-up
2026-08-17 14:11:51 +00:00
m ccf790d130 DEV-000066 natural response target resolution
CI / test (push) Successful in 17s
CI / test (pull_request) Successful in 16s
2026-08-17 17:11:10 +03:00
m 54c2cc27a3 Promote DEV-000066 human response continuation semantics
CI / test (push) Successful in 15s
DEV-000066 Human Response + Continuation Semantics V1
2026-08-17 14:04:11 +00:00
5 changed files with 145 additions and 28 deletions
+56 -21
View File
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/continuity" "git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity" "git.cin.su/m/Assistant/contracts/identity"
continuitydomain "git.cin.su/m/Assistant/modules/continuity/domain" continuitydomain "git.cin.su/m/Assistant/modules/continuity/domain"
@@ -213,21 +214,20 @@ func (r *ContinuityRepository) AttentionByOwner(ctx context.Context, owner ident
} }
func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner identity.PrincipalID, limit int) ([]continuitydomain.ResponseCandidate, error) { func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner identity.PrincipalID, limit int) ([]continuitydomain.ResponseCandidate, error) {
if owner == "" || limit < 1 || limit > 10 { if owner == "" || limit < 1 || limit > 50 {
return nil, fmt.Errorf("invalid response candidate query") return nil, fmt.Errorf("invalid response candidate query")
} }
rows, err := r.pool.Query(ctx, `SELECT rows, err := r.pool.Query(ctx, `SELECT
a.attention_item_id,a.owner_principal_id,a.scope_id,a.source_kind,a.source_prospective_item_id,a.level,a.state,a.summary, COALESCE(a.attention_item_id,''),COALESCE(a.owner_principal_id,''),COALESCE(a.scope_id,''),COALESCE(a.source_kind,''),COALESCE(a.source_prospective_item_id,''),COALESCE(a.level,''),COALESCE(a.state,''),COALESCE(a.summary,''),
a.due_at_unix_millis,a.created_at_unix_millis,a.delivered_at_unix_millis,a.action_intent_id,a.authorized_action_id,a.action_attempt_id,a.delivery_outcome, COALESCE(a.due_at_unix_millis,0),COALESCE(a.created_at_unix_millis,0),COALESCE(a.delivered_at_unix_millis,0),COALESCE(a.action_intent_id,''),COALESCE(a.authorized_action_id,''),COALESCE(a.action_attempt_id,''),COALESCE(a.delivery_outcome,''),
a.response_id,a.response_utterance_id,a.response_operation,a.response_outcome,a.responded_at_unix_millis,a.revision, COALESCE(a.response_id,''),COALESCE(a.response_utterance_id,''),COALESCE(a.response_operation,''),COALESCE(a.response_outcome,''),COALESCE(a.responded_at_unix_millis,0),COALESCE(a.revision,0),
p.prospective_item_id,p.commitment_id,p.owner_principal_id,p.scope_id,p.goal_id,p.source_intent_id,p.source_utterance_id,p.summary,p.trigger_at_unix_millis,p.state,p.revision p.prospective_item_id,p.commitment_id,p.owner_principal_id,p.scope_id,p.goal_id,p.source_intent_id,p.source_utterance_id,p.summary,p.trigger_at_unix_millis,p.state,p.revision
FROM continuity.attention_items a FROM continuity.prospective_items p
JOIN continuity.prospective_items p ON p.prospective_item_id=a.source_prospective_item_id LEFT JOIN continuity.attention_items a ON a.source_kind=$2 AND a.source_prospective_item_id=p.prospective_item_id AND a.owner_principal_id=p.owner_principal_id
WHERE a.owner_principal_id=$1 WHERE p.owner_principal_id=$1
AND a.source_kind=$2 AND (a.attention_item_id IS NULL OR a.state IN ($3,$4,$5))
AND a.state IN ($3,$4,$5)
AND p.state IN ($6,$7) AND p.state IN ($6,$7)
ORDER BY a.delivered_at_unix_millis DESC, a.due_at_unix_millis DESC, a.attention_item_id DESC ORDER BY p.recorded_at DESC, COALESCE(a.delivered_at_unix_millis,0) DESC, p.prospective_item_id DESC
LIMIT $8`, LIMIT $8`,
owner, continuity.AttentionSourceProspectiveReminder, continuity.AttentionDelivered, continuity.AttentionPending, owner, continuity.AttentionSourceProspectiveReminder, continuity.AttentionDelivered, continuity.AttentionPending,
continuity.AttentionFailed, continuity.ProspectiveDelivered, continuity.ProspectiveArmed, limit) continuity.AttentionFailed, continuity.ProspectiveDelivered, continuity.ProspectiveArmed, limit)
@@ -238,20 +238,45 @@ func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner ide
var candidates []continuitydomain.ResponseCandidate var candidates []continuitydomain.ResponseCandidate
for rows.Next() { for rows.Next() {
var candidate continuitydomain.ResponseCandidate var candidate continuitydomain.ResponseCandidate
var attentionID, attentionOwner, attentionScope, sourceKind, sourceProspective, level, attentionState, attentionSummary string
var actionIntent, authorizedAction, actionAttempt, deliveryOutcome, responseID, responseUtterance, responseOperation, responseOutcome string
var dueAt, createdAt, deliveredAt, respondedAt int64
var attentionRevision uint64
if err := rows.Scan( if err := rows.Scan(
&candidate.Attention.ID, &candidate.Attention.OwnerPrincipalID, &candidate.Attention.ScopeID, &candidate.Attention.SourceKind, &attentionID, &attentionOwner, &attentionScope, &sourceKind, &sourceProspective, &level, &attentionState, &attentionSummary,
&candidate.Attention.SourceProspectiveID, &candidate.Attention.Level, &candidate.Attention.State, &candidate.Attention.Summary, &dueAt, &createdAt, &deliveredAt, &actionIntent, &authorizedAction, &actionAttempt, &deliveryOutcome,
&candidate.Attention.DueAtUnixMillis, &candidate.Attention.CreatedAtUnixMillis, &candidate.Attention.DeliveredAtUnixMillis, &responseID, &responseUtterance, &responseOperation, &responseOutcome, &respondedAt, &attentionRevision,
&candidate.Attention.ActionIntentID, &candidate.Attention.AuthorizedActionID, &candidate.Attention.ActionAttemptID,
&candidate.Attention.DeliveryOutcome, &candidate.Attention.ResponseID, &candidate.Attention.ResponseUtteranceID,
&candidate.Attention.ResponseOperation, &candidate.Attention.ResponseOutcome, &candidate.Attention.RespondedAtUnixMillis,
&candidate.Attention.Revision,
&candidate.Prospective.ID, &candidate.Prospective.CommitmentID, &candidate.Prospective.OwnerPrincipalID, &candidate.Prospective.ID, &candidate.Prospective.CommitmentID, &candidate.Prospective.OwnerPrincipalID,
&candidate.Prospective.ScopeID, &candidate.Prospective.GoalID, &candidate.Prospective.SourceIntentID, &candidate.Prospective.ScopeID, &candidate.Prospective.GoalID, &candidate.Prospective.SourceIntentID,
&candidate.Prospective.SourceUtteranceID, &candidate.Prospective.Summary, &candidate.Prospective.TriggerAtUnixMillis, &candidate.Prospective.SourceUtteranceID, &candidate.Prospective.Summary, &candidate.Prospective.TriggerAtUnixMillis,
&candidate.Prospective.State, &candidate.Prospective.Revision); err != nil { &candidate.Prospective.State, &candidate.Prospective.Revision); err != nil {
return nil, fmt.Errorf("scan human response candidate: %w", err) return nil, fmt.Errorf("scan human response candidate: %w", err)
} }
if attentionID != "" {
candidate.Attention = continuity.AttentionItem{
ID: continuity.AttentionItemID(attentionID),
OwnerPrincipalID: identity.PrincipalID(attentionOwner),
ScopeID: authority.ScopeID(attentionScope),
SourceKind: continuity.AttentionSourceKind(sourceKind),
SourceProspectiveID: continuity.ProspectiveItemID(sourceProspective),
Level: continuity.AttentionLevel(level),
State: continuity.AttentionState(attentionState),
Summary: attentionSummary,
DueAtUnixMillis: dueAt,
CreatedAtUnixMillis: createdAt,
DeliveredAtUnixMillis: deliveredAt,
ActionIntentID: continuity.CommittedActionIntentID(actionIntent),
AuthorizedActionID: continuity.CommittedAuthorizedActionID(authorizedAction),
ActionAttemptID: continuity.CommittedActionAttemptID(actionAttempt),
DeliveryOutcome: deliveryOutcome,
ResponseID: continuity.HumanResponseID(responseID),
ResponseUtteranceID: responseUtterance,
ResponseOperation: continuity.HumanResponseOperation(responseOperation),
ResponseOutcome: continuity.ResponseOutcome(responseOutcome),
RespondedAtUnixMillis: respondedAt,
Revision: continuity.AttentionRevision(attentionRevision),
}
}
candidates = append(candidates, candidate) candidates = append(candidates, candidate)
} }
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
@@ -292,6 +317,9 @@ func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response
if tag.RowsAffected() != 1 { if tag.RowsAffected() != 1 {
return ErrProspectiveStateConflict return ErrProspectiveStateConflict
} }
if response.TargetAttentionID == "" {
return ErrAttentionNotFound
}
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil { if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil {
return err return err
} }
@@ -306,8 +334,10 @@ func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response
if tag.RowsAffected() != 1 { if tag.RowsAffected() != 1 {
return ErrProspectiveStateConflict return ErrProspectiveStateConflict
} }
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil { if response.TargetAttentionID != "" {
return err if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil {
return err
}
} }
case continuity.ResponseCancel: case continuity.ResponseCancel:
tag, err := tx.Exec(ctx, `UPDATE continuity.prospective_items SET state=$1, revision=revision+1 tag, err := tx.Exec(ctx, `UPDATE continuity.prospective_items SET state=$1, revision=revision+1
@@ -320,10 +350,15 @@ func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response
if tag.RowsAffected() != 1 { if tag.RowsAffected() != 1 {
return ErrProspectiveStateConflict return ErrProspectiveStateConflict
} }
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionSuppressed); err != nil { if response.TargetAttentionID != "" {
return err if err = markAttentionResponded(ctx, tx, response, continuity.AttentionSuppressed); err != nil {
return err
}
} }
case continuity.ResponseAcknowledge, continuity.ResponseDismiss: case continuity.ResponseAcknowledge, continuity.ResponseDismiss:
if response.TargetAttentionID == "" {
return ErrAttentionNotFound
}
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil { if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil {
return err return err
} }
+1 -1
View File
@@ -42,7 +42,7 @@ func registerCreatorResponseRoute(mux *http.ServeMux, identityClient creatorIden
http.Error(w, "human response unavailable", http.StatusServiceUnavailable) http.Error(w, "human response unavailable", http.StatusServiceUnavailable)
return return
} }
candidates, err := repository.ResponseCandidates(r.Context(), session.PrincipalID, 3) candidates, err := repository.ResponseCandidates(r.Context(), session.PrincipalID, 20)
if err != nil { if err != nil {
http.Error(w, "response candidates unavailable", http.StatusInternalServerError) http.Error(w, "response candidates unavailable", http.StatusInternalServerError)
return return
+4 -3
View File
@@ -4,7 +4,7 @@ Audit date: 2026-08-17. Active implementation: DEV-000066 revision 1.
## DEV-000066 checkpoint ## DEV-000066 checkpoint
- `DEV_000066_STATUS = IMPLEMENTED_NOT_PROMOTED` - `DEV_000066_STATUS = PRODUCTION_ACTIVE`
- `LAST_COMPLETED = ADR-0015 accepted; Continuity-owned HumanResponse contract foundation; ResponseTargetResolution semantics; append-only human_responses migration; Attention response projection fields; deterministic COMPLETE/SNOOZE/CANCEL/DISMISS interpretation; Creator Web response buttons over ordinary /chat; ActiveProjectFocus leak regression under Interaction semantics` - `LAST_COMPLETED = ADR-0015 accepted; Continuity-owned HumanResponse contract foundation; ResponseTargetResolution semantics; append-only human_responses migration; Attention response projection fields; deterministic COMPLETE/SNOOZE/CANCEL/DISMISS interpretation; Creator Web response buttons over ordinary /chat; ActiveProjectFocus leak regression under Interaction semantics`
- `ADR_STATUS = ADR-0015 accepted` - `ADR_STATUS = ADR-0015 accepted`
- `OWNERSHIP_STATUS = no new owner; HumanResponse and response target resolution belong to Continuity; Interaction remains utterance/original-intent owner` - `OWNERSHIP_STATUS = no new owner; HumanResponse and response target resolution belong to Continuity; Interaction remains utterance/original-intent owner`
@@ -14,8 +14,9 @@ Audit date: 2026-08-17. Active implementation: DEV-000066 revision 1.
- `AUTHORITY_STATUS = no new external mutation authority; human reported completion is not verified world truth` - `AUTHORITY_STATUS = no new external mutation authority; human reported completion is not verified world truth`
- `LLM_FREE_STATUS = obvious response operations are deterministic and do not require cognitive model` - `LLM_FREE_STATUS = obvious response operations are deterministic and do not require cognitive model`
- `TESTS_PASS = go test ./...; go vet ./...; go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json; go run ./cmd/devctl check-architecture; go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json; tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1; tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1` - `TESTS_PASS = go test ./...; go vet ./...; go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json; go run ./cmd/devctl check-architecture; go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json; tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1; tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1`
- `LIVE_ACCEPTANCE = pending production deployment` - `LIVE_ACCEPTANCE = PASS: external /chat created real reminders; SNOOZE after delivery re-armed the same ProspectiveItem and redelivered once without duplicate AttentionItem; COMPLETE after delivery closed Attention as USER_REPORTED_COMPLETE; CANCEL before delivery suppressed ProspectiveItem and produced no AttentionItem after due window; stale UPO ActiveProjectFocus cleared`
- `PRODUCTION_STATUS = pending promotion/deployment` - `PRODUCTION_STATUS = healthy`
- `PRODUCTION_REVISION = 071adb5ecbf31325678a557c371c512c486d8274`
- `ACTIVE_COGNITIVE_BINDINGS = none; CPU CANARY unchanged` - `ACTIVE_COGNITIVE_BINDINGS = none; CPU CANARY unchanged`
- `OPENAI_REAL_CREDENTIAL = not required / not activated` - `OPENAI_REAL_CREDENTIAL = not required / not activated`
+45 -3
View File
@@ -50,6 +50,12 @@ func InterpretHumanResponse(request contract.HumanResponseRequest, owner identit
response.Clarification = "Я не вижу активного ожидания, к которому это относится." response.Clarification = "Я не вижу активного ожидания, к которому это относится."
return contract.HumanResponseResult{Response: response, Handled: true, Reply: response.Clarification}, nil return contract.HumanResponseResult{Response: response, Handled: true, Reply: response.Clarification}, nil
} }
if len(candidates) > 1 {
matched := matchingCandidates(message, candidates)
if len(matched) == 1 {
candidates = matched
}
}
if len(candidates) > 1 { if len(candidates) > 1 {
response.ResolutionState = contract.ResponseAmbiguous response.ResolutionState = contract.ResponseAmbiguous
response.Outcome = contract.ResponseOutcomeClarificationNeeded response.Outcome = contract.ResponseOutcomeClarificationNeeded
@@ -57,9 +63,14 @@ func InterpretHumanResponse(request contract.HumanResponseRequest, owner identit
return contract.HumanResponseResult{Response: response, Handled: true, Reply: response.Clarification}, nil return contract.HumanResponseResult{Response: response, Handled: true, Reply: response.Clarification}, nil
} }
candidate := candidates[0] candidate := candidates[0]
response.TargetKind = contract.ResponseTargetAttentionItem if candidate.Attention.ID != "" {
response.TargetAttentionID = candidate.Attention.ID response.TargetKind = contract.ResponseTargetAttentionItem
response.TargetProspectiveID = candidate.Attention.SourceProspectiveID response.TargetAttentionID = candidate.Attention.ID
response.TargetProspectiveID = candidate.Attention.SourceProspectiveID
} else {
response.TargetKind = contract.ResponseTargetProspectiveItem
response.TargetProspectiveID = candidate.Prospective.ID
}
response.ResolutionState = contract.ResponseResolved response.ResolutionState = contract.ResponseResolved
switch operation { switch operation {
case contract.ResponseComplete: case contract.ResponseComplete:
@@ -142,6 +153,9 @@ func candidateSummary(candidates []ResponseCandidate) string {
parts := make([]string, 0, len(candidates)) parts := make([]string, 0, len(candidates))
for _, candidate := range candidates { for _, candidate := range candidates {
text := strings.TrimSpace(candidate.Attention.Summary) text := strings.TrimSpace(candidate.Attention.Summary)
if text == "" {
text = strings.TrimSpace(candidate.Prospective.Summary)
}
if text == "" { if text == "" {
text = "ожидание" text = "ожидание"
} }
@@ -153,6 +167,34 @@ func candidateSummary(candidates []ResponseCandidate) string {
return strings.Join(parts, "; ") return strings.Join(parts, "; ")
} }
func matchingCandidates(message string, candidates []ResponseCandidate) []ResponseCandidate {
lower := strings.ToLower(message)
var matched []ResponseCandidate
for _, candidate := range candidates {
summary := candidate.Attention.Summary
if summary == "" {
summary = candidate.Prospective.Summary
}
if summaryMatches(lower, summary) {
matched = append(matched, candidate)
}
}
return matched
}
func summaryMatches(lowerMessage string, summary string) bool {
for _, token := range strings.Fields(strings.ToLower(summary)) {
token = strings.Trim(token, ".,!?:;()[]«»\"'")
if len([]rune(token)) < 4 {
continue
}
if strings.Contains(lowerMessage, token) {
return true
}
}
return false
}
func humanResponseTime(value time.Time) string { func humanResponseTime(value time.Time) string {
if value.IsZero() { if value.IsZero() {
return "позже" return "позже"
@@ -34,6 +34,38 @@ func TestInterpretHumanResponseCancelsSingleReminder(t *testing.T) {
} }
} }
func TestInterpretHumanResponseCancelsArmedProspectiveBeforeAttention(t *testing.T) {
result := interpretResponseForTest(t, "Отмени встретить курьера", []ResponseCandidate{{
Prospective: continuity.ProspectiveItem{ID: "p1", OwnerPrincipalID: "principal-1", ScopeID: "scope-1", Summary: "встретить курьера", State: continuity.ProspectiveArmed, Revision: 1},
}})
if !result.Handled || result.Response.TargetKind != continuity.ResponseTargetProspectiveItem || result.Response.TargetProspectiveID != "p1" || result.Response.Outcome != continuity.ResponseOutcomeCancelled {
t.Fatalf("unexpected prospective cancel response: %#v", result)
}
}
func TestInterpretHumanResponseResolvesArmedProspectiveByNaturalSummaryReference(t *testing.T) {
result := interpretResponseForTest(t, "Отмени встретить курьера", []ResponseCandidate{
{Prospective: continuity.ProspectiveItem{ID: "p1", OwnerPrincipalID: "principal-1", ScopeID: "scope-1", Summary: "позвонить врачу", State: continuity.ProspectiveArmed, Revision: 1}},
{Prospective: continuity.ProspectiveItem{ID: "p2", OwnerPrincipalID: "principal-1", ScopeID: "scope-1", Summary: "встретить курьера", State: continuity.ProspectiveArmed, Revision: 1}},
})
if !result.Handled || result.Response.TargetKind != continuity.ResponseTargetProspectiveItem || result.Response.TargetProspectiveID != "p2" || result.Response.Outcome != continuity.ResponseOutcomeCancelled {
t.Fatalf("unexpected prospective natural target response: %#v", result)
}
}
func TestInterpretHumanResponseResolvesSummaryReferenceBeyondFirstThreeCandidates(t *testing.T) {
candidates := []ResponseCandidate{
responseCandidate("a1", "p1", "первое ожидание"),
responseCandidate("a2", "p2", "второе ожидание"),
responseCandidate("a3", "p3", "третье ожидание"),
{Prospective: continuity.ProspectiveItem{ID: "p4", OwnerPrincipalID: "principal-1", ScopeID: "scope-1", Summary: "встретить курьера", State: continuity.ProspectiveArmed, Revision: 1}},
}
result := interpretResponseForTest(t, "Отмени встретить курьера", candidates)
if !result.Handled || result.Response.ResolutionState != continuity.ResponseResolved || result.Response.TargetProspectiveID != "p4" {
t.Fatalf("expected target resolution beyond first three candidates, got %#v", result)
}
}
func TestInterpretHumanResponseAmbiguousTargetAsksClarificationWithoutResolvedMutation(t *testing.T) { func TestInterpretHumanResponseAmbiguousTargetAsksClarificationWithoutResolvedMutation(t *testing.T) {
result := interpretResponseForTest(t, "Готово", []ResponseCandidate{responseCandidate("a1", "p1", "первое"), responseCandidate("a2", "p2", "второе")}) result := interpretResponseForTest(t, "Готово", []ResponseCandidate{responseCandidate("a1", "p1", "первое"), responseCandidate("a2", "p2", "второе")})
if !result.Handled || result.Response.ResolutionState != continuity.ResponseAmbiguous || result.Response.Outcome != continuity.ResponseOutcomeClarificationNeeded || !strings.Contains(result.Reply, "несколько") { if !result.Handled || result.Response.ResolutionState != continuity.ResponseAmbiguous || result.Response.Outcome != continuity.ResponseOutcomeClarificationNeeded || !strings.Contains(result.Reply, "несколько") {
@@ -41,6 +73,13 @@ func TestInterpretHumanResponseAmbiguousTargetAsksClarificationWithoutResolvedMu
} }
} }
func TestInterpretHumanResponseResolvesAmbiguousTargetByNaturalSummaryReference(t *testing.T) {
result := interpretResponseForTest(t, "Готово, проверить базу сделано", []ResponseCandidate{responseCandidate("a1", "p1", "позвонить врачу"), responseCandidate("a2", "p2", "проверить базу")})
if !result.Handled || result.Response.ResolutionState != continuity.ResponseResolved || result.Response.TargetAttentionID != "a2" {
t.Fatalf("expected natural summary target resolution, got %#v", result)
}
}
func TestInterpretHumanResponseNoTargetAsksClarification(t *testing.T) { func TestInterpretHumanResponseNoTargetAsksClarification(t *testing.T) {
result := interpretResponseForTest(t, "Готово", nil) result := interpretResponseForTest(t, "Готово", nil)
if !result.Handled || result.Response.ResolutionState != continuity.ResponseNoTarget || result.Response.Outcome != continuity.ResponseOutcomeClarificationNeeded { if !result.Handled || result.Response.ResolutionState != continuity.ResponseNoTarget || result.Response.Outcome != continuity.ResponseOutcomeClarificationNeeded {