Files
Assistant/modules/interaction/domain/task_decomposition.go
T
m 3515a483ca
CI / test (push) Successful in 1m29s
CI / test (pull_request) Successful in 1m28s
Fix generic creative target extraction
2026-08-25 14:49:01 +03:00

111 lines
3.1 KiB
Go

package domain
import "strings"
// TaskDecomposition is the small, typed intermediate representation used
// before a capability is called. It separates the user's goal from the
// object, source and target instead of treating the whole sentence as a
// command string.
type TaskDecomposition struct {
Intent string
Object string
Source string
Target string
Qualifiers []string
Missing []string
NeedsResearch bool
}
type TaskLexiconEntry struct {
Alias string
Value string
}
type TaskLexicon struct {
ObjectAliases []TaskLexiconEntry
SourceAliases []TaskLexiconEntry
}
const (
TaskIntentSearch = "SEARCH"
TaskIntentCreate = "CREATE"
)
// DecomposeTask performs the structural pass without assuming a mutable
// world vocabulary. Learned names, aliases and source meanings are supplied
// by memory through DecomposeTaskWithLexicon.
func DecomposeTask(message string) TaskDecomposition {
return DecomposeTaskWithLexicon(message, TaskLexicon{})
}
// DecomposeTaskWithLexicon performs a cheap semantic pass. The lexicon is
// intentionally an input: a remembered alias may be revalidated or replaced
// without changing the parser or shipping a new hard-coded branch.
func DecomposeTaskWithLexicon(message string, lexicon TaskLexicon) TaskDecomposition {
lower := strings.ToLower(strings.TrimSpace(message))
tokens := semanticTokens(message)
result := TaskDecomposition{}
if containsStemInTokens(tokens, "найд", "поищ", "подбер", "проверь") {
result.Intent = TaskIntentSearch
}
if containsStemInTokens(tokens, "нарис", "рису", "изобраз", "создай") {
result.Intent = TaskIntentCreate
}
for _, entry := range lexicon.ObjectAliases {
if containsStemInTokens(tokens, entry.Alias) {
result.Object = entry.Value
break
}
}
for _, entry := range lexicon.SourceAliases {
if containsStemInTokens(tokens, entry.Alias) {
result.Source = entry.Value
break
}
}
if result.Source != "" {
result.NeedsResearch = result.Intent == TaskIntentSearch
}
if result.Intent == TaskIntentCreate {
result.Target = valueAfterAnyCue(lower, "нарисуй", "нарисовать", "изобрази", "создай")
}
if result.Target == "" && result.Object != "" {
for _, entry := range lexicon.ObjectAliases {
if entry.Value == result.Object {
result.Target = valueAfterCue(lower, entry.Alias)
break
}
}
}
if result.Intent == TaskIntentSearch && result.Source == "" {
result.Missing = append(result.Missing, "SOURCE_OR_SEARCH_SCOPE")
}
if result.Intent == TaskIntentSearch && result.Target == "" {
result.Missing = append(result.Missing, "TARGET")
}
return result
}
func valueAfterAnyCue(message string, cues ...string) string {
for _, cue := range cues {
if value := valueAfterCue(message, cue); value != "" {
return value
}
}
return ""
}
func valueAfterCue(message, cue string) string {
index := strings.Index(message, cue)
if index < 0 {
return ""
}
value := strings.TrimSpace(message[index+len(cue):])
value = strings.Trim(value, " \t:,-")
if end := strings.IndexAny(value, "?!,.;"); end >= 0 {
value = value[:end]
}
return strings.TrimSpace(value)
}