Semantic-first Domovoy chat routing and reflex drift audit #270
@@ -5,8 +5,10 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
@@ -61,12 +63,53 @@ func (c cognitiveInterpretationClient) Interpret(ctx context.Context, request co
|
||||
}
|
||||
|
||||
func shouldRequestBroaderInterpretation(result interactiondomain.SemanticResult) bool {
|
||||
switch result.Interpretation.Applicability {
|
||||
case interaction.SemanticNeedsBroaderInterpret, interaction.SemanticLowConfidence, interaction.SemanticMixedIntent:
|
||||
return true
|
||||
default:
|
||||
return shouldRequestSemanticInterpretation(result)
|
||||
}
|
||||
|
||||
// shouldRequestSemanticInterpretation keeps the deterministic layer as a
|
||||
// fast reflex, not as the meaning authority. Questions, requests, directives,
|
||||
// mixed signals and capability-bearing messages go through cognitive
|
||||
// interpretation; only stable low-risk conversational/assertive reflexes may
|
||||
// answer without it.
|
||||
func shouldRequestSemanticInterpretation(result interactiondomain.SemanticResult) bool {
|
||||
if result.Interpretation.Clarification == interaction.ClarificationRequired {
|
||||
return false
|
||||
}
|
||||
if result.NeedsCognition || result.InformationRequestKind != "" || len(result.CapabilityHints) > 0 {
|
||||
return true
|
||||
}
|
||||
if result.Interpretation.Applicability != interaction.SemanticApplicable {
|
||||
return true
|
||||
}
|
||||
switch result.Interpretation.SpeechAct {
|
||||
case interaction.SpeechActConversational, interaction.SpeechActConfirmation, interaction.SpeechActRejection:
|
||||
return false
|
||||
case interaction.SpeechActAssertion:
|
||||
return result.Interpretation.ConfidenceMillis < 850
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
deterministicReflexAuditInterval = 10 * time.Minute
|
||||
deterministicReflexAuditSample = uint32(16)
|
||||
)
|
||||
|
||||
// shouldAuditDeterministicReflex samples stable reflexes in a time bucket.
|
||||
// This is observability, not a correctness gate: a failed audit leaves the
|
||||
// reflex usable, while a successful disagreement can replace it with the
|
||||
// cognitive candidate and becomes durable evidence through the normal
|
||||
// candidate store.
|
||||
func shouldAuditDeterministicReflex(result interactiondomain.SemanticResult, utteranceID string, now time.Time) bool {
|
||||
if shouldRequestSemanticInterpretation(result) || utteranceID == "" {
|
||||
return false
|
||||
}
|
||||
bucket := now.Unix() / int64(deterministicReflexAuditInterval/time.Second)
|
||||
hash := fnv.New32a()
|
||||
_, _ = hash.Write([]byte(utteranceID))
|
||||
_, _ = hash.Write([]byte(fmt.Sprintf(":%d", bucket)))
|
||||
return hash.Sum32()%deterministicReflexAuditSample == 0
|
||||
}
|
||||
|
||||
func isSimpleLocalQuestion(result interactiondomain.SemanticResult) bool {
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
||||
@@ -34,3 +35,42 @@ func TestOperationalQuestionStillUsesStrictInterpretation(t *testing.T) {
|
||||
t.Fatal("operational question must retain strict interpretation routing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectiveAndInventoryPreferMeaningInterpretationEvenWhenDeterministicLooksApplicable(t *testing.T) {
|
||||
result := interactiondomain.SemanticResult{
|
||||
Interpretation: interaction.UtteranceInterpretation{
|
||||
SpeechAct: interaction.SpeechActDirective,
|
||||
Applicability: interaction.SemanticApplicable,
|
||||
ConfidenceMillis: 980,
|
||||
},
|
||||
InformationRequestKind: interaction.InformationRequestCognitiveModelInventory,
|
||||
}
|
||||
if !shouldRequestSemanticInterpretation(result) {
|
||||
t.Fatal("model inventory directive must be interpreted by meaning, not only token reflex")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStableReflexIsPeriodicallySampledForDrift(t *testing.T) {
|
||||
result := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{
|
||||
SpeechAct: interaction.SpeechActConversational, Applicability: interaction.SemanticApplicable,
|
||||
}}
|
||||
found := false
|
||||
for minute := 0; minute < 160; minute++ {
|
||||
if shouldAuditDeterministicReflex(result, "utterance-drift-probe", time.Unix(int64(minute)*60, 0)) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("stable reflex was never sampled during the audit window")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeaningfulSignalsAreNotReflexOnly(t *testing.T) {
|
||||
for _, act := range []interaction.SpeechAct{interaction.SpeechActQuestion, interaction.SpeechActRequestToInvestigate, interaction.SpeechActDirective} {
|
||||
result := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{SpeechAct: act, Applicability: interaction.SemanticApplicable}}
|
||||
if !shouldRequestSemanticInterpretation(result) {
|
||||
t.Fatalf("%s must prefer cognitive interpretation", act)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,7 +514,9 @@ func main() {
|
||||
}
|
||||
cognitiveAttempted := false
|
||||
cognitiveAvailable := true
|
||||
if shouldRequestBroaderInterpretation(semantic) {
|
||||
semanticInterpretationRequested := shouldRequestSemanticInterpretation(semantic)
|
||||
semanticReflexAudit := shouldAuditDeterministicReflex(semantic, string(utterance.ID), now)
|
||||
if semanticInterpretationRequested || semanticReflexAudit {
|
||||
cognitiveAttempted = true
|
||||
projectNames := make([]string, 0, len(semantic.Projects))
|
||||
for _, ref := range semantic.Projects {
|
||||
|
||||
@@ -102,8 +102,11 @@ services:
|
||||
LISTEN_ADDR: ":8083"
|
||||
CORE_RUNTIME_URL: http://core-runtime:8080
|
||||
LOCAL_CPU_PROVIDER_URL: http://docker.cin.su:18082/v1
|
||||
LOCAL_MEDIUM_PROVIDER_URL: ${LOCAL_MEDIUM_PROVIDER_URL:-http://docker.cin.su:18082/v1}
|
||||
LOCAL_MEDIUM_PROVIDER_TIMEOUT: ${LOCAL_MEDIUM_PROVIDER_TIMEOUT:-120s}
|
||||
LOCAL_LIFEBOAT_PROVIDER_URL: ${LOCAL_LIFEBOAT_PROVIDER_URL:-}
|
||||
LOCAL_GPU_PROVIDER_URL: http://192.168.220.91:11434
|
||||
COGNITIVE_ALLOW_CANARY: "true"
|
||||
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
|
||||
ports: ["19083:8083"]
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000291-mobile-acceptance-evidence.json",
|
||||
"task_id": "DEV-000291",
|
||||
"task_file": "dev/tasks/DEV-000292-development-mode-runtime-access.json",
|
||||
"task_id": "DEV-000292",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"task_id": "DEV-000292",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "HEAD",
|
||||
"risk_tier": "E",
|
||||
"intent": "Provide an explicitly isolated development/test runtime profile for Domovoy so local model, speech, image, OCR, Docker and diagnostic paths are usable without changing production behavior.",
|
||||
"requirement_ids": ["isolated development runtime", "local multimodal model access", "development diagnostics"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["interaction", "core-runtime", "cognitive-worker", "creator-web", "deployment", "development-governance"],
|
||||
"paths": [
|
||||
"cmd/cognitive-worker/**",
|
||||
"cmd/core-runtime/**",
|
||||
"adapters/models/**",
|
||||
"contracts/cognitive/**",
|
||||
"modules/interaction/domain/**",
|
||||
"deploy/docker-compose.dev.yml",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000292-development-mode-runtime-access.json",
|
||||
"docs/evidence/**",
|
||||
"docs/PROJECT_STATUS.md"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/**", "security/**", "database/**", "deploy/docker-compose.prod.yml", "LLM-local/**"]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 1,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": ["existing cognitive gateway", "existing local model HTTP APIs", "existing dev compose network"],
|
||||
"may_change": ["dev-only modality and diagnostic wiring; no production contract change"]
|
||||
},
|
||||
"semantic_owner": "development-governance owns the isolated dev profile; cognitive-worker owns model capability routing",
|
||||
"canonical_state": ["existing dev databases and Docker volumes only"],
|
||||
"state_transitions": ["production-only restriction -> explicit dev-only capability switch"],
|
||||
"side_effects_and_privileges": ["dev network and Docker diagnostics only", "no production credentials", "no secret logging", "no production external mutation"],
|
||||
"persistence_and_migrations": ["dev volumes only; no production migration"],
|
||||
"failure_retry_restart": ["dev services remain independently restartable", "unsupported modality stays explicitly unavailable"],
|
||||
"assumptions": ["the developer starts the profile separately from production", "test credentials are disposable and never reused in production"],
|
||||
"acceptance_scenarios": ["dev profile starts independently", "text, speech, image and OCR routes are discoverable or report a precise unavailable state", "diagnostic endpoints are reachable inside the dev network", "meaningful chat requests prefer cognitive interpretation over token reflexes", "stable reflexes are sampled periodically for cognitive drift and a disagreement is observable as a cognitive candidate", "production compose remains unchanged", "no secret value appears in source or logs"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl independent", "docker compose -f deploy/docker-compose.dev.yml config", "git diff --check", "dev runtime health smoke"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_MISSING_CAPABILITY"]
|
||||
}
|
||||
@@ -215,7 +215,7 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo
|
||||
conversationWords := []string{"привет", "здравствуйте", "спасибо", "ку", "здравствуй"}
|
||||
investigateStems := []string{"разбер", "проверь", "проанализ", "исслед", "выясн", "подум", "понят", "посмотри"}
|
||||
directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "спрос", "посчитай", "рассчитай", "ответь"}
|
||||
explanatoryRequestStems := []string{"расска", "раска", "объяс", "опиш", "покаж", "подскаж"}
|
||||
explanatoryRequestStems := []string{"расска", "раска", "объяс", "опиш", "покаж", "подскаж", "сообщ"}
|
||||
preferenceStems := []string{"предпоч", "хочу", "лучше", "не используй"}
|
||||
hypothesisStems := []string{"думаю", "кажется", "гипотез", "похоже"}
|
||||
decisionStems := []string{"решил", "решение", "выбираю"}
|
||||
@@ -471,7 +471,7 @@ func informationRequestKind(message string, candidate interaction.SemanticCandid
|
||||
inquiry := candidate.PrimarySpeechAct == interaction.SpeechActQuestion || candidate.PrimarySpeechAct == interaction.SpeechActRequestToInvestigate || candidate.SecondarySpeechAct == interaction.SpeechActQuestion || candidate.SecondarySpeechAct == interaction.SpeechActRequestToInvestigate
|
||||
cognitiveSubject := containsStemInTokens(tokens, "модел", "ии", "ai", "локальн", "когнитив", "канал")
|
||||
inventoryCue := containsStemInTokens(tokens, "список", "доступ", "подключ", "вариант") || (containsMeaningfulInterrogative(tokens) && containsStemInTokens(tokens, "настро"))
|
||||
inventoryRequest := inquiry || containsStemInTokens(tokens, "напиш", "покаж", "перечисл")
|
||||
inventoryRequest := inquiry || containsStemInTokens(tokens, "напиш", "покаж", "перечисл", "сообщ", "вывед", "предостав")
|
||||
if cognitiveSubject && inventoryRequest && inventoryCue {
|
||||
return interaction.InformationRequestCognitiveModelInventory
|
||||
}
|
||||
|
||||
@@ -121,6 +121,8 @@ func TestInformationRequestsAreClassifiedOnceByInteraction(t *testing.T) {
|
||||
{"что ты умеешь?", interaction.InformationRequestCapabilityInventory},
|
||||
{"проверь состояние локального канала", interaction.InformationRequestCognitiveChannelStatus},
|
||||
{"нет связи с ИИ, восстанови канал", interaction.InformationRequestCognitiveRecovery},
|
||||
{"сообщии список моделей", interaction.InformationRequestCognitiveModelInventory},
|
||||
{"сообщите список подключённых моделей", interaction.InformationRequestCognitiveModelInventory},
|
||||
}
|
||||
for _, test := range cases {
|
||||
result, err := InterpretUtterance(interaction.HumanUtterance{Content: test.message}, nil, nil, time.Unix(10, 0))
|
||||
|
||||
Reference in New Issue
Block a user