Files
Assistant/modules/interaction/domain/chat.go
m 2d142cdfdb
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 19s
DEV-000063 classify reminder and capability directives
2026-08-17 15:14:56 +03:00

145 lines
6.8 KiB
Go

package domain
import (
"errors"
"strings"
"time"
"unicode/utf8"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
"git.cin.su/m/Assistant/platform/ids"
)
const MaxHumanUtteranceBytes = 16 << 10
var (
ErrEmptyMessage = errors.New("empty human message")
ErrMessageTooLarge = errors.New("human message too large")
ErrInvalidPrincipal = errors.New("invalid interaction principal")
)
type Clock func() time.Time
func NewConversation(actorID identity.ActorID, principalID identity.PrincipalID, now time.Time) (interaction.Conversation, error) {
if actorID == "" || principalID == "" {
return interaction.Conversation{}, ErrInvalidPrincipal
}
id, err := ids.New()
if err != nil {
return interaction.Conversation{}, err
}
millis := now.UnixMilli()
return interaction.Conversation{
ID: interaction.ConversationID(id),
ActorID: actorID,
PrincipalID: principalID,
Channel: interaction.ChannelWebChat,
State: interaction.ConversationOpen,
CreatedAtUnixMillis: millis,
UpdatedAtUnixMillis: millis,
}, nil
}
func NewHumanUtterance(conversation interaction.Conversation, request interaction.ChatMessageRequest, now time.Time) (interaction.HumanUtterance, interaction.OriginalHumanIntent, []interaction.InteractionRelation, error) {
message := strings.TrimSpace(request.Message)
if message == "" {
return interaction.HumanUtterance{}, interaction.OriginalHumanIntent{}, nil, ErrEmptyMessage
}
if len(message) > MaxHumanUtteranceBytes || !utf8.ValidString(message) {
return interaction.HumanUtterance{}, interaction.OriginalHumanIntent{}, nil, ErrMessageTooLarge
}
utteranceID, err := ids.New()
if err != nil {
return interaction.HumanUtterance{}, interaction.OriginalHumanIntent{}, nil, err
}
intentID, err := ids.New()
if err != nil {
return interaction.HumanUtterance{}, interaction.OriginalHumanIntent{}, nil, err
}
millis := now.UnixMilli()
speechAct := ClassifySpeechAct(message, request.CorrectsUtteranceID)
utterance := interaction.HumanUtterance{
ID: interaction.HumanUtteranceID(utteranceID),
ConversationID: conversation.ID,
ActorID: conversation.ActorID,
PrincipalID: conversation.PrincipalID,
Channel: interaction.ChannelWebChat,
ReceivedAtUnixMillis: millis,
CorrelationID: request.CorrelationID,
Content: message,
ContentRef: "interaction:utterance:" + string(utteranceID),
SpeechAct: speechAct,
InterpretationState: interaction.InterpretationInterpreted,
CorrectsUtteranceID: request.CorrectsUtteranceID,
}
intent := interaction.OriginalHumanIntent{
ID: interaction.OriginalHumanIntentID(intentID),
UtteranceID: utterance.ID,
RecordedAtUnixMillis: millis,
Text: message,
}
var relations []interaction.InteractionRelation
if request.CorrectsUtteranceID != "" || speechAct == interaction.SpeechActCorrection {
relationID, err := ids.New()
if err != nil {
return interaction.HumanUtterance{}, interaction.OriginalHumanIntent{}, nil, err
}
relations = append(relations, interaction.InteractionRelation{
ID: interaction.InteractionRelationID(relationID),
FromUtteranceID: utterance.ID,
ToUtteranceID: request.CorrectsUtteranceID,
Kind: interaction.RelationCorrects,
RecordedAtUnixMillis: millis,
})
}
return utterance, intent, relations, nil
}
func ClassifySpeechAct(message string, corrects interaction.HumanUtteranceID) interaction.SpeechAct {
lower := strings.ToLower(strings.TrimSpace(message))
switch {
case corrects != "" || strings.Contains(lower, "исправ") || strings.Contains(lower, "не так") || strings.HasPrefix(lower, "нет,"):
return interaction.SpeechActCorrection
case strings.Contains(lower, "?") || strings.HasPrefix(lower, "что ") || strings.HasPrefix(lower, "как ") || strings.HasPrefix(lower, "почему ") || strings.HasPrefix(lower, "кто ") || strings.Contains(lower, "объясни"):
return interaction.SpeechActQuestion
case strings.Contains(lower, "проверь") || strings.Contains(lower, "исслед") || strings.Contains(lower, "разбер"):
return interaction.SpeechActRequestToInvestigate
case strings.Contains(lower, "сделай") || strings.Contains(lower, "запусти") || strings.Contains(lower, "создай") || strings.Contains(lower, "настрой") || strings.Contains(lower, "добавь") || strings.Contains(lower, "измени") || strings.Contains(lower, "поменяй") || strings.Contains(lower, "используй") || strings.Contains(lower, "удали") || strings.Contains(lower, "удалить") || strings.Contains(lower, "напомни") || strings.Contains(lower, "подключи"):
return interaction.SpeechActDirective
case strings.HasPrefix(lower, "я решил") || strings.Contains(lower, "решение"):
return interaction.SpeechActDecision
case strings.Contains(lower, "предпочита") || strings.Contains(lower, "лучше"):
return interaction.SpeechActPreference
case strings.HasPrefix(lower, "думаю") || strings.Contains(lower, "гипотез"):
return interaction.SpeechActHypothesis
case lower == "да" || strings.HasPrefix(lower, "да,") || strings.Contains(lower, "подтверж"):
return interaction.SpeechActConfirmation
case lower == "нет" || strings.HasPrefix(lower, "нет,") || strings.Contains(lower, "отмен"):
return interaction.SpeechActRejection
case strings.Contains(lower, "привет") || strings.Contains(lower, "здравств"):
return interaction.SpeechActConversational
default:
return interaction.SpeechActAssertion
}
}
func NeedsCognition(act interaction.SpeechAct) bool {
return act == interaction.SpeechActQuestion || act == interaction.SpeechActRequestToInvestigate
}
func FallbackText(act interaction.SpeechAct) string {
switch act {
case interaction.SpeechActDirective:
return "Я понял действие как просьбу, но без отдельного Authority/Commit шага не выполняю мутацию."
case interaction.SpeechActAssertion:
return "Я записал это как утверждение человека. Это не считается проверенным фактом."
case interaction.SpeechActCorrection:
return "Исправление записано отдельной репликой; история не переписана."
case interaction.SpeechActQuestion, interaction.SpeechActRequestToInvestigate:
return "Когнитивный маршрут сейчас недоступен, но сообщение сохранено и может быть обработано позже."
default:
return "Сообщение записано."
}
}