package domain import ( "strings" "time" "git.cin.su/m/Assistant/contracts/epistemics" "git.cin.su/m/Assistant/contracts/interaction" "git.cin.su/m/Assistant/contracts/situation" "git.cin.su/m/Assistant/platform/ids" ) type SemanticResult struct { Interpretation interaction.UtteranceInterpretation Candidates []interaction.SemanticCandidate Projects []interaction.ProjectReference Relations []interaction.InteractionRelation ActiveFocus *situation.ActiveProjectFocus Reply string NeedsCognition bool CapabilityHints []string InformationRequestKind interaction.InformationRequestKind } func InterpretUtterance(utterance interaction.HumanUtterance, projects []situation.ProjectDescriptor, previousFocus *situation.ActiveProjectFocus, now time.Time) (SemanticResult, error) { interpretationID, err := ids.New() if err != nil { return SemanticResult{}, err } millis := now.UnixMilli() deterministicCandidate := BuildDeterministicCandidate(utterance.Content, utterance.CorrectsUtteranceID, millis) deterministicCandidate.UtteranceID = utterance.ID selectedCandidate := deterministicCandidate result := SemanticResult{ Interpretation: interaction.UtteranceInterpretation{ ID: interaction.InterpretationID(interpretationID), UtteranceID: utterance.ID, SpeechAct: selectedCandidate.PrimarySpeechAct, SecondarySpeechAct: selectedCandidate.SecondarySpeechAct, ConfidenceMillis: selectedCandidate.ConfidenceMillis, Source: string(selectedCandidate.Source), AssertionVerification: string(epistemics.VerificationUnverified), RequestedOutcome: selectedCandidate.RequestedOutcome, Clarification: selectedCandidate.Clarification, NeedsCognition: selectedCandidate.NeedsCognition, Applicability: selectedCandidate.Applicability, SelectionReason: selectedCandidate.SelectionReason, RecordedAtUnixMillis: millis, }, Candidates: []interaction.SemanticCandidate{deterministicCandidate}, NeedsCognition: selectedCandidate.NeedsCognition, CapabilityHints: append([]string(nil), selectedCandidate.CapabilityHints...), InformationRequestKind: informationRequestKind(utterance.Content, selectedCandidate), } explicitFocus := explicitProjectFocus(utterance.Content, projects) projectRefs, err := projectReferences(utterance, projects, explicitFocus, previousFocus, millis) if err != nil { return SemanticResult{}, err } result.Projects = projectRefs if explicitFocus != nil { result.ActiveFocus = &situation.ActiveProjectFocus{ ActorID: utterance.ActorID, PrincipalID: utterance.PrincipalID, ProjectID: explicitFocus.ProjectID, ProjectDisplayName: explicitFocus.ProjectDisplayName, Provenance: situation.ProjectResolutionExplicit, ConfidenceMillis: explicitFocus.ConfidenceMillis, SourceUtteranceID: string(utterance.ID), UpdatedAtUnixMillis: millis, } } if selectedCandidate.Applicability == interaction.SemanticCandidateClarify || isAmbiguousDestructive(selectedCandidate, utterance.Content, result.Projects) { result.Interpretation.Clarification = interaction.ClarificationRequired result.Interpretation.Applicability = interaction.SemanticCandidateClarify result.Interpretation.SelectionReason = "consequential ambiguity requires clarification before relying on cheap deterministic interpretation" result.NeedsCognition = false result.Reply = "Уточните проект и цель: это потенциально разрушительное изменение, а текущая ссылка недостаточно надёжна." return result, nil } if hasCorrectionMeaning(selectedCandidate, utterance) { relations, err := correctionRelations(utterance, millis) if err != nil { return SemanticResult{}, err } result.Relations = append(result.Relations, relations...) } result.Reply = semanticReply(utterance.Content, result, previousFocus) if isCurrentFocusQuestion(utterance.Content, selectedCandidate.PrimarySpeechAct) && previousFocus != nil { result.NeedsCognition = false result.Interpretation.NeedsCognition = false } return result, nil } 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 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 } } if !selected { result.Candidates = append(result.Candidates, candidate) } result.Interpretation.SpeechAct = candidate.PrimarySpeechAct result.Interpretation.SecondarySpeechAct = candidate.SecondarySpeechAct result.Interpretation.ConfidenceMillis = candidate.ConfidenceMillis result.Interpretation.Source = string(candidate.Source) result.Interpretation.RequestedOutcome = candidate.RequestedOutcome result.Interpretation.Clarification = candidate.Clarification result.Interpretation.NeedsCognition = candidate.NeedsCognition result.Interpretation.Applicability = candidate.Applicability result.Interpretation.SelectionReason = candidate.SelectionReason result.NeedsCognition = candidate.NeedsCognition result.CapabilityHints = append([]string(nil), candidate.CapabilityHints...) if result.Interpretation.SpeechAct == interaction.SpeechActAssertion { result.Interpretation.AssertionVerification = string(epistemics.VerificationUnverified) } else { result.Interpretation.AssertionVerification = string(epistemics.VerificationNotApplicable) } if candidate.Applicability == interaction.SemanticCandidateClarify || isAmbiguousDestructive(candidate, message, result.Projects) { result.Interpretation.Clarification = interaction.ClarificationRequired result.NeedsCognition = false result.Interpretation.NeedsCognition = false } result.Reply = semanticReply(message, result, previousFocus) return result } 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), "теперь ")) { return nil } for _, project := range projects { for _, alias := range projectAliases(project) { if strings.Contains(lower, alias) { return &interaction.ProjectReference{ProjectID: project.ID, ProjectDisplayName: project.DisplayName, Provenance: situation.ProjectResolutionExplicit, ConfidenceMillis: 980} } } } return nil } func projectReferences(utterance interaction.HumanUtterance, projects []situation.ProjectDescriptor, explicit *interaction.ProjectReference, previousFocus *situation.ActiveProjectFocus, millis int64) ([]interaction.ProjectReference, error) { if explicit != nil { ref, err := newProjectReference(utterance.ID, explicit.ProjectID, explicit.ProjectDisplayName, interaction.ProjectRolePrimary, explicit.Provenance, explicit.ConfidenceMillis, millis) return []interaction.ProjectReference{ref}, err } lower := strings.ToLower(utterance.Content) var refs []interaction.ProjectReference for _, project := range projects { for _, alias := range projectAliases(project) { if strings.Contains(lower, alias) { role := projectRoleForAlias(lower, alias) ref, err := newProjectReference(utterance.ID, project.ID, project.DisplayName, role, situation.ProjectResolutionEntityReference, 850, millis) if err != nil { return nil, err } refs = append(refs, ref) break } } } if len(refs) == 0 && previousFocus != nil && inheritedFocusAllowed(utterance.SpeechAct, utterance.Content) { ref, err := newProjectReference(utterance.ID, previousFocus.ProjectID, previousFocus.ProjectDisplayName, interaction.ProjectRolePrimary, situation.ProjectResolutionInheritedFocus, 700, millis) if err != nil { return nil, err } refs = append(refs, ref) } if len(refs) > 1 { hasPrimary := false for _, ref := range refs { hasPrimary = hasPrimary || ref.Role == interaction.ProjectRolePrimary } if !hasPrimary { refs[len(refs)-1].Role = interaction.ProjectRolePrimary } } return refs, nil } func projectRoleForAlias(lower string, alias string) interaction.ProjectReferenceRole { aliasIndex := strings.Index(lower, alias) if aliasIndex < 0 { return interaction.ProjectRoleRelated } before := strings.TrimSpace(lower[:aliasIndex]) afterStart := aliasIndex + len(alias) after := "" if afterStart < len(lower) { after = strings.TrimSpace(lower[afterStart:]) } if strings.Contains(before, "как") || strings.Contains(before, "посмотри") || strings.Contains(after, "как источник") { return interaction.ProjectRoleReferenceSource } targetMarkers := []string{"сделай", "добавь", "измени", "поменяй", "перенеси", "используй", "настрой", "в "} for _, marker := range targetMarkers { if strings.Contains(before, marker) { return interaction.ProjectRolePrimary } } return interaction.ProjectRoleRelated } func newProjectReference(utteranceID interaction.HumanUtteranceID, projectID situation.ProjectID, display string, role interaction.ProjectReferenceRole, provenance situation.ProjectResolutionProvenance, confidence int64, millis int64) (interaction.ProjectReference, error) { id, err := ids.New() if err != nil { return interaction.ProjectReference{}, err } return interaction.ProjectReference{ID: interaction.ProjectReferenceID(id), UtteranceID: utteranceID, ProjectID: projectID, ProjectDisplayName: display, Role: role, Provenance: provenance, ConfidenceMillis: confidence, RecordedAtUnixMillis: millis}, nil } func projectAliases(project situation.ProjectDescriptor) []string { values := []string{strings.ToLower(project.DisplayName), strings.ToLower(string(project.ID))} for _, alias := range project.Aliases { values = append(values, strings.ToLower(string(alias))) } return values } func correctionRelations(utterance interaction.HumanUtterance, millis int64) ([]interaction.InteractionRelation, error) { if utterance.CorrectsUtteranceID == "" { return nil, nil } id, err := ids.New() if err != nil { return nil, err } return []interaction.InteractionRelation{{ID: interaction.InteractionRelationID(id), FromUtteranceID: utterance.ID, ToUtteranceID: utterance.CorrectsUtteranceID, Kind: interaction.RelationSupersedes, RecordedAtUnixMillis: millis}}, nil } func inheritedFocusAllowed(act interaction.SpeechAct, message string) bool { lower := strings.ToLower(message) if act == interaction.SpeechActUnknown { return false } if strings.Contains(lower, "туда") || strings.Contains(lower, "там") || strings.Contains(lower, "в этом проекте") { return true } if act == interaction.SpeechActQuestion || act == interaction.SpeechActRequestToInvestigate { return strings.Contains(lower, "база") || strings.Contains(lower, "сервер") || strings.Contains(lower, "почему тормозит") || strings.Contains(lower, "что там") } return false } func isAmbiguousDestructive(candidate interaction.SemanticCandidate, message string, projects []interaction.ProjectReference) bool { lower := strings.ToLower(message) destructive := strings.Contains(lower, "удали") || strings.Contains(lower, "снеси") || strings.Contains(lower, "удалить") if !destructive || (candidate.PrimarySpeechAct != interaction.SpeechActDirective && candidate.SecondarySpeechAct != interaction.SpeechActDirective) { return false } if len(projects) == 0 { return true } return projects[0].Provenance == situation.ProjectResolutionInheritedFocus || projects[0].Provenance == situation.ProjectResolutionInferred } func semanticReply(message string, result SemanticResult, previousFocus *situation.ActiveProjectFocus) string { if result.ActiveFocus != nil { return "Фокус переключён: " + result.ActiveFocus.ProjectDisplayName + "." } switch result.Interpretation.SpeechAct { case interaction.SpeechActAssertion: if result.Interpretation.Applicability == interaction.SemanticNeedsBroaderInterpret || result.Interpretation.Applicability == interaction.SemanticLowConfidence { return "Смысл сообщения пока понят недостаточно надёжно. Я сохраню его и при доступности более широкого когнитивного разбора не буду притворяться, что это уже проверенный факт." } return "Записал как утверждение человека. Это пока не проверенный факт." case interaction.SpeechActDirective: if result.Interpretation.SecondarySpeechAct == interaction.SpeechActCorrection { return "Понял это как исправление предыдущего понимания и одновременно как новую просьбу. Ничего не выполнено без отдельного подтверждённого действия." } return "Записал просьбу как намерение пользователя. Ничего не выполнено без отдельного подтверждённого действия." case interaction.SpeechActDecision: return "Записал это как явное решение пользователя для дальнейшей обработки." case interaction.SpeechActCorrection: if result.Interpretation.SecondarySpeechAct == interaction.SpeechActDirective || result.Interpretation.SecondarySpeechAct == interaction.SpeechActRequestToInvestigate { return "Исправление записано отдельно, и я вижу здесь ещё новую рабочую просьбу; история сохранена без переписывания." } return "Исправление записано новым сообщением; прежняя история сохранена." case interaction.SpeechActQuestion: lower := strings.ToLower(message) if previousFocus != nil && (strings.Contains(lower, "сейчас") || strings.Contains(lower, "что мы") || strings.Contains(lower, "делаем")) { return "Сейчас в фокусе: " + previousFocus.ProjectDisplayName + "." } if result.Interpretation.Applicability == interaction.SemanticNeedsBroaderInterpret || result.Interpretation.Applicability == interaction.SemanticLowConfidence { return "Похоже, здесь нужен более широкий разбор смысла, а не дешёвый поверхностный рефлекс. Я не буду делать вид, что уже понял всё достаточно точно." } case interaction.SpeechActRequestToInvestigate: if result.Interpretation.Applicability == interaction.SemanticNeedsBroaderInterpret || result.Interpretation.Applicability == interaction.SemanticLowConfidence { return "Похоже, здесь запрошено рассуждение или исследование. Я не буду финализировать смысл только по поверхностным признакам." } } return FallbackText(result.Interpretation.SpeechAct) } func isCurrentFocusQuestion(message string, act interaction.SpeechAct) bool { if act != interaction.SpeechActQuestion { return false } lower := strings.ToLower(message) return strings.Contains(lower, "сейчас") || strings.Contains(lower, "что мы") || strings.Contains(lower, "делаем") } func hasCorrectionMeaning(candidate interaction.SemanticCandidate, utterance interaction.HumanUtterance) bool { if utterance.CorrectsUtteranceID != "" { return true } return candidate.PrimarySpeechAct == interaction.SpeechActCorrection || candidate.SecondarySpeechAct == interaction.SpeechActCorrection }