DEV-000061 checkpoint interaction chat foundation
CI / test (push) Failing after 10s
CI / test (pull_request) Failing after 11s

This commit is contained in:
m
2026-08-17 14:10:04 +03:00
parent e4b6121dc0
commit 41154be211
26 changed files with 1376 additions and 8 deletions
+144
View File
@@ -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 "Сообщение записано."
}
}
+44
View File
@@ -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")
}
}
+2
View File
@@ -0,0 +1,2 @@
// Package interaction owns durable human interaction semantics.
package interaction