diff --git a/cmd/cognitive-worker/interpretation.go b/cmd/cognitive-worker/interpretation.go index 3882401..1123d67 100644 --- a/cmd/cognitive-worker/interpretation.go +++ b/cmd/cognitive-worker/interpretation.go @@ -80,20 +80,59 @@ func (g cognitiveGateway) interpretationHandler(w http.ResponseWriter, r *http.R func (g cognitiveGateway) interpretWithBuiltin(ctx context.Context, request cognitive.GatewayInterpretationRequest, maxOutputBytes int) cognitive.GatewayInterpretationResponse { if g.builtinLocal == nil || g.builtinLocal.Provider == nil { - return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable} + return deterministicInterpretationResponse(request) } invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: interpretationPrompt(request), Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ModelDescriptorID: "builtin-local-model-v1", ProviderModelID: "domovoy-lifeboat"} response, err := g.builtinLocal.Provider.Advise(ctx, invocation) if err != nil || response.FinishReason != cognitive.FinishCompleted { - return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable} + return deterministicInterpretationResponse(request) } candidate, ok := parseInterpretationCandidate(response.AdvisoryText) if !ok { - return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishInvalidStructuredOutput, Usage: response.Usage} + return deterministicInterpretationResponse(request) } return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage} } +// deterministicInterpretationResponse keeps the semantic route alive when the +// local model cannot produce strict JSON in time. It does not invent meaning: +// it carries forward the already-recorded deterministic interpretation and +// leaves the advisory answer to the built-in gateway path. +func deterministicInterpretationResponse(request cognitive.GatewayInterpretationRequest) cognitive.GatewayInterpretationResponse { + speechAct := request.DeterministicSpeechAct + if !validSpeechAct(speechAct) { + speechAct = "UNKNOWN" + } + applicability := request.DeterministicApplicability + switch applicability { + case cognitive.InterpretationApplicable, cognitive.InterpretationLowConfidence, cognitive.InterpretationMixedIntent, cognitive.InterpretationNeedsBroader, cognitive.InterpretationClarificationRequired: + default: + applicability = cognitive.InterpretationLowConfidence + } + confidence := request.DeterministicConfidence + if confidence < 0 { + confidence = 0 + } + if confidence > 1000 { + confidence = 1000 + } + candidate := cognitive.GatewayInterpretationCandidate{ + PrimarySpeechAct: speechAct, + SecondarySpeechAct: request.DeterministicSecondary, + ConfidenceMillis: confidence, + Applicability: applicability, + RequestedOutcome: request.RequestedOutcome, + NeedsCognition: true, + Clarification: "NOT_REQUIRED", + SelectionReason: "deterministic interpretation retained; built-in structured interpretation was unavailable", + } + if applicability == cognitive.InterpretationClarificationRequired { + candidate.NeedsCognition = false + candidate.Clarification = "REQUIRED" + } + return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted} +} + func selectionObjectiveForInterpretation(request cognitive.GatewayInterpretationRequest) capability.SelectionObjective { if request.RequestedOutcome != "" || request.DeterministicApplicability == cognitive.InterpretationNeedsBroader || request.DeterministicApplicability == cognitive.InterpretationMixedIntent || request.DeterministicApplicability == cognitive.InterpretationLowConfidence { return capability.SelectionQuality diff --git a/cmd/cognitive-worker/interpretation_test.go b/cmd/cognitive-worker/interpretation_test.go new file mode 100644 index 0000000..3183b53 --- /dev/null +++ b/cmd/cognitive-worker/interpretation_test.go @@ -0,0 +1,26 @@ +package main + +import ( + "context" + "testing" + + "git.cin.su/m/Assistant/contracts/cognitive" +) + +func TestBuiltinInterpretationTimeoutRetainsDeterministicCandidate(t *testing.T) { + request := cognitive.GatewayInterpretationRequest{ + Version: cognitive.CreatorAdvisoryV1, + RequestID: "request-1", + DeterministicSpeechAct: "QUESTION", + DeterministicConfidence: 720, + DeterministicApplicability: cognitive.InterpretationNeedsBroader, + RequestedOutcome: "вывести список настроек", + } + response := (cognitiveGateway{}).interpretWithBuiltin(context.Background(), request, 4096) + if response.FinishReason != cognitive.FinishCompleted { + t.Fatalf("finish reason=%q", response.FinishReason) + } + if response.Candidate.PrimarySpeechAct != "QUESTION" || response.Candidate.ConfidenceMillis != 720 || response.Candidate.RequestedOutcome != request.RequestedOutcome { + t.Fatalf("candidate did not retain deterministic meaning: %+v", response.Candidate) + } +} diff --git a/cmd/core-runtime/cognitive_interpretation.go b/cmd/core-runtime/cognitive_interpretation.go index 9661eb4..8c7f751 100644 --- a/cmd/core-runtime/cognitive_interpretation.go +++ b/cmd/core-runtime/cognitive_interpretation.go @@ -61,6 +61,13 @@ func (c cognitiveInterpretationClient) Interpret(ctx context.Context, request co } func shouldRequestBroaderInterpretation(result interactiondomain.SemanticResult) bool { + // The built-in advisory channel is the default conversational path. A + // simple question must not first wait for the slower, strict JSON + // interpretation route: that route exists for ambiguity and work routing, + // not as a prerequisite for talking to Domovoy. + if isSimpleLocalQuestion(result) { + return false + } switch result.Interpretation.Applicability { case interaction.SemanticNeedsBroaderInterpret, interaction.SemanticLowConfidence, interaction.SemanticMixedIntent: return true @@ -69,6 +76,19 @@ func shouldRequestBroaderInterpretation(result interactiondomain.SemanticResult) } } +func isSimpleLocalQuestion(result interactiondomain.SemanticResult) bool { + if result.Interpretation.SpeechAct != interaction.SpeechActQuestion || result.Interpretation.Clarification == interaction.ClarificationRequired { + return false + } + for _, hint := range result.CapabilityHints { + switch hint { + case "MODIFY_CODE", "RUN_TESTS", "SEND_EMAIL", "SCHEDULE_REMINDER", "READ_DATABASE", "QUERY_LOGS": + return false + } + } + return true +} + func buildGatewayInterpretationRequest(session identity.CreatorSession, utterance interaction.HumanUtterance, semantic interactiondomain.SemanticResult, projectNames []string) cognitive.GatewayInterpretationRequest { requestID, _ := ids.New() return cognitive.GatewayInterpretationRequest{ diff --git a/cmd/core-runtime/cognitive_interpretation_test.go b/cmd/core-runtime/cognitive_interpretation_test.go new file mode 100644 index 0000000..b530085 --- /dev/null +++ b/cmd/core-runtime/cognitive_interpretation_test.go @@ -0,0 +1,36 @@ +package main + +import ( + "testing" + + "git.cin.su/m/Assistant/contracts/interaction" + interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain" +) + +func TestSimpleQuestionUsesBuiltInAdvisoryWithoutStrictInterpretation(t *testing.T) { + result := interactiondomain.SemanticResult{ + Interpretation: interaction.UtteranceInterpretation{ + SpeechAct: interaction.SpeechActQuestion, + Applicability: interaction.SemanticNeedsBroaderInterpret, + Clarification: interaction.ClarificationNotRequired, + }, + CapabilityHints: []string{"COGNITIVE_REASONING", "VERIFY_FACT"}, + } + if shouldRequestBroaderInterpretation(result) { + t.Fatal("simple question must go directly to the built-in advisory path") + } +} + +func TestOperationalQuestionStillUsesStrictInterpretation(t *testing.T) { + result := interactiondomain.SemanticResult{ + Interpretation: interaction.UtteranceInterpretation{ + SpeechAct: interaction.SpeechActQuestion, + Applicability: interaction.SemanticNeedsBroaderInterpret, + Clarification: interaction.ClarificationNotRequired, + }, + CapabilityHints: []string{"MODIFY_CODE", "RUN_TESTS"}, + } + if !shouldRequestBroaderInterpretation(result) { + t.Fatal("operational question must retain strict interpretation routing") + } +} diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index b9f7960..d0968e5 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000090-chat-runtime-error-recovery.json", - "task_id": "DEV-000090", + "task_file": "dev/tasks/DEV-000091-builtin-local-chat-routing.json", + "task_id": "DEV-000091", "task_revision": 1 } diff --git a/dev/tasks/DEV-000091-builtin-local-chat-routing.json b/dev/tasks/DEV-000091-builtin-local-chat-routing.json new file mode 100644 index 0000000..bb130da --- /dev/null +++ b/dev/tasks/DEV-000091-builtin-local-chat-routing.json @@ -0,0 +1,33 @@ +{ + "task_id": "DEV-000091", + "task_revision": 1, + "architecture_revision": "DEV-000073-baseline", + "base_repo_revision": "a6059cc9c6a391152f0981ee2ae8eccee620794d", + "risk_tier": "E", + "intent": "Keep ordinary conversation and simple informational questions available through Domovoy's built-in local channel without waiting for strict cognitive interpretation, while preserving deterministic semantics when local structured interpretation times out.", + "requirement_ids": ["Built-in local channel is always usable", "Simple questions do not depend on external bindings", "Interpretation timeout does not become runtime outage"], + "allowed_change_scope": { + "modules": ["core-runtime", "cognitive-worker", "development-governance"], + "paths": [ + "cmd/core-runtime/cognitive_interpretation.go", + "cmd/core-runtime/cognitive_interpretation_test.go", + "cmd/cognitive-worker/interpretation.go", + "cmd/cognitive-worker/interpretation_test.go", + "dev/ACTIVE_TASK.json", + "dev/tasks/DEV-000091-builtin-local-chat-routing.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}, + "contracts": {"consume": ["existing cognitive interpretation HTTP contract", "existing built-in advisory gateway"], "may_change": []}, + "semantic_owner": "interaction", + "canonical_state": [], + "state_transitions": ["simple informational question -> built-in advisory path", "local structured interpretation timeout -> deterministic candidate retained"], + "side_effects_and_privileges": ["No new privilege", "No direct provider calls outside cognitive gateway"], + "persistence_and_migrations": ["No persistence or migration"], + "failure_retry_restart": ["Interpretation remains best-effort and bounded", "Built-in advisory remains the local fallback"], + "assumptions": ["Creator Web invokes advisory for a question whose core response retains NeedsCognition"], + "acceptance_scenarios": ["A simple Russian question reaches the built-in advisory without /v1/interpretations", "A timed-out built-in interpretation returns a completed deterministic candidate", "Investigative or ambiguous requests still use strict interpretation"], + "required_tests": ["go test ./cmd/core-runtime", "go test ./cmd/cognitive-worker", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "git diff --check"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"] +}