402 lines
20 KiB
Go
402 lines
20 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
|
||
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) {
|
||
result.NeedsCognition = false
|
||
result.Interpretation.NeedsCognition = false
|
||
result.CapabilityHints = nil
|
||
selectedCandidate.CapabilityHints = nil
|
||
result.Candidates[0] = selectedCandidate
|
||
}
|
||
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
|
||
}
|
||
|
||
// CarryForwardPreviousTask transfers only the routing meaning of a previous
|
||
// user task to a short retry utterance. The new utterance keeps its own IDs
|
||
// and is persisted separately, while the caller may use the previous content
|
||
// as the effective task input for an adapter or capability.
|
||
func CarryForwardPreviousTask(current, previous SemanticResult) SemanticResult {
|
||
if previous.Interpretation.Clarification == interaction.ClarificationRequired ||
|
||
previous.Interpretation.SpeechAct == interaction.SpeechActAssertion ||
|
||
previous.Interpretation.SpeechAct == interaction.SpeechActUnknown {
|
||
return current
|
||
}
|
||
current.Interpretation.SpeechAct = previous.Interpretation.SpeechAct
|
||
current.Interpretation.SecondarySpeechAct = previous.Interpretation.SecondarySpeechAct
|
||
current.Interpretation.ConfidenceMillis = previous.Interpretation.ConfidenceMillis
|
||
current.Interpretation.Source = "CONVERSATION_CONTINUATION"
|
||
current.Interpretation.AssertionVerification = previous.Interpretation.AssertionVerification
|
||
current.Interpretation.RequestedOutcome = previous.Interpretation.RequestedOutcome
|
||
current.Interpretation.Clarification = previous.Interpretation.Clarification
|
||
current.Interpretation.NeedsCognition = previous.Interpretation.NeedsCognition
|
||
current.Interpretation.Applicability = previous.Interpretation.Applicability
|
||
current.Interpretation.SelectionReason = "short retry carries forward the previous user task"
|
||
current.NeedsCognition = previous.NeedsCognition
|
||
current.CapabilityHints = append([]string(nil), previous.CapabilityHints...)
|
||
current.InformationRequestKind = previous.InformationRequestKind
|
||
if len(current.Candidates) > 0 && len(previous.Candidates) > 0 {
|
||
candidate := previous.Candidates[0]
|
||
candidate.ID = current.Candidates[0].ID
|
||
candidate.UtteranceID = current.Interpretation.UtteranceID
|
||
candidate.Source = interaction.CandidateSourceCognitive
|
||
candidate.SelectionState = interaction.CandidateSelected
|
||
candidate.RecordedAtUnixMillis = current.Interpretation.RecordedAtUnixMillis
|
||
current.Candidates[0] = candidate
|
||
}
|
||
current.Reply = "Повторяю предыдущую задачу."
|
||
return current
|
||
}
|
||
|
||
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.SpeechActPreference:
|
||
if name := PreferredAssistantName(message); name != "" {
|
||
return "Хорошо, зови меня " + name + "."
|
||
}
|
||
return "Хорошо, учту это дальше."
|
||
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.SpeechActConversational:
|
||
lower := strings.ToLower(message)
|
||
if strings.Contains(lower, "спасибо") || strings.Contains(lower, "благодар") {
|
||
return "Пожалуйста."
|
||
}
|
||
if strings.Contains(lower, "привет") || strings.Contains(lower, "здравств") {
|
||
return "Здравствуйте."
|
||
}
|
||
return "Я рядом."
|
||
case interaction.SpeechActConfirmation:
|
||
return "Хорошо."
|
||
case interaction.SpeechActRejection:
|
||
return "Понял."
|
||
case interaction.SpeechActQuestion:
|
||
if focusIntrospectionQuestion(semanticTokens(message)) {
|
||
if previousFocus == nil {
|
||
return "Сейчас активный Focus не выбран."
|
||
}
|
||
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)
|
||
}
|
||
|
||
// StableConversationalReply returns the small set of low-risk conversational
|
||
// replies whose meaning must remain owned by interaction even after a sampled
|
||
// cognitive audit or a capability fast path.
|
||
func StableConversationalReply(message string, act interaction.SpeechAct) (string, bool) {
|
||
switch act {
|
||
case interaction.SpeechActConversational:
|
||
lower := strings.ToLower(message)
|
||
if strings.Contains(lower, "спасибо") || strings.Contains(lower, "благодар") {
|
||
return "Пожалуйста.", true
|
||
}
|
||
if strings.Contains(lower, "привет") || strings.Contains(lower, "здравств") {
|
||
return "Здравствуйте.", true
|
||
}
|
||
return "Я рядом.", true
|
||
case interaction.SpeechActConfirmation:
|
||
return "Хорошо.", true
|
||
case interaction.SpeechActRejection:
|
||
return "Понял.", true
|
||
default:
|
||
return "", false
|
||
}
|
||
}
|
||
|
||
func isCurrentFocusQuestion(message string, act interaction.SpeechAct) bool {
|
||
if act != interaction.SpeechActQuestion {
|
||
return false
|
||
}
|
||
return focusIntrospectionQuestion(semanticTokens(message))
|
||
}
|
||
|
||
func hasCorrectionMeaning(candidate interaction.SemanticCandidate, utterance interaction.HumanUtterance) bool {
|
||
if utterance.CorrectsUtteranceID != "" {
|
||
return true
|
||
}
|
||
return candidate.PrimarySpeechAct == interaction.SpeechActCorrection || candidate.SecondarySpeechAct == interaction.SpeechActCorrection
|
||
}
|