DEV-000061 checkpoint interaction chat foundation
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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, "не так"):
|
||||
return interaction.SpeechActCorrection
|
||||
case strings.Contains(lower, "?") || strings.HasPrefix(lower, "что ") || strings.HasPrefix(lower, "как ") || strings.HasPrefix(lower, "почему ") || strings.HasPrefix(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, "настрой"):
|
||||
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 strings.Contains(lower, "да") || strings.Contains(lower, "подтверж"):
|
||||
return interaction.SpeechActConfirmation
|
||||
case strings.Contains(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 "Сообщение записано."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
)
|
||||
|
||||
func TestNewHumanUtteranceClassifiesQuestionAndRequiresCognition(t *testing.T) {
|
||||
conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen}
|
||||
utterance, intent, relations, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: "Кто ты?"}, time.Unix(10, 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if utterance.SpeechAct != interaction.SpeechActQuestion || !NeedsCognition(utterance.SpeechAct) {
|
||||
t.Fatalf("expected cognitive question, got %s", utterance.SpeechAct)
|
||||
}
|
||||
if intent.Text != "Кто ты?" || len(relations) != 0 {
|
||||
t.Fatalf("unexpected intent or relations")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHumanUtteranceAppendsCorrectionRelation(t *testing.T) {
|
||||
conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen}
|
||||
utterance, _, relations, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: "исправь прошлое", CorrectsUtteranceID: "utterance-0"}, time.Unix(10, 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if utterance.SpeechAct != interaction.SpeechActCorrection {
|
||||
t.Fatalf("expected correction, got %s", utterance.SpeechAct)
|
||||
}
|
||||
if len(relations) != 1 || relations[0].Kind != interaction.RelationCorrects || relations[0].ToUtteranceID != "utterance-0" {
|
||||
t.Fatalf("unexpected correction relation: %#v", relations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConversationRequiresActorAndPrincipal(t *testing.T) {
|
||||
_, err := NewConversation(identity.ActorID(""), identity.PrincipalID("principal-1"), time.Unix(10, 0))
|
||||
if err == nil {
|
||||
t.Fatal("expected invalid principal")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package interaction owns durable human interaction semantics.
|
||||
package interaction
|
||||
Reference in New Issue
Block a user