Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
465a90b25d | ||
|
|
95d6ad2d6a | ||
|
|
1e554f6758 | ||
|
|
1c7d512710 | ||
|
|
8c6a7bebbd | ||
|
|
36774b37ae | ||
|
|
c79de1c757 | ||
|
|
acf012720f | ||
|
|
ccf790d130 | ||
|
|
54c2cc27a3 |
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
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) {
|
||||
if owner == "" || limit < 1 || limit > 10 {
|
||||
if owner == "" || limit < 1 || limit > 50 {
|
||||
return nil, fmt.Errorf("invalid response candidate query")
|
||||
}
|
||||
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,
|
||||
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,
|
||||
a.response_id,a.response_utterance_id,a.response_operation,a.response_outcome,a.responded_at_unix_millis,a.revision,
|
||||
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,''),
|
||||
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,''),
|
||||
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
|
||||
FROM continuity.attention_items a
|
||||
JOIN continuity.prospective_items p ON p.prospective_item_id=a.source_prospective_item_id
|
||||
WHERE a.owner_principal_id=$1
|
||||
AND a.source_kind=$2
|
||||
AND a.state IN ($3,$4,$5)
|
||||
FROM continuity.prospective_items p
|
||||
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 p.owner_principal_id=$1
|
||||
AND (a.attention_item_id IS NULL OR a.state IN ($3,$4,$5))
|
||||
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`,
|
||||
owner, continuity.AttentionSourceProspectiveReminder, continuity.AttentionDelivered, continuity.AttentionPending,
|
||||
continuity.AttentionFailed, continuity.ProspectiveDelivered, continuity.ProspectiveArmed, limit)
|
||||
@@ -238,20 +238,45 @@ func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner ide
|
||||
var candidates []continuitydomain.ResponseCandidate
|
||||
for rows.Next() {
|
||||
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(
|
||||
&candidate.Attention.ID, &candidate.Attention.OwnerPrincipalID, &candidate.Attention.ScopeID, &candidate.Attention.SourceKind,
|
||||
&candidate.Attention.SourceProspectiveID, &candidate.Attention.Level, &candidate.Attention.State, &candidate.Attention.Summary,
|
||||
&candidate.Attention.DueAtUnixMillis, &candidate.Attention.CreatedAtUnixMillis, &candidate.Attention.DeliveredAtUnixMillis,
|
||||
&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,
|
||||
&attentionID, &attentionOwner, &attentionScope, &sourceKind, &sourceProspective, &level, &attentionState, &attentionSummary,
|
||||
&dueAt, &createdAt, &deliveredAt, &actionIntent, &authorizedAction, &actionAttempt, &deliveryOutcome,
|
||||
&responseID, &responseUtterance, &responseOperation, &responseOutcome, &respondedAt, &attentionRevision,
|
||||
&candidate.Prospective.ID, &candidate.Prospective.CommitmentID, &candidate.Prospective.OwnerPrincipalID,
|
||||
&candidate.Prospective.ScopeID, &candidate.Prospective.GoalID, &candidate.Prospective.SourceIntentID,
|
||||
&candidate.Prospective.SourceUtteranceID, &candidate.Prospective.Summary, &candidate.Prospective.TriggerAtUnixMillis,
|
||||
&candidate.Prospective.State, &candidate.Prospective.Revision); err != nil {
|
||||
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)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
@@ -292,6 +317,9 @@ func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response
|
||||
if tag.RowsAffected() != 1 {
|
||||
return ErrProspectiveStateConflict
|
||||
}
|
||||
if response.TargetAttentionID == "" {
|
||||
return ErrAttentionNotFound
|
||||
}
|
||||
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -306,8 +334,10 @@ func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response
|
||||
if tag.RowsAffected() != 1 {
|
||||
return ErrProspectiveStateConflict
|
||||
}
|
||||
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil {
|
||||
return err
|
||||
if response.TargetAttentionID != "" {
|
||||
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case continuity.ResponseCancel:
|
||||
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 {
|
||||
return ErrProspectiveStateConflict
|
||||
}
|
||||
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionSuppressed); err != nil {
|
||||
return err
|
||||
if response.TargetAttentionID != "" {
|
||||
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionSuppressed); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case continuity.ResponseAcknowledge, continuity.ResponseDismiss:
|
||||
if response.TargetAttentionID == "" {
|
||||
return ErrAttentionNotFound
|
||||
}
|
||||
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func registerCreatorResponseRoute(mux *http.ServeMux, identityClient creatorIden
|
||||
http.Error(w, "human response unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
candidates, err := repository.ResponseCandidates(r.Context(), session.PrincipalID, 3)
|
||||
candidates, err := repository.ResponseCandidates(r.Context(), session.PrincipalID, 20)
|
||||
if err != nil {
|
||||
http.Error(w, "response candidates unavailable", http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -50,6 +50,12 @@ func InterpretHumanResponse(request contract.HumanResponseRequest, owner identit
|
||||
response.Clarification = "Я не вижу активного ожидания, к которому это относится."
|
||||
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 {
|
||||
response.ResolutionState = contract.ResponseAmbiguous
|
||||
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
|
||||
}
|
||||
candidate := candidates[0]
|
||||
response.TargetKind = contract.ResponseTargetAttentionItem
|
||||
response.TargetAttentionID = candidate.Attention.ID
|
||||
response.TargetProspectiveID = candidate.Attention.SourceProspectiveID
|
||||
if candidate.Attention.ID != "" {
|
||||
response.TargetKind = contract.ResponseTargetAttentionItem
|
||||
response.TargetAttentionID = candidate.Attention.ID
|
||||
response.TargetProspectiveID = candidate.Attention.SourceProspectiveID
|
||||
} else {
|
||||
response.TargetKind = contract.ResponseTargetProspectiveItem
|
||||
response.TargetProspectiveID = candidate.Prospective.ID
|
||||
}
|
||||
response.ResolutionState = contract.ResponseResolved
|
||||
switch operation {
|
||||
case contract.ResponseComplete:
|
||||
@@ -142,6 +153,9 @@ func candidateSummary(candidates []ResponseCandidate) string {
|
||||
parts := make([]string, 0, len(candidates))
|
||||
for _, candidate := range candidates {
|
||||
text := strings.TrimSpace(candidate.Attention.Summary)
|
||||
if text == "" {
|
||||
text = strings.TrimSpace(candidate.Prospective.Summary)
|
||||
}
|
||||
if text == "" {
|
||||
text = "ожидание"
|
||||
}
|
||||
@@ -153,6 +167,34 @@ func candidateSummary(candidates []ResponseCandidate) string {
|
||||
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 {
|
||||
if value.IsZero() {
|
||||
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) {
|
||||
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, "несколько") {
|
||||
@@ -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) {
|
||||
result := interpretResponseForTest(t, "Готово", nil)
|
||||
if !result.Handled || result.Response.ResolutionState != continuity.ResponseNoTarget || result.Response.Outcome != continuity.ResponseOutcomeClarificationNeeded {
|
||||
|
||||
Reference in New Issue
Block a user