Files
2026-08-29 23:28:10 +03:00

402 lines
20 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}