253 lines
11 KiB
Go
253 lines
11 KiB
Go
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, "делаем")
|
||
}
|