This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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, "напомин", "напомни")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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), "теперь ")) {
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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", "создай адаптер", "сервис, которого сейчас нет")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user