diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index f2da7bf..b7a9c6b 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -394,7 +394,7 @@ func main() { } needsCognition = false } - if isModelInventoryQuestion(utterance.Content) { + if isModelInventoryQuestion(semantic) { candidates, inventoryErr := capabilityRepository.LoadCognitiveCandidates(r.Context(), capability.CapabilityID("creator-advisory"), session.PrincipalID) planningReply = humanCognitiveModelInventory(candidates, inventoryErr) needsCognition = false @@ -1158,14 +1158,8 @@ func isCapabilityQuestion(message string) bool { return strings.Contains(lower, "что ты умеешь") || strings.Contains(lower, "что умеешь") || strings.Contains(lower, "какие возможности") } -func isModelInventoryQuestion(message string) bool { - lower := strings.ToLower(strings.TrimSpace(message)) - if lower == "" { - return false - } - asksAboutModels := strings.Contains(lower, "модел") || strings.Contains(lower, "ии") && (strings.Contains(lower, "внешн") || strings.Contains(lower, "настро") || strings.Contains(lower, "подключ")) - asksForInventory := strings.Contains(lower, "какие") || strings.Contains(lower, "какая") || strings.Contains(lower, "список") || strings.Contains(lower, "доступ") || strings.Contains(lower, "настро") - return asksAboutModels && asksForInventory +func isModelInventoryQuestion(result interactiondomain.SemanticResult) bool { + return result.InformationRequestKind == interactiondomain.InformationRequestCognitiveModelInventory } func humanCognitiveModelInventory(candidates []capability.ResolvedCognitiveBinding, inventoryErr error) string { diff --git a/cmd/core-runtime/main_test.go b/cmd/core-runtime/main_test.go index b8e50b1..0af9bea 100644 --- a/cmd/core-runtime/main_test.go +++ b/cmd/core-runtime/main_test.go @@ -11,6 +11,8 @@ import ( "time" "git.cin.su/m/Assistant/contracts/capability" + "git.cin.su/m/Assistant/contracts/interaction" + interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain" ) func TestBearerTokenRequiresBearerScheme(t *testing.T) { @@ -51,9 +53,20 @@ func TestCognitiveDegradedReplyKeepsDomovoyAlive(t *testing.T) { func TestModelInventoryQuestionUsesReadOnlyFastPath(t *testing.T) { for _, message := range []string{"какие модели доступны?", "напиши список настроек ИИ", "какие есть подключения ИИ"} { - if !isModelInventoryQuestion(message) { - t.Fatalf("message %q was not recognized as model inventory question", message) + result, err := interactiondomain.InterpretUtterance(interaction.HumanUtterance{Content: message}, nil, nil, time.Now()) + if err != nil { + t.Fatalf("interpretation failed for %q: %v", message, err) } + if !isModelInventoryQuestion(result) { + t.Fatalf("semantic inventory request %q was not recognized: kind=%q act=%q hints=%v", message, result.InformationRequestKind, result.Interpretation.SpeechAct, result.CapabilityHints) + } + } + ordinary, err := interactiondomain.InterpretUtterance(interaction.HumanUtterance{Content: "настрой локальную модель"}, nil, nil, time.Now()) + if err != nil { + t.Fatal(err) + } + if isModelInventoryQuestion(ordinary) { + t.Fatal("configuration mutation must not use read-only model inventory path") } } diff --git a/dev/tasks/DEV-000092-cognitive-model-inventory-fast-path.json b/dev/tasks/DEV-000092-cognitive-model-inventory-fast-path.json index 5d4a697..106d6c2 100644 --- a/dev/tasks/DEV-000092-cognitive-model-inventory-fast-path.json +++ b/dev/tasks/DEV-000092-cognitive-model-inventory-fast-path.json @@ -4,11 +4,11 @@ "architecture_revision": "DEV-000073-baseline", "base_repo_revision": "2306c8d6b31fbf5ac0933ecc72b4f76bec7218f2", "risk_tier": "E", - "intent": "Answer model and external-AI settings questions from the capability registry through a deterministic read-only chat fast path instead of treating them as unavailable cognition.", + "intent": "Answer semantically identified model and external-AI inventory requests from the capability registry through a deterministic read-only chat fast path instead of treating them as unavailable cognition.", "requirement_ids": ["Model inventory is readable from chat", "No secret material is shown", "Inventory does not require model reasoning"], "allowed_change_scope": { - "modules": ["core-runtime", "development-governance"], - "paths": ["cmd/core-runtime/main.go", "cmd/core-runtime/main_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000092-cognitive-model-inventory-fast-path.json"] + "modules": ["core-runtime", "interaction", "development-governance"], + "paths": ["cmd/core-runtime/main.go", "cmd/core-runtime/main_test.go", "modules/interaction/domain/chat.go", "modules/interaction/domain/chat_test.go", "modules/interaction/domain/semantics.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000092-cognitive-model-inventory-fast-path.json"] }, "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**"]}, "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0}, diff --git a/modules/interaction/domain/chat.go b/modules/interaction/domain/chat.go index e0868ea..327d01c 100644 --- a/modules/interaction/domain/chat.go +++ b/modules/interaction/domain/chat.go @@ -194,6 +194,9 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo if hasQuestionMark { scores[interaction.SpeechActQuestion]++ } + if containsMeaningfulInterrogative(tokens) { + scores[interaction.SpeechActQuestion] += 2 + } if explicitCorrection { scores[interaction.SpeechActCorrection] += 6 } @@ -432,6 +435,9 @@ func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []strin if containsToken(tokens, "свободно") || containsToken(tokens, "гб") || containsToken(tokens, "модель") { add("VERIFY_FACT") } + if containsStemInTokens(tokens, "модел", "ии") && containsStemInTokens(tokens, "настро", "список", "доступ", "подключ") { + add("VERIFY_FACT") + } values := make([]string, 0, len(seen)) for kind := range seen { values = append(values, kind) @@ -439,6 +445,18 @@ func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []strin return values } +func informationRequestKind(message string) InformationRequestKind { + tokens := semanticTokens(message) + if len(tokens) == 0 || containsStem(tokens[0], "настро") || !containsStemInTokens(tokens, "модел", "ии") { + return "" + } + inventoryCue := containsStemInTokens(tokens, "список", "доступ", "подключ") || (containsMeaningfulInterrogative(tokens) && containsStemInTokens(tokens, "настро")) + if !inventoryCue { + return "" + } + return InformationRequestCognitiveModelInventory +} + func boundedConfidence(primaryScore, secondaryScore int, scores map[interaction.SpeechAct]int) int64 { total := 0 for _, score := range scores { diff --git a/modules/interaction/domain/semantics.go b/modules/interaction/domain/semantics.go index 37cf4c7..249bed9 100644 --- a/modules/interaction/domain/semantics.go +++ b/modules/interaction/domain/semantics.go @@ -11,16 +11,21 @@ import ( ) type SemanticResult struct { - Interpretation interaction.UtteranceInterpretation - Candidates []interaction.SemanticCandidate - Projects []interaction.ProjectReference - Relations []interaction.InteractionRelation - ActiveFocus *situation.ActiveProjectFocus - Reply string - NeedsCognition bool - CapabilityHints []string + Interpretation interaction.UtteranceInterpretation + Candidates []interaction.SemanticCandidate + Projects []interaction.ProjectReference + Relations []interaction.InteractionRelation + ActiveFocus *situation.ActiveProjectFocus + Reply string + NeedsCognition bool + CapabilityHints []string + InformationRequestKind InformationRequestKind } +type InformationRequestKind string + +const InformationRequestCognitiveModelInventory InformationRequestKind = "COGNITIVE_MODEL_INVENTORY" + func InterpretUtterance(utterance interaction.HumanUtterance, projects []situation.ProjectDescriptor, previousFocus *situation.ActiveProjectFocus, now time.Time) (SemanticResult, error) { interpretationID, err := ids.New() if err != nil { @@ -46,9 +51,10 @@ func InterpretUtterance(utterance interaction.HumanUtterance, projects []situati SelectionReason: selectedCandidate.SelectionReason, RecordedAtUnixMillis: millis, }, - Candidates: []interaction.SemanticCandidate{deterministicCandidate}, - NeedsCognition: selectedCandidate.NeedsCognition, - CapabilityHints: append([]string(nil), selectedCandidate.CapabilityHints...), + Candidates: []interaction.SemanticCandidate{deterministicCandidate}, + NeedsCognition: selectedCandidate.NeedsCognition, + CapabilityHints: append([]string(nil), selectedCandidate.CapabilityHints...), + InformationRequestKind: informationRequestKind(utterance.Content), } explicitFocus := explicitProjectFocus(utterance.Content, projects) projectRefs, err := projectReferences(utterance, projects, explicitFocus, previousFocus, millis)