From fba05d5ed43ca890c6d8d3f56680feb4f8c22263 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 26 Aug 2026 08:59:49 +0300 Subject: [PATCH] Fallback to local interpretation when provider fails --- cmd/cognitive-worker/interpretation.go | 10 ++++-- cmd/cognitive-worker/interpretation_test.go | 35 +++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/cmd/cognitive-worker/interpretation.go b/cmd/cognitive-worker/interpretation.go index baeafb3..5cb19dc 100644 --- a/cmd/cognitive-worker/interpretation.go +++ b/cmd/cognitive-worker/interpretation.go @@ -67,12 +67,18 @@ func (g cognitiveGateway) interpretationHandler(w http.ResponseWriter, r *http.R } g.observe(r.Context(), selected, response, started) if response.FinishReason != cognitive.FinishCompleted { - writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: response.FinishReason, Usage: response.Usage}) + // A selected provider can become unavailable after discovery. Semantic + // routing must still retain the deterministic interpretation and use the + // local lifeboat rather than turn an ordinary conversation into a + // degraded reply solely because one provider failed. + writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes)) return } candidate, ok := parseInterpretationCandidate(response.AdvisoryText) if !ok { - writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishInvalidStructuredOutput, Usage: response.Usage}) + // Structured interpretation is a transport contract. A malformed model + // answer is not a reason to discard the available local fallback. + writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes)) return } writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage}) diff --git a/cmd/cognitive-worker/interpretation_test.go b/cmd/cognitive-worker/interpretation_test.go index 3183b53..45fa4e4 100644 --- a/cmd/cognitive-worker/interpretation_test.go +++ b/cmd/cognitive-worker/interpretation_test.go @@ -1,9 +1,15 @@ package main import ( + "bytes" "context" + "encoding/json" + "net/http" + "net/http/httptest" "testing" + "time" + "git.cin.su/m/Assistant/contracts/capability" "git.cin.su/m/Assistant/contracts/cognitive" ) @@ -24,3 +30,32 @@ func TestBuiltinInterpretationTimeoutRetainsDeterministicCandidate(t *testing.T) t.Fatalf("candidate did not retain deterministic meaning: %+v", response.Candidate) } } + +func TestInterpretationProviderFailureUsesLocalLifeboat(t *testing.T) { + selected := gatewaySelection() + selected.Binding.DataPolicy = capability.DataPolicyLocalOnly + primary := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{FinishReason: cognitive.FinishProviderUnavailable}} + lifeboat := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{AdvisoryText: `{"primary_speech_act":"QUESTION","secondary_speech_act":"","confidence_millis":900,"applicability":"APPLICABLE","requested_outcome":"","needs_cognition":true,"clarification":"NOT_REQUIRED","selection_reason":"local lifeboat","capability_hints":["COGNITIVE_REASONING"]}`, FinishReason: cognitive.FinishCompleted}} + gateway := cognitiveGateway{ + resolver: ®istryStub{selected: selected}, + providers: map[capability.ProviderKind][]providerRegistration{ + selected.Provider.Kind: {{ProviderInstanceID: selected.Provider.ID, Kind: selected.Provider.Kind, Provider: primary}}, + }, + builtinLocal: &providerRegistration{Provider: lifeboat}, + now: time.Now, + } + request := cognitive.GatewayInterpretationRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1", Message: "сколько фигур в шахматах?", Locale: "ru", MaxOutputBytes: 4096, DataPolicy: capability.DataPolicyLocalOnly, DeterministicSpeechAct: "QUESTION", DeterministicConfidence: 800, DeterministicApplicability: cognitive.InterpretationApplicable} + payload, _ := json.Marshal(request) + recorder := httptest.NewRecorder() + gateway.interpretationHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/interpretations", bytes.NewReader(payload))) + if recorder.Code != http.StatusOK || !primary.called || !lifeboat.called { + t.Fatalf("status=%d primary=%t lifeboat=%t body=%s", recorder.Code, primary.called, lifeboat.called, recorder.Body.String()) + } + var response cognitive.GatewayInterpretationResponse + if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil { + t.Fatal(err) + } + if response.FinishReason != cognitive.FinishCompleted || response.Candidate.SelectionReason != "local lifeboat" { + t.Fatalf("response=%+v", response) + } +} -- 2.54.0