Carry forward previous chat tasks on retry #281
@@ -507,11 +507,26 @@ func main() {
|
||||
http.Error(w, "project focus unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
var effectiveTaskMessage = utterance.Content
|
||||
var previousTask *interaction.HumanUtterance
|
||||
if request.ConversationID != "" && interactiondomain.IsRetryRequest(utterance.Content) {
|
||||
if history, historyErr := interactionRepository.Utterances(r.Context(), conversation.ID); historyErr == nil && len(history) > 0 {
|
||||
candidate := history[len(history)-1]
|
||||
previousTask = &candidate
|
||||
effectiveTaskMessage = candidate.Content
|
||||
}
|
||||
}
|
||||
semantic, err := interactiondomain.InterpretUtterance(utterance, projects, previousFocus, now)
|
||||
if err != nil {
|
||||
http.Error(w, "interpret chat message", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
if previousTask != nil {
|
||||
previousSemantic, previousErr := interactiondomain.InterpretUtterance(*previousTask, projects, previousFocus, now)
|
||||
if previousErr == nil {
|
||||
semantic = interactiondomain.CarryForwardPreviousTask(semantic, previousSemantic)
|
||||
}
|
||||
}
|
||||
routePlan := buildChatRoutePlan(semantic)
|
||||
cognitiveAttempted := false
|
||||
cognitiveAvailable := true
|
||||
@@ -525,9 +540,11 @@ func main() {
|
||||
projectNames = append(projectNames, ref.ProjectDisplayName)
|
||||
}
|
||||
}
|
||||
if response, interpretationErr := cognitiveClient.Interpret(r.Context(), buildGatewayInterpretationRequest(session, utterance, semantic, projectNames)); interpretationErr == nil && response.FinishReason == cognitive.FinishCompleted {
|
||||
cognitiveUtterance := utterance
|
||||
cognitiveUtterance.Content = effectiveTaskMessage
|
||||
if response, interpretationErr := cognitiveClient.Interpret(r.Context(), buildGatewayInterpretationRequest(session, cognitiveUtterance, semantic, projectNames)); interpretationErr == nil && response.FinishReason == cognitive.FinishCompleted {
|
||||
if candidate, candidateErr := interactionCandidateFromGateway(utterance.ID, response.Candidate, now); candidateErr == nil {
|
||||
semantic = interactiondomain.ApplyCognitiveCandidate(semantic, utterance.Content, candidate, previousFocus)
|
||||
semantic = interactiondomain.ApplyCognitiveCandidate(semantic, effectiveTaskMessage, candidate, previousFocus)
|
||||
} else {
|
||||
cognitiveAvailable = false
|
||||
}
|
||||
@@ -546,7 +563,7 @@ func main() {
|
||||
responseResult, responseErr := responseClient.Respond(r.Context(), r.Header.Get("Authorization"), continuity.HumanResponseRequest{
|
||||
SourceUtteranceID: string(utterance.ID),
|
||||
SourceIntentID: string(intent.ID),
|
||||
Message: utterance.Content,
|
||||
Message: effectiveTaskMessage,
|
||||
RequestedAtUnixMillis: now.UnixMilli(),
|
||||
})
|
||||
if semantic.Interpretation.SpeechAct == interaction.SpeechActCorrection {
|
||||
@@ -594,7 +611,7 @@ func main() {
|
||||
ProjectID: work.ProjectRef(projectID),
|
||||
ProjectDisplayName: activeFocusName,
|
||||
ProjectProvenance: work.ProjectProvenanceRef(activeFocusProvenance),
|
||||
Message: utterance.Content,
|
||||
Message: effectiveTaskMessage,
|
||||
SpeechAct: work.SpeechActRef(semantic.Interpretation.SpeechAct),
|
||||
SecondarySpeechAct: work.SpeechActRef(semantic.Interpretation.SecondarySpeechAct),
|
||||
AssertionVerification: epistemics.VerificationState(semantic.Interpretation.AssertionVerification),
|
||||
@@ -630,8 +647,8 @@ func main() {
|
||||
if planID, idErr := ids.New(); idErr == nil {
|
||||
researchCtx, cancelResearch := context.WithTimeout(r.Context(), 8*time.Second)
|
||||
result, researchErr := researchClient.SearchWeb(researchCtx, epistemics.WebSearchRequest{
|
||||
Plan: epistemics.ResearchPlan{ID: epistemics.ResearchPlanID("plan:" + planID), Question: utterance.Content, SourceClasses: []string{"web-search"}, FreshnessPolicy: "current"},
|
||||
PropositionKey: "question:" + string(utterance.ID), Query: utterance.Content,
|
||||
Plan: epistemics.ResearchPlan{ID: epistemics.ResearchPlanID("plan:" + planID), Question: effectiveTaskMessage, SourceClasses: []string{"web-search"}, FreshnessPolicy: "current"},
|
||||
PropositionKey: "question:" + string(utterance.ID), Query: effectiveTaskMessage,
|
||||
})
|
||||
cancelResearch()
|
||||
planningReply, researchBacked = renderResearchReply(result, researchErr)
|
||||
|
||||
@@ -213,7 +213,7 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo
|
||||
}
|
||||
questionWords := map[string]struct{}{"кто": {}, "что": {}, "как": {}, "почему": {}, "зачем": {}, "где": {}, "когда": {}, "сколько": {}, "какой": {}, "какая": {}, "какие": {}, "чей": {}}
|
||||
conversationWords := []string{"привет", "здравствуйте", "спасибо", "ку", "здравствуй"}
|
||||
investigateStems := []string{"разбер", "проверь", "проанализ", "исслед", "выясн", "уточн", "подум", "понят", "посмотри"}
|
||||
investigateStems := []string{"разбер", "проверь", "проанализ", "исслед", "выясн", "уточн", "подум", "понят", "посмотри", "попроб", "повтор", "снов"}
|
||||
directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "спрос", "посчитай", "рассчитай", "ответь"}
|
||||
explanatoryRequestStems := []string{"расска", "раска", "объяс", "опиш", "покаж", "подскаж", "сообщ"}
|
||||
preferenceStems := []string{"предпоч", "хочу", "лучше", "не используй"}
|
||||
@@ -300,6 +300,24 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo
|
||||
return scores
|
||||
}
|
||||
|
||||
// IsRetryRequest identifies a short continuation that asks Domovoy to repeat
|
||||
// the previous task. It deliberately carries no subject-specific knowledge;
|
||||
// the conversation owner supplies the previous task when available.
|
||||
func IsRetryRequest(message string) bool {
|
||||
tokens := semanticTokens(message)
|
||||
if len(tokens) == 0 || len(tokens) > 8 {
|
||||
return false
|
||||
}
|
||||
hasRetryCue := false
|
||||
for _, token := range tokens {
|
||||
if containsStem(token, "повтор", "снов", "попроб", "ещ") {
|
||||
hasRetryCue = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return hasRetryCue
|
||||
}
|
||||
|
||||
func topAct(scores map[interaction.SpeechAct]int) (interaction.SpeechAct, int) {
|
||||
bestAct, bestScore := interaction.SpeechActUnknown, -1
|
||||
for _, act := range []interaction.SpeechAct{
|
||||
|
||||
@@ -164,6 +164,14 @@ func TestRequestToClarifyCurrentFactRequestsWebResearch(t *testing.T) {
|
||||
t.Fatalf("clarification request for current fact was not routed to WEB_RESEARCH: %+v", candidate)
|
||||
}
|
||||
|
||||
func TestRetryRequestIsAContinuationCue(t *testing.T) {
|
||||
for _, message := range []string{"ещё раз попробуй", "повтори", "попробуй снова"} {
|
||||
if !IsRetryRequest(message) {
|
||||
t.Fatalf("expected retry cue for %q", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeterministicCandidateKeepsFirstPersonCompletedWorkAsAssertion(t *testing.T) {
|
||||
candidate := BuildDeterministicCandidate("Я всё уже настроил.", "", time.Unix(10, 0).UnixMilli())
|
||||
if candidate.PrimarySpeechAct != interaction.SpeechActAssertion {
|
||||
|
||||
@@ -136,6 +136,42 @@ func ApplyCognitiveCandidate(result SemanticResult, message string, candidate in
|
||||
return result
|
||||
}
|
||||
|
||||
// CarryForwardPreviousTask transfers only the routing meaning of a previous
|
||||
// user task to a short retry utterance. The new utterance keeps its own IDs
|
||||
// and is persisted separately, while the caller may use the previous content
|
||||
// as the effective task input for an adapter or capability.
|
||||
func CarryForwardPreviousTask(current, previous SemanticResult) SemanticResult {
|
||||
if previous.Interpretation.Clarification == interaction.ClarificationRequired ||
|
||||
previous.Interpretation.SpeechAct == interaction.SpeechActAssertion ||
|
||||
previous.Interpretation.SpeechAct == interaction.SpeechActUnknown {
|
||||
return current
|
||||
}
|
||||
current.Interpretation.SpeechAct = previous.Interpretation.SpeechAct
|
||||
current.Interpretation.SecondarySpeechAct = previous.Interpretation.SecondarySpeechAct
|
||||
current.Interpretation.ConfidenceMillis = previous.Interpretation.ConfidenceMillis
|
||||
current.Interpretation.Source = "CONVERSATION_CONTINUATION"
|
||||
current.Interpretation.AssertionVerification = previous.Interpretation.AssertionVerification
|
||||
current.Interpretation.RequestedOutcome = previous.Interpretation.RequestedOutcome
|
||||
current.Interpretation.Clarification = previous.Interpretation.Clarification
|
||||
current.Interpretation.NeedsCognition = previous.Interpretation.NeedsCognition
|
||||
current.Interpretation.Applicability = previous.Interpretation.Applicability
|
||||
current.Interpretation.SelectionReason = "short retry carries forward the previous user task"
|
||||
current.NeedsCognition = previous.NeedsCognition
|
||||
current.CapabilityHints = append([]string(nil), previous.CapabilityHints...)
|
||||
current.InformationRequestKind = previous.InformationRequestKind
|
||||
if len(current.Candidates) > 0 && len(previous.Candidates) > 0 {
|
||||
candidate := previous.Candidates[0]
|
||||
candidate.ID = current.Candidates[0].ID
|
||||
candidate.UtteranceID = current.Interpretation.UtteranceID
|
||||
candidate.Source = interaction.CandidateSourceCognitive
|
||||
candidate.SelectionState = interaction.CandidateSelected
|
||||
candidate.RecordedAtUnixMillis = current.Interpretation.RecordedAtUnixMillis
|
||||
current.Candidates[0] = candidate
|
||||
}
|
||||
current.Reply = "Повторяю предыдущую задачу."
|
||||
return current
|
||||
}
|
||||
|
||||
func explicitProjectFocus(message string, projects []situation.ProjectDescriptor) *interaction.ProjectReference {
|
||||
lower := strings.ToLower(message)
|
||||
if !(strings.Contains(lower, "работаем с") || strings.Contains(lower, "вернемся к") || strings.Contains(lower, "вернёмся к") || strings.HasPrefix(strings.TrimSpace(lower), "теперь ")) {
|
||||
|
||||
@@ -136,6 +136,30 @@ func TestInterpretUtterancePunctuationFreeArithmeticStillRequestsCognition(t *te
|
||||
}
|
||||
}
|
||||
|
||||
func TestCarryForwardPreviousTaskPreservesRoutingForShortRetry(t *testing.T) {
|
||||
current := SemanticResult{
|
||||
Interpretation: interaction.UtteranceInterpretation{UtteranceID: "retry", SpeechAct: interaction.SpeechActAssertion},
|
||||
Candidates: []interaction.SemanticCandidate{{ID: "current", UtteranceID: "retry"}},
|
||||
}
|
||||
previous := SemanticResult{
|
||||
Interpretation: interaction.UtteranceInterpretation{
|
||||
UtteranceID: "previous",
|
||||
SpeechAct: interaction.SpeechActRequestToInvestigate,
|
||||
RequestedOutcome: "узнать текущую погоду",
|
||||
NeedsCognition: false,
|
||||
Applicability: interaction.SemanticApplicable,
|
||||
Clarification: interaction.ClarificationNotRequired,
|
||||
RecordedAtUnixMillis: 100,
|
||||
},
|
||||
Candidates: []interaction.SemanticCandidate{{ID: "previous-candidate", PrimarySpeechAct: interaction.SpeechActRequestToInvestigate, CapabilityHints: []string{"WEB_RESEARCH"}}},
|
||||
CapabilityHints: []string{"WEB_RESEARCH"},
|
||||
}
|
||||
result := CarryForwardPreviousTask(current, previous)
|
||||
if result.Interpretation.SpeechAct != interaction.SpeechActRequestToInvestigate || result.CapabilityHints[0] != "WEB_RESEARCH" || result.Candidates[0].UtteranceID != "retry" {
|
||||
t.Fatalf("previous routing was not carried forward: %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpretUtteranceModalExplanationRequestDefersToCognition(t *testing.T) {
|
||||
for _, message := range []string{"можешь расказать об утре", "расскажи об утре", "про утро расскажешь"} {
|
||||
result := interpretForTest(t, message, nil, "")
|
||||
|
||||
Reference in New Issue
Block a user