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 Projects []interaction.ProjectReference Relations []interaction.InteractionRelation ActiveFocus *situation.ActiveProjectFocus Reply string NeedsCognition bool } func InterpretUtterance(utterance interaction.HumanUtterance, projects []situation.ProjectDescriptor, previousFocus *situation.ActiveProjectFocus, now time.Time) (SemanticResult, error) { act := ClassifySpeechAct(utterance.Content, utterance.CorrectsUtteranceID) interpretationID, err := ids.New() if err != nil { return SemanticResult{}, err } millis := now.UnixMilli() result := SemanticResult{ Interpretation: interaction.UtteranceInterpretation{ ID: interaction.InterpretationID(interpretationID), UtteranceID: utterance.ID, SpeechAct: act, ConfidenceMillis: confidenceFor(act), Source: "deterministic-v1", AssertionVerification: string(epistemics.VerificationUnverified), RequestedOutcome: requestedOutcome(utterance.Content, act), Clarification: interaction.ClarificationNotRequired, RecordedAtUnixMillis: millis, }, NeedsCognition: NeedsCognition(act), } 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 isAmbiguousDestructive(act, utterance.Content, result.Projects) { result.Interpretation.Clarification = interaction.ClarificationRequired result.Reply = "Уточните проект и цель: это потенциально разрушительное изменение, а текущая ссылка недостаточно надёжна." return result, nil } if act == interaction.SpeechActCorrection { 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, act) && previousFocus != nil { result.NeedsCognition = false } return result, nil } 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 confidenceFor(act interaction.SpeechAct) int64 { if act == interaction.SpeechActUnknown { return 300 } return 800 } func requestedOutcome(message string, act interaction.SpeechAct) string { if act == interaction.SpeechActDirective || act == interaction.SpeechActDecision || act == interaction.SpeechActRequestToInvestigate { return strings.TrimSpace(message) } return "" } 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(act interaction.SpeechAct, message string, projects []interaction.ProjectReference) bool { lower := strings.ToLower(message) destructive := strings.Contains(lower, "удали") || strings.Contains(lower, "снеси") || strings.Contains(lower, "удалить") if !destructive || act != 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: return "Записал как утверждение человека. Это пока не проверенный факт." case interaction.SpeechActDirective: return "Записал просьбу как намерение пользователя. Ничего не выполнено без отдельного подтверждённого действия." case interaction.SpeechActDecision: return "Записал это как явное решение пользователя для дальнейшей обработки." case interaction.SpeechActCorrection: return "Исправление записано новым сообщением; прежняя история сохранена." case interaction.SpeechActQuestion: lower := strings.ToLower(message) if previousFocus != nil && (strings.Contains(lower, "сейчас") || strings.Contains(lower, "что мы") || strings.Contains(lower, "делаем")) { return "Сейчас в фокусе: " + previousFocus.ProjectDisplayName + "." } } 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, "делаем") }