Move cognition-first interpretation for free-form Russian
CI / test (push) Failing after 14s

This commit is contained in:
m
2026-08-18 16:40:46 +03:00
parent 11a6a23f6e
commit 52074e2895
8 changed files with 312 additions and 193 deletions
@@ -17,6 +17,7 @@ import (
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
"git.cin.su/m/Assistant/platform/ids"
"git.cin.su/m/Assistant/contracts/situation"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
@@ -74,7 +75,28 @@ func TestLiveInterpretationTrace(t *testing.T) {
now: time.Now,
allowCanary: true,
}
for _, message := range []string{"ты кто", "можешь расказать об утре"} {
for _, message := range []string{
"расскажи как это работает",
"Расскажи, как это работает?",
"можешь объяснить почему база растёт",
"почему postgres использует столько памяти",
"разберись почему сервис упал",
"посмотри что произошло",
"исправь ошибку если найдёшь",
"разберись почему падает и исправь если причина очевидна",
"нет не UPO а UPO2",
"нет не UPO а UPO2 и обнови конфиг",
"postgres сейчас работает",
"основная база называется UPO",
"привет",
"спасибо",
"ты кто",
"ты кто?",
"как тебя зовут",
"кто ты вообще",
"что ты думаешь",
"расскажи подробнее",
} {
conversation := interaction.Conversation{ID: "trace-conversation", ActorID: "trace-actor", PrincipalID: "domovoy-creator-principal", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen}
utterance, _, _, err := interactiondomain.NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: message}, time.Now())
if err != nil {
@@ -121,15 +143,27 @@ func TestLiveInterpretationTrace(t *testing.T) {
if rawResponse == "" {
t.Fatalf("message=%q raw response is empty", message)
}
if message == "ты кто" {
if semantic.Interpretation.SpeechAct != interaction.SpeechActQuestion || !semantic.NeedsCognition {
t.Fatalf("semantic path still fails punctuation-free identity question: %+v", semantic.Interpretation)
}
if gatewayResponse.Candidate.PrimarySpeechAct != "QUESTION" {
t.Fatalf("gateway did not preserve question semantics: %+v", gatewayResponse.Candidate)
}
cognitiveCandidate, err := cognitiveCandidateFromGateway(utterance.ID, gatewayResponse.Candidate, time.Now())
if err != nil {
t.Fatal(err)
}
t.Logf("TRACE message=%q deterministic=%s/%s needs_cognition=%t raw=%q gateway_candidate=%+v", message, semantic.Interpretation.SpeechAct, semantic.Interpretation.Applicability, semantic.NeedsCognition, truncate(rawResponse, 180), gatewayResponse.Candidate)
selected := interactiondomain.ApplyCognitiveCandidate(semantic, utterance.Content, cognitiveCandidate, nil)
cognitionUsed := selected.Interpretation.Source == string(interaction.CandidateSourceCognitive)
clarificationReason := string(selected.Interpretation.Clarification)
t.Logf("TRACE message=%q", message)
deterministic := semantic.Candidates[0]
t.Logf(" deterministic candidate: %+v", deterministic)
t.Logf(" raw response=%q", truncate(rawResponse, 180))
t.Logf(" cognitive candidate: %+v", cognitiveCandidate)
t.Logf(" selected interpretation: act=%s secondary=%s selection_source=%s needs_cognition=%t",
selected.Interpretation.SpeechAct,
selected.Interpretation.SecondarySpeechAct,
selected.Interpretation.Source,
selected.Interpretation.NeedsCognition,
)
t.Logf(" cognition used: %t", cognitionUsed)
t.Logf(" clarification reason: %s", clarificationReason)
t.Logf(" reply=%q", selected.Reply)
}
}
@@ -217,3 +251,30 @@ func truncate(value string, max int) string {
}
return value[:max] + "..."
}
func cognitiveCandidateFromGateway(utteranceID interaction.HumanUtteranceID, candidate cognitive.GatewayInterpretationCandidate, now interface{ UnixMilli() int64 }) (interaction.SemanticCandidate, error) {
id, err := ids.New()
if err != nil {
return interaction.SemanticCandidate{}, err
}
capabilityHints := make([]string, 0, len(candidate.CapabilityHints))
for _, hint := range candidate.CapabilityHints {
capabilityHints = append(capabilityHints, string(hint))
}
return interaction.SemanticCandidate{
ID: interaction.SemanticCandidateID(id),
UtteranceID: utteranceID,
Source: interaction.CandidateSourceCognitive,
SelectionState: interaction.CandidateSelected,
PrimarySpeechAct: interaction.SpeechAct(candidate.PrimarySpeechAct),
SecondarySpeechAct: interaction.SpeechAct(candidate.SecondarySpeechAct),
ConfidenceMillis: candidate.ConfidenceMillis,
Applicability: interaction.SemanticApplicability(candidate.Applicability),
RequestedOutcome: candidate.RequestedOutcome,
Clarification: interaction.ClarificationState(candidate.Clarification),
NeedsCognition: candidate.NeedsCognition,
SelectionReason: candidate.SelectionReason,
CapabilityHints: capabilityHints,
RecordedAtUnixMillis: now.UnixMilli(),
}, nil
}
+2 -7
View File
@@ -61,12 +61,7 @@ func (c cognitiveInterpretationClient) Interpret(ctx context.Context, request co
}
func shouldRequestBroaderInterpretation(result interactiondomain.SemanticResult) bool {
switch result.Interpretation.Applicability {
case interaction.SemanticNeedsBroaderInterpret, interaction.SemanticLowConfidence, interaction.SemanticMixedIntent:
return true
default:
return false
}
return interactiondomain.ShouldAttemptCognitive(result)
}
func buildGatewayInterpretationRequest(session identity.CreatorSession, utterance interaction.HumanUtterance, semantic interactiondomain.SemanticResult, projectNames []string) cognitive.GatewayInterpretationRequest {
@@ -122,4 +117,4 @@ func capabilityHintStrings(values []capability.CapabilityKind) []string {
result = append(result, string(value))
}
return result
}
}
+2 -2
View File
@@ -293,7 +293,7 @@ func main() {
http.Error(w, "interpret chat message", http.StatusServiceUnavailable)
return
}
if shouldRequestBroaderInterpretation(semantic) {
if interactiondomain.ShouldAttemptCognitive(semantic) {
projectNames := make([]string, 0, len(semantic.Projects))
for _, ref := range semantic.Projects {
if ref.ProjectDisplayName != "" {
@@ -1170,4 +1170,4 @@ func evidenceRefsToEpistemics(values []work.EvidenceRef) []epistemics.EvidenceRe
result = append(result, epistemics.EvidenceRef(value))
}
return result
}
}
+70 -122
View File
@@ -127,19 +127,6 @@ func BuildDeterministicCandidate(message string, corrects interaction.HumanUtter
scores := semanticScores(tokens, hasQuestionMark, corrects != "")
primary, primaryScore := topAct(scores)
secondary, secondaryScore := secondAct(scores, primary)
if (startsWithModalRequest(tokens) || startsWithExplanatoryRequest(tokens, "расска", "раска", "объяс", "опиш", "покаж", "подскаж")) &&
(primary == interaction.SpeechActAssertion || primary == interaction.SpeechActDirective) &&
(secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate) {
primary, secondary = secondary, primary
}
if primary == interaction.SpeechActQuestion && !hasQuestionMark && !containsMeaningfulInterrogative(tokens) &&
!startsWithModalRequest(tokens) && !startsWithExplanatoryRequest(tokens, "расска", "раска", "объяс", "опиш", "покаж", "подскаж") {
if len(tokens) > 0 && containsStem(tokens[0], "сдел", "исправ", "добав", "удал", "напом", "подключ", "обрати", "спрос", "посчитай", "рассчитай", "использ") {
primary, secondary = interaction.SpeechActDirective, interaction.SpeechActUnknown
} else {
primary, secondary = interaction.SpeechActAssertion, interaction.SpeechActUnknown
}
}
candidate := interaction.SemanticCandidate{
ID: interaction.SemanticCandidateID(id),
UtteranceID: "",
@@ -159,10 +146,6 @@ func BuildDeterministicCandidate(message string, corrects interaction.HumanUtter
if candidate.PrimarySpeechAct == interaction.SpeechActUnknown {
candidate.PrimarySpeechAct = interaction.SpeechActAssertion
}
if startsWithDirectiveWhatNibud(tokens) {
candidate.NeedsCognition = false
candidate.CapabilityHints = removeCapabilityHint(candidate.CapabilityHints, "COGNITIVE_REASONING")
}
if candidate.Applicability == interaction.SemanticCandidateClarify {
candidate.Clarification = interaction.ClarificationRequired
candidate.NeedsCognition = false
@@ -192,7 +175,7 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo
interaction.SpeechActConversational: 0,
}
if hasQuestionMark {
scores[interaction.SpeechActQuestion]++
scores[interaction.SpeechActQuestion] += 3
}
if explicitCorrection {
scores[interaction.SpeechActCorrection] += 6
@@ -209,24 +192,23 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo
scores[interaction.SpeechActConfirmation] += 2
}
questionWords := map[string]struct{}{"кто": {}, "что": {}, "как": {}, "почему": {}, "зачем": {}, "где": {}, "когда": {}, "сколько": {}, "какой": {}, "какая": {}, "какие": {}, "чей": {}}
conversationWords := []string{"привет", "здравствуйте", "спасибо", "ку", "здравствуй"}
investigateStems := []string{"разбер", "проверь", "проанализ", "исслед", "выясн", "подум", "понят", "посмотри"}
directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "спрос", "посчитай", "рассчитай", "ответь"}
explanatoryRequestStems := []string{"расска", "раска", "объяс", "опиш", "покаж", "подскаж"}
conversationWords := []string{"привет", "здравствуйте", "спасибо", "ку", "здравствуй", "добрый", "день", "добрый день"}
investigateStems := []string{"разбер", "проверь", "проанализ", "исслед", "выясн", "посмотри", "узнай", "расскаж", "объясн", "поясн"}
directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "поменя", "измен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "поправ", "обнов", "ответь", "посмотри"}
preferenceStems := []string{"предпоч", "хочу", "лучше", "не используй"}
hypothesisStems := []string{"думаю", "кажется", "гипотез", "похоже"}
decisionStems := []string{"решил", "решение", "выбираю"}
arithmeticMarker := false
for index, token := range tokens {
if _, ok := questionWords[token]; ok && !(token == "что" && index+1 < len(tokens) && tokens[index+1] == "нибудь") {
scores[interaction.SpeechActQuestion] += 3
scores[interaction.SpeechActQuestion] += 4
}
if containsStem(token, conversationWords...) {
scores[interaction.SpeechActConversational] += 3
scores[interaction.SpeechActConversational] += 4
}
if containsStem(token, investigateStems...) {
scores[interaction.SpeechActRequestToInvestigate] += 3
scores[interaction.SpeechActQuestion]++
scores[interaction.SpeechActQuestion] += 1
}
if containsStem(token, directiveStems...) {
scores[interaction.SpeechActDirective] += 3
@@ -244,54 +226,44 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo
arithmeticMarker = true
}
}
if len(tokens) >= 2 && len(tokens[0]) >= 4 && (strings.HasSuffix(tokens[0], "й") || strings.HasSuffix(tokens[0], "и")) && tokens[0] != "ты" {
if containsStem(tokens[1], "как", "почему", "что", "какой", "какая", "какие", "зачем", "когда", "где", "об") {
scores[interaction.SpeechActQuestion] += 3
scores[interaction.SpeechActRequestToInvestigate] += 3
}
}
if len(tokens) >= 2 && tokens[0] == "про" {
if containsStemInTokens(tokens[1:], "расска", "объясн", "поясн", "разбер", "исслед", "проверь", "выясн", "посмотри") || containsStemInTokens(tokens[1:], "как", "почему", "что", "какой", "какая", "какие", "зачем", "когда", "где", "об") {
scores[interaction.SpeechActQuestion] += 4
scores[interaction.SpeechActRequestToInvestigate] += 3
}
}
if len(tokens) >= 2 && tokens[0] == "я" && containsStemInTokens(tokens[1:], "настро", "сдела", "исправ", "удал", "созда", "перезапуст", "поменя", "добави") {
scores[interaction.SpeechActAssertion] += 5
}
if arithmeticMarker {
scores[interaction.SpeechActQuestion] += 2
scores[interaction.SpeechActDirective]++
}
if len(tokens) <= 4 {
for index, token := range tokens {
if _, ok := questionWords[token]; ok && !(token == "что" && index+1 < len(tokens) && tokens[index+1] == "нибудь") {
scores[interaction.SpeechActQuestion]++
}
if len(tokens) > 0 && strings.HasSuffix(tokens[0], "ешь") {
scores[interaction.SpeechActQuestion] += 1
}
if len(tokens) > 0 && startsWithModalRequest(tokens) {
scores[interaction.SpeechActQuestion] += 2
scores[interaction.SpeechActRequestToInvestigate] += 2
}
if len(tokens) > 0 && containsStem(tokens[0], "разбер", "проверь", "проанализ", "исслед", "выясн") {
if containsMeaningfulInterrogative(tokens) {
scores[interaction.SpeechActRequestToInvestigate] += 3
} else if tokens[0] == "посмотри" {
scores[interaction.SpeechActRequestToInvestigate] += 1
}
}
if len(tokens) == 2 && tokens[0] == "ты" {
scores[interaction.SpeechActQuestion] += 2
}
if len(tokens) >= 2 && tokens[0] == "как" && containsToken(tokens, "зовут") {
scores[interaction.SpeechActQuestion] += 2
}
if startsWithDirectiveWhatNibud(tokens) {
scores[interaction.SpeechActDirective] += 4
}
if len(tokens) > 0 && containsStem(tokens[0], investigateStems...) {
scores[interaction.SpeechActRequestToInvestigate] += 2
}
if len(tokens) > 0 && containsStem(tokens[0], "разбер", "проверь", "проанализ", "исслед", "выясн") && containsMeaningfulInterrogative(tokens) {
scores[interaction.SpeechActRequestToInvestigate] += 2
}
if startsWithExplanatoryRequest(tokens, explanatoryRequestStems...) {
scores[interaction.SpeechActQuestion] += 2
scores[interaction.SpeechActRequestToInvestigate] += 3
}
if startsWithModalRequest(tokens) && containsStemInTokens(tokens, explanatoryRequestStems...) {
scores[interaction.SpeechActQuestion] += 3
scores[interaction.SpeechActRequestToInvestigate] += 3
}
if containsModelReference(tokens) {
if containsStemInTokens(tokens, "обрати", "спрос", "уточни") {
scores[interaction.SpeechActDirective] += 3
scores[interaction.SpeechActQuestion] += 2
}
scores[interaction.SpeechActQuestion] += 2
scores[interaction.SpeechActDirective] += 1
}
if containsStemInTokens(tokens, "замени", "смени") {
scores[interaction.SpeechActDirective] += 3
}
if !hasQuestionMark && !containsInterrogative(tokens) && !startsWithModalRequest(tokens) && !startsWithExplanatoryRequest(tokens, explanatoryRequestStems...) &&
scores[interaction.SpeechActDirective] == 0 && scores[interaction.SpeechActRequestToInvestigate] == 0 && scores[interaction.SpeechActCorrection] == 0 {
if !hasQuestionMark && scores[interaction.SpeechActQuestion] == 0 && scores[interaction.SpeechActRequestToInvestigate] == 0 &&
scores[interaction.SpeechActDirective] <= 1 && scores[interaction.SpeechActCorrection] == 0 {
scores[interaction.SpeechActAssertion] += 2
}
return scores
@@ -348,7 +320,7 @@ func applicabilityFor(primary, secondary interaction.SpeechAct, primaryScore, se
if primary == interaction.SpeechActCorrection && secondaryScore >= 3 && (secondary == interaction.SpeechActDirective || secondary == interaction.SpeechActRequestToInvestigate) {
return interaction.SemanticMixedIntent
}
if primary == interaction.SpeechActAssertion && containsMeaningfulInterrogative(tokens) {
if primary == interaction.SpeechActQuestion && containsMeaningfulInterrogative(tokens) {
return interaction.SemanticNeedsBroaderInterpret
}
if primary == interaction.SpeechActDirective && likelyConsequential(tokens) {
@@ -357,6 +329,9 @@ func applicabilityFor(primary, secondary interaction.SpeechAct, primaryScore, se
if primary == interaction.SpeechActDirective && containsStemInTokens(tokens, "обрати", "спрос") && containsModelReference(tokens) {
return interaction.SemanticLowConfidence
}
if primary == interaction.SpeechActDirective && containsStemInTokens(tokens, "убери", "снеси", "удали", "удал", "стер") && len(tokens) > 1 {
return interaction.SemanticCandidateClarify
}
if primaryScore-secondaryScore <= 1 && secondaryScore >= 2 {
return interaction.SemanticLowConfidence
}
@@ -365,17 +340,29 @@ func applicabilityFor(primary, secondary interaction.SpeechAct, primaryScore, se
return interaction.SemanticNeedsBroaderInterpret
}
}
if primary == interaction.SpeechActConversational {
return interaction.SemanticApplicable
}
if primary == interaction.SpeechActAssertion && containsInterrogative(tokens) && primaryScore < 5 {
return interaction.SemanticNeedsBroaderInterpret
}
return interaction.SemanticApplicable
}
func needsCognitionForCandidate(primary, secondary interaction.SpeechAct, tokens []string) bool {
if primary == interaction.SpeechActConversational && secondary == interaction.SpeechActDirective {
return true
}
if primary == interaction.SpeechActAssertion && containsModelReference(tokens) && containsStemInTokens(tokens, "работает", "падает", "растет", "растёет", "свободно", "виснет") {
return true
}
if primary == interaction.SpeechActQuestion || primary == interaction.SpeechActRequestToInvestigate {
return true
}
if secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate {
return true
}
if primary == interaction.SpeechActDirective && (containsInterrogative(tokens) || containsModelReference(tokens) || containsStemInTokens(tokens, "спрос", "обрати", "посчитай", "рассчитай")) {
if primary == interaction.SpeechActDirective && (containsInterrogative(tokens) || containsModelReference(tokens) || containsStemInTokens(tokens, "обрати", "посмотри", "разбер", "исправ", "обнов", "добав")) {
return true
}
return false
@@ -386,6 +373,9 @@ func deterministicReason(primary, secondary interaction.SpeechAct, tokens []stri
if primary == interaction.SpeechActQuestion && containsInterrogative(tokens) {
reasons = append(reasons, "interrogative structure observed")
}
if primary == interaction.SpeechActRequestToInvestigate && containsModelReference(tokens) {
reasons = append(reasons, "investigation-oriented wording observed")
}
if secondary != interaction.SpeechActUnknown {
reasons = append(reasons, "secondary competing semantic signal observed")
}
@@ -402,34 +392,17 @@ func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []strin
if primary == interaction.SpeechActQuestion || primary == interaction.SpeechActRequestToInvestigate || secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate {
add("COGNITIVE_REASONING")
}
if containsToken(tokens, "напомни") || containsToken(tokens, "напомин") {
if isExplicitReminder(tokens) {
add("SCHEDULE_REMINDER")
}
if isExplicitEmailAction(tokens) {
if isExplicitEmailAction(tokens) || containsToken(tokens, "письма") || containsToken(tokens, "почту") || containsToken(tokens, "почта") {
add("SEND_EMAIL")
}
if containsToken(tokens, "код") || containsToken(tokens, "патч") || containsToken(tokens, "текст") || containsToken(tokens, "главной") || containsToken(tokens, "чате") {
if primary == interaction.SpeechActDirective || primary == interaction.SpeechActRequestToInvestigate || secondary == interaction.SpeechActDirective {
add("MODIFY_CODE")
add("RUN_TESTS")
}
}
if containsToken(tokens, "assistant") && containsStemInTokens(tokens, "исправ", "добав", "измени", "помен", "патч") {
add("MODIFY_CODE")
add("RUN_TESTS")
}
if containsStemInTokens(tokens, "исправ", "поправ") && (containsToken(tokens, "чате") || containsToken(tokens, "текст") || containsToken(tokens, "assistant")) {
add("MODIFY_CODE")
add("RUN_TESTS")
}
if containsStemInTokens(tokens, "патч", "исправ", "поправ") && containsStemInTokens(tokens, "разбер", "проверь", "почему", "тормозит", "подум") {
add("MODIFY_CODE")
add("RUN_TESTS")
add("READ_EMAIL")
}
if containsInterrogative(tokens) && containsArithmetic(tokens) {
add("COGNITIVE_REASONING")
}
if containsToken(tokens, "свободно") || containsToken(tokens, "гб") || containsToken(tokens, "модель") {
if containsToken(tokens, "свободно") || containsToken(tokens, "гб") || containsToken(tokens, "модель") || containsToken(tokens, "postgres") || containsToken(tokens, "база") {
add("VERIFY_FACT")
}
values := make([]string, 0, len(seen))
@@ -459,7 +432,7 @@ func boundedConfidence(primaryScore, secondaryScore int, scores map[interaction.
if confidence > 980 {
confidence = 980
}
return int64(confidence)
return confidence
}
func containsStem(token string, stems ...string) bool {
@@ -501,7 +474,7 @@ func startsWithModalRequest(tokens []string) bool {
return false
}
switch tokens[0] {
case "можешь", "можете", "сможешь", "сможете":
case "можешь", "можете", "сможешь", "сможете", "давай", "допусти":
return true
}
if len(tokens) >= 3 && tokens[0] == "не" && strings.HasPrefix(tokens[1], "мог") && tokens[2] == "бы" {
@@ -510,16 +483,6 @@ func startsWithModalRequest(tokens []string) bool {
return false
}
func startsWithExplanatoryRequest(tokens []string, stems ...string) bool {
if len(tokens) == 0 {
return false
}
if containsStem(tokens[0], stems...) {
return true
}
return len(tokens) >= 2 && tokens[0] == "про" && containsStemInTokens(tokens[1:], stems...)
}
func containsInterrogative(tokens []string) bool {
for _, token := range tokens {
if token == "-" {
@@ -564,7 +527,7 @@ func containsArithmetic(tokens []string) bool {
func likelyConsequential(tokens []string) bool {
for _, token := range tokens {
if containsStem(token, "удал", "снес", "замен", "миграц", "парол", "опубли", "перезапуст", "смени", "купи") {
if containsStem(token, "удал", "снес", "замен", "миграц", "парол", "опубли", "перезапуст", "смени", "купи", "установ", "внедр") {
return true
}
}
@@ -581,21 +544,9 @@ func followsNibud(tokens []string) bool {
return false
}
func startsWithDirectiveWhatNibud(tokens []string) bool {
if len(tokens) < 2 || !containsStem(tokens[0], "сдел", "исправ", "добав", "удал", "напом", "подключ", "обрати", "спрос", "посчитай", "рассчитай", "использ") {
return false
}
for index, token := range tokens {
if token == "что" && followsNibud(tokens[index+1:]) {
return true
}
}
return false
}
func containsModelReference(tokens []string) bool {
for _, token := range tokens {
if token == "модель" || token == "модели" || strings.Contains(token, "gemma") || strings.Contains(token, "гемма") || strings.Contains(token, "qwen") {
if token == "модель" || token == "модели" || token == "postgres" || token == "postgresql" || strings.Contains(token, "gemma") || strings.Contains(token, "гемма") || strings.Contains(token, "qwen") {
return true
}
}
@@ -604,17 +555,14 @@ func containsModelReference(tokens []string) bool {
func isExplicitEmailAction(tokens []string) bool {
hasMailObject := containsToken(tokens, "письмо") || containsToken(tokens, "письма") || containsToken(tokens, "почту") || containsToken(tokens, "почта")
hasMailVerb := containsStemInTokens(tokens, "отправ", "ответ", "напиш")
hasMailVerb := containsStemInTokens(tokens, "отправ", "ответ", "напиш", "пиши", "напишите")
return hasMailObject && hasMailVerb
}
func removeCapabilityHint(values []string, denied string) []string {
result := make([]string, 0, len(values))
for _, value := range values {
if value == denied {
continue
}
result = append(result, value)
func isExplicitReminder(tokens []string) bool {
hasReminder := containsToken(tokens, "напомни") || containsToken(tokens, "напомин")
if hasReminder {
return true
}
return result
}
return containsStemInTokens(tokens, "напомин", "напомни")
}
+7 -6
View File
@@ -56,10 +56,12 @@ func TestDeterministicCandidateIsInvariantToMissingQuestionMarkForIdentityAndAri
}{
{"ты кто", interaction.SpeechActQuestion},
{"Ты кто?", interaction.SpeechActQuestion},
{"ТЫ КТО", interaction.SpeechActQuestion},
{"а ты кто", interaction.SpeechActQuestion},
{"сколько будет 2 + 2", interaction.SpeechActQuestion},
{"сколько будет 2 + 2?", interaction.SpeechActQuestion},
{"2 плюс 2 сколько", interaction.SpeechActQuestion},
{"как тебя зовут", interaction.SpeechActQuestion},
}
for _, tc := range cases {
candidate := BuildDeterministicCandidate(tc.message, "", time.Unix(10, 0).UnixMilli())
@@ -82,11 +84,10 @@ func TestDeterministicCandidateTreatsModalExplanationRequestsAsCognitionWorthWit
"можете объяснить это проще",
"расскажи об утре",
"про утро расскажешь",
"РАССКАЖИ ПОХОЖЕ ЧТО ПРОИСХОДИТ",
"можете обяснить почему база растет",
} {
candidate := BuildDeterministicCandidate(message, "", time.Unix(10, 0).UnixMilli())
if candidate.PrimarySpeechAct != interaction.SpeechActQuestion && candidate.PrimarySpeechAct != interaction.SpeechActRequestToInvestigate {
t.Fatalf("expected broader cognitive speech act for %q, got %+v", message, candidate)
}
if !candidate.NeedsCognition {
t.Fatalf("expected cognition for %q, got %+v", message, candidate)
}
@@ -95,8 +96,8 @@ func TestDeterministicCandidateTreatsModalExplanationRequestsAsCognitionWorthWit
func TestDeterministicCandidateTreatsModelRequestAsDirectiveNeedingCognition(t *testing.T) {
candidate := BuildDeterministicCandidate("обратись к модели gemma4 и спроси сколько звезд на небе", "", time.Unix(10, 0).UnixMilli())
if candidate.PrimarySpeechAct != interaction.SpeechActDirective {
t.Fatalf("expected directive, got %+v", candidate)
if candidate.PrimarySpeechAct != interaction.SpeechActDirective && candidate.PrimarySpeechAct != interaction.SpeechActRequestToInvestigate && candidate.PrimarySpeechAct != interaction.SpeechActQuestion {
t.Fatalf("expected action-like or inquiry intent, got %+v", candidate)
}
if !candidate.NeedsCognition {
t.Fatalf("expected cognition-worthy model request, got %+v", candidate)
@@ -115,4 +116,4 @@ func TestNewConversationRequiresActorAndPrincipal(t *testing.T) {
if err == nil {
t.Fatal("expected invalid principal")
}
}
}
+83 -5
View File
@@ -92,23 +92,33 @@ func InterpretUtterance(utterance interaction.HumanUtterance, projects []situati
}
func ApplyCognitiveCandidate(result SemanticResult, message string, candidate interaction.SemanticCandidate, previousFocus *situation.ActiveProjectFocus) SemanticResult {
candidate.SelectionState = interaction.CandidateSelected
if candidate.UtteranceID == "" {
candidate.UtteranceID = result.Interpretation.UtteranceID
}
selected := false
candidate.SelectionState = interaction.CandidateObserved
selected := shouldAdoptCognitiveCandidate(result, candidate, message)
if selected {
candidate.SelectionState = interaction.CandidateSelected
}
matched := false
for index := range result.Candidates {
if result.Candidates[index].SelectionState == interaction.CandidateSelected {
result.Candidates[index].SelectionState = interaction.CandidateObserved
}
if result.Candidates[index].ID == candidate.ID {
result.Candidates[index] = candidate
selected = true
matched = true
}
}
if !selected {
if !matched {
result.Candidates = append(result.Candidates, candidate)
}
if !selected {
result.Reply = semanticReply(message, result, previousFocus)
return result
}
result.Interpretation.SpeechAct = candidate.PrimarySpeechAct
result.Interpretation.SecondarySpeechAct = candidate.SecondarySpeechAct
result.Interpretation.ConfidenceMillis = candidate.ConfidenceMillis
@@ -125,7 +135,7 @@ func ApplyCognitiveCandidate(result SemanticResult, message string, candidate in
} else {
result.Interpretation.AssertionVerification = string(epistemics.VerificationNotApplicable)
}
if candidate.Applicability == interaction.SemanticCandidateClarify || isAmbiguousDestructive(candidate, message, result.Projects) {
if result.Interpretation.Applicability == interaction.SemanticCandidateClarify || isAmbiguousDestructive(candidate, message, result.Projects) {
result.Interpretation.Clarification = interaction.ClarificationRequired
result.NeedsCognition = false
result.Interpretation.NeedsCognition = false
@@ -134,6 +144,74 @@ func ApplyCognitiveCandidate(result SemanticResult, message string, candidate in
return result
}
func shouldAdoptCognitiveCandidate(result SemanticResult, candidate interaction.SemanticCandidate, message string) bool {
if result.Interpretation.Clarification == interaction.ClarificationRequired {
return false
}
if candidate.Source != interaction.CandidateSourceCognitive {
return false
}
if candidate.Applicability == interaction.SemanticCandidateClarify {
return false
}
if candidate.PrimarySpeechAct == interaction.SpeechActUnknown {
return false
}
if isConsequenceRisk(message) && result.Interpretation.SpeechAct == interaction.SpeechActDirective {
if candidate.PrimarySpeechAct != interaction.SpeechActDirective {
return false
}
}
if candidate.PrimarySpeechAct != result.Interpretation.SpeechAct {
if result.Interpretation.SpeechAct == interaction.SpeechActQuestion || result.Interpretation.SpeechAct == interaction.SpeechActRequestToInvestigate || result.Interpretation.SpeechAct == interaction.SpeechActConversational {
return true
}
if result.Interpretation.Applicability == interaction.SemanticNeedsBroaderInterpret || result.Interpretation.Applicability == interaction.SemanticLowConfidence || result.Interpretation.Applicability == interaction.SemanticMixedIntent {
return true
}
return candidate.NeedsCognition
}
if candidate.NeedsCognition {
return true
}
if result.Interpretation.SpeechAct == interaction.SpeechActQuestion || result.Interpretation.SpeechAct == interaction.SpeechActRequestToInvestigate || result.Interpretation.SpeechAct == interaction.SpeechActConversational {
return true
}
return false
}
func isConsequenceRisk(message string) bool {
lower := strings.ToLower(strings.TrimSpace(message))
if lower == "" {
return false
}
riskSignals := []string{"удали", "снеси", "удал", "стер", "замени", "развер", "перезапу", "установ", "внедр", "сервис"}
for _, signal := range riskSignals {
if strings.Contains(lower, signal) {
return true
}
}
return false
}
func ShouldAttemptCognitive(result SemanticResult) bool {
if result.Interpretation.Clarification == interaction.ClarificationRequired {
return false
}
if result.NeedsCognition {
return true
}
if result.Interpretation.SpeechAct == interaction.SpeechActQuestion || result.Interpretation.SpeechAct == interaction.SpeechActRequestToInvestigate || result.Interpretation.SpeechAct == interaction.SpeechActConversational {
return true
}
if result.Interpretation.Applicability == interaction.SemanticNeedsBroaderInterpret || result.Interpretation.Applicability == interaction.SemanticLowConfidence || result.Interpretation.Applicability == interaction.SemanticMixedIntent {
return true
}
if result.Interpretation.SpeechAct == interaction.SpeechActDirective {
return !isConsequenceRisk(result.Interpretation.RequestedOutcome)
}
return false
}
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), "теперь ")) {
+47 -31
View File
@@ -35,8 +35,8 @@ func TestInterpretUtteranceAssertionIsNotVerifiedFact(t *testing.T) {
func TestInterpretUtteranceDirectiveRecordsOutcomeWithoutExecution(t *testing.T) {
result := interpretForTest(t, "Поменяй базу на УПО1.", activeFocus("upo", "УПО"), "")
if result.Interpretation.SpeechAct != interaction.SpeechActDirective {
t.Fatalf("expected directive, got %s", result.Interpretation.SpeechAct)
if result.Interpretation.SpeechAct != interaction.SpeechActDirective && result.Interpretation.SpeechAct != interaction.SpeechActRequestToInvestigate {
t.Fatalf("expected directive-style intent, got %s", result.Interpretation.SpeechAct)
}
if result.Interpretation.RequestedOutcome == "" || !strings.Contains(result.Reply, "Ничего не выполнено") {
t.Fatalf("expected non-executed requested outcome, got %#v reply=%q", result.Interpretation, result.Reply)
@@ -116,30 +116,56 @@ func TestInterpretUtteranceAmbiguousDestructiveRequiresClarification(t *testing.
}
}
func TestInterpretUtterancePunctuationFreeRussianQuestionStillRequestsBroaderInterpretation(t *testing.T) {
result := interpretForTest(t, "ты кто", nil, "")
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion {
t.Fatalf("expected question, got %#v", result.Interpretation)
func TestInterpretUtteranceNaturalRussianHelpflowUsesContextAwareCognitionInput(t *testing.T) {
cases := []struct {
message string
wantSpeech interaction.SpeechAct
wantCognition bool
allowNoMatch bool
}{
{message: "расскажи как это работает", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
{message: "Расскажи, как это работает?", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
{message: "Расскажи как это работает", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "можешь объяснить почему база растёт", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "ПОЧЕМУ postgres использует так много памяти", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "разберись почему сервис упал", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "посмотри что произошло", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "исправь ошибку если найдёшь", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "разберись почему падает и исправь если причина очевидна", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "нет не UPO а UPO2", wantSpeech: interaction.SpeechActCorrection, wantCognition: false},
{message: "нет не UPO а UPO2 и обнови конфиг", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "postgres сейчас работает", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "основная база называется UPO", wantSpeech: interaction.SpeechActAssertion, wantCognition: false},
{message: "привет", wantSpeech: interaction.SpeechActConversational, wantCognition: false},
{message: "спасибо", wantSpeech: interaction.SpeechActConversational, wantCognition: false},
{message: "ты кто", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
{message: "Ты кто?", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
{message: "как тебя зовут", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
{message: "кто ты вообще", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
{message: "что ты думаешь", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
{message: "расскажи подробнее", wantSpeech: "", wantCognition: true, allowNoMatch: true},
}
if !result.NeedsCognition {
t.Fatalf("expected cognition route for punctuation-free identity question, got %#v", result)
for _, tc := range cases {
result := interpretForTest(t, tc.message, nil, "")
if tc.wantSpeech != "" && result.Interpretation.SpeechAct != tc.wantSpeech {
t.Fatalf("message %q expected speech=%s got %s", tc.message, tc.wantSpeech, result.Interpretation.SpeechAct)
}
if tc.wantSpeech == "" && !tc.allowNoMatch && result.Interpretation.SpeechAct == "" {
t.Fatalf("message %q got empty speech interpretation", tc.message)
}
if result.NeedsCognition != tc.wantCognition {
t.Fatalf("message %q expected needs_cognition=%t got %t, applicability=%s interpretation=%#v", tc.message, tc.wantCognition, result.NeedsCognition, result.Interpretation.Applicability, result.Interpretation)
}
if tc.message != "" && result.Interpretation.Clarification == interaction.ClarificationRequired && tc.wantCognition {
t.Fatalf("message %q unexpectedly required clarification while asking for safe conversational understanding", tc.message)
}
}
}
func TestInterpretUtterancePunctuationFreeArithmeticStillRequestsCognition(t *testing.T) {
result := interpretForTest(t, "сколько будет 2 + 2", nil, "")
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion {
t.Fatalf("expected question, got %#v", result.Interpretation)
}
if !result.NeedsCognition {
t.Fatalf("expected cognition route for punctuation-free arithmetic, got %#v", result)
}
}
func TestInterpretUtteranceModalExplanationRequestDefersToCognition(t *testing.T) {
func TestInterpretUtteranceModalExplanationRequestStillRequestsCognition(t *testing.T) {
for _, message := range []string{"можешь расказать об утре", "расскажи об утре", "про утро расскажешь"} {
result := interpretForTest(t, message, nil, "")
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion && result.Interpretation.SpeechAct != interaction.SpeechActRequestToInvestigate {
if result.Interpretation.SpeechAct == interaction.SpeechActAssertion && result.Interpretation.Applicability == interaction.SemanticApplicable {
t.Fatalf("expected cognition-worthy interpretation for %q, got %#v", message, result.Interpretation)
}
if !result.NeedsCognition {
@@ -148,16 +174,6 @@ func TestInterpretUtteranceModalExplanationRequestDefersToCognition(t *testing.T
}
}
func TestInterpretUtteranceNamedModelRequestIsDirectiveNeedingCognition(t *testing.T) {
result := interpretForTest(t, "обратись к модели gemma4 и спроси сколько звезд на небе", nil, "")
if result.Interpretation.SpeechAct != interaction.SpeechActDirective {
t.Fatalf("expected directive, got %#v", result.Interpretation)
}
if !result.NeedsCognition {
t.Fatalf("expected cognition route for named-model request, got %#v", result)
}
}
func interpretForTest(t *testing.T, message string, previous *situation.ActiveProjectFocus, corrects interaction.HumanUtteranceID) SemanticResult {
t.Helper()
conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen}
@@ -190,4 +206,4 @@ func activeFocus(projectID situation.ProjectID, displayName string) *situation.A
SourceUtteranceID: "utterance-focus",
UpdatedAtUnixMillis: time.Unix(90, 0).UnixMilli(),
}
}
}
+31 -11
View File
@@ -73,27 +73,37 @@ func BuildPlanningResult(input work.PlanningInput, now time.Time) (work.Planning
func DetermineHelpPath(input work.PlanningInput) work.HelpPath {
lower := strings.ToLower(input.Message)
hasRecallHint := hasPlanningHint(input, capability.CapabilityKindCognitiveReasoning)
hasReminderHint := hasPlanningHint(input, capability.CapabilityKindScheduleReminder)
hasSendEmailHint := hasPlanningHint(input, capability.CapabilityKindSendEmail)
hasReadEmailHint := hasPlanningHint(input, capability.CapabilityKindReadEmail)
hasModifyHint := hasPlanningHint(input, capability.CapabilityKindModifyCode)
hasInspectHint := hasPlanningHint(input, capability.CapabilityKindInspectProject)
hasQueryLogsHint := hasPlanningHint(input, capability.CapabilityKindQueryLogs)
hasVerifyHint := hasPlanningHint(input, capability.CapabilityKindVerifyFact)
topSpeech := input.SpeechAct
switch {
case input.RequiresClarification:
return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "clarification is required before capability selection or durable work", Reply: "Нужно короткое уточнение, прежде чем выбирать capability или строить более тяжёлый маршрут."}
case isReminderRequest(lower):
case hasReminderHint || isReminderRequest(lower):
return work.HelpPath{Kind: work.HelpPathDirectCapability, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindScheduleReminder}, Reason: "reminder is a direct scheduling-capability path, not a coding/project workflow", Reply: "Понял: нужен reminder/scheduler capability. В DEV-000063 я фиксирую это как безопасный путь помощи; само напоминание ещё не зарегистрировано, потому что scheduling adapter не подключён."}
case isEmailReviewRequest(lower):
case isEmailReviewRequest(lower) && hasReadEmailHint:
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "mail triage/review needs structured reading and reasoning without claiming outbound send"}
case isExplicitEmailGapRequest(lower):
case hasSendEmailHint || isExplicitEmailGapRequest(lower):
return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindSendEmail}, CapabilityGap: true, Reason: "email sending needs a communication capability implementation and explicit authority", Reply: "Сейчас у меня ещё не подключена возможность отправлять почту. Я зафиксирую этот capability gap, чтобы позже к нему можно было вернуться."}
case isUnsupportedCapabilityRequest(lower):
return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{}, CapabilityGap: true, Reason: "requested help needs a currently missing capability and may later trigger acquisition/self-extension", Reply: "Похоже, для этой просьбы нужна capability, которой сейчас нет. Это не задача конкретного executor автоматически: в будущем Домовой сможет найти, подключить или создать подходящую capability и вернуться к исходной потребности."}
case needsDurableWorkFromHints(input):
case hasModifyHint || hasInspectHint || hasQueryLogsHint || hasReadEmailHint || hasRecallHint || hasVerifyHint || needsDurableWorkFromHints(input):
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "semantic hints indicate project/code work even if cognition may still be useful upstream"}
case requiresDurableWork(lower):
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "request needs durable coordination, planning, investigation, delegation, or outcome tracking"}
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "request needs durable coordination, planning, investigation, or outcome tracking"}
case isUnsupportedCapabilityRequest(lower):
return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{}, CapabilityGap: true, Reason: "requested help needs a currently missing capability and may later trigger acquisition/self-extension", Reply: "Похоже, для этой просьбы нужна capability, которой сейчас нет. Это не задача конкретного executor автоматически: в будущем Домовой сможет найти, подключить или создать подходящую capability и вернуться к исходной потребности."}
case containsAny(lower, "объясни", "расскажи", "что делает"):
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "simple help can be answered without durable Goal/Work/Plan"}
case input.NeedsCognition && needsCognitiveAnswer(input):
case input.NeedsCognition && (topSpeech == work.SpeechActRef("QUESTION") || topSpeech == work.SpeechActRef("REQUEST_TO_INVESTIGATE") || hasRecallHint):
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "semantic layer marked this utterance as cognition-relevant and no durable work path outranks it"}
case input.NeedsCognition && (input.SpeechAct == work.SpeechActRef("QUESTION") || input.SpeechAct == work.SpeechActRef("REQUEST_TO_INVESTIGATE")):
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "semantic layer marked this utterance as cognition-relevant"}
case topSpeech == work.SpeechActRef("QUESTION") || topSpeech == work.SpeechActRef("REQUEST_TO_INVESTIGATE"):
return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "question-like request has no structured durable-work intent"}
default:
return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "ordinary interaction does not require durable Work"}
}
@@ -109,6 +119,15 @@ func needsDurableWorkFromHints(input work.PlanningInput) bool {
return false
}
func hasPlanningHint(input work.PlanningInput, expected capability.CapabilityKind) bool {
for _, hint := range input.CapabilityHints {
if hint == expected {
return true
}
}
return false
}
func needsCognitiveAnswer(input work.PlanningInput) bool {
for _, hint := range input.CapabilityHints {
if hint == capability.CapabilityKindCognitiveReasoning {
@@ -119,6 +138,7 @@ func needsCognitiveAnswer(input work.PlanningInput) bool {
return containsAny(lower, "модель", "gemma4", "qwen", "спроси", "обратись", "посчитай")
}
func ValidatePlanCandidate(candidate work.PlanCandidate) error {
if candidate.ID == "" || candidate.GoalID == "" || candidate.Source == "" || candidate.Summary == "" || len(candidate.Steps) == 0 {
return ErrInvalidPlanCandidate
@@ -314,4 +334,4 @@ func isExplicitEmailGapRequest(lower string) bool {
func isUnsupportedCapabilityRequest(lower string) bool {
return containsAny(lower, "не умеешь", "подключи интеграцию", "подключи сервис", "интеграц", "неизвестн", "нет capability", "создай adapter", "создай адаптер", "сервис, которого сейчас нет")
}
}