diff --git a/cmd/cognitive-worker/gateway.go b/cmd/cognitive-worker/gateway.go index d87fa0a..008e0e9 100644 --- a/cmd/cognitive-worker/gateway.go +++ b/cmd/cognitive-worker/gateway.go @@ -14,6 +14,7 @@ import ( "git.cin.su/m/Assistant/adapters/models" "git.cin.su/m/Assistant/contracts/capability" "git.cin.su/m/Assistant/contracts/cognitive" + "git.cin.su/m/Assistant/contracts/interaction" "git.cin.su/m/Assistant/platform/ids" ) @@ -159,10 +160,23 @@ func selectionObjectiveForMeaning(meaning cognitive.TaskMeaning, message string) } func builtinAdvisory(request cognitive.GatewayAdvisoryRequest) cognitive.GatewayAdvisoryResponse { - message := strings.ToLower(strings.TrimSpace(request.Message)) - text := "Я рядом и продолжаю работу во встроенном режиме Домового. Подключённые модели сейчас недоступны; могу помочь проверить канал, сравнить доступные модели и подготовить безопасное переключение." - if strings.Contains(message, "модел") || strings.Contains(message, "канал") || strings.Contains(message, "ии") || strings.Contains(message, "ai") { - text = "Встроенный режим Домового доступен. Я могу проверить подключённые модели по назначению, качеству, скорости и стоимости, после чего предложить подходящую; ключи и пароли в разговор не нужны." + text := "Я рядом и продолжаю работу во встроенном режиме Домового. Внешний когнитивный маршрут сейчас недоступен, поэтому опираюсь на уже установленный смысл сообщения и локальные возможности." + switch request.Meaning.InformationRequestKind { + case interaction.InformationRequestCognitiveModelInventory: + text = "Запрос понят как проверка моделей. Встроенный режим Домового доступен; я могу показать подключённые модели по назначению, качеству, скорости и стоимости. Ключи и пароли в разговор не нужны." + case interaction.InformationRequestCapabilityInventory: + text = "Запрос понят как проверка возможностей. Я могу объяснить доступные capabilities и отличить то, что уже подключено, от того, что пока является только планом или capability gap." + case interaction.InformationRequestCognitiveChannelStatus: + text = "Запрос понят как проверка когнитивного канала. Локальный режим остаётся доступен; я объясню наблюдаемое состояние и что можно сделать без ожидания внешней модели." + case interaction.InformationRequestCognitiveRecovery: + text = "Запрос понят как восстановление ИИ. Начинаю с локального маршрута: фиксирую, что недоступно, проверяю безопасные варианты восстановления и сформулирую следующий обратимый шаг." + default: + switch request.Meaning.SpeechAct { + case "REQUEST_TO_INVESTIGATE": + text = "Я принял это как исследование. Даже без внешней модели могу сформулировать гипотезы, отделить известное от неизвестного и предложить следующий проверяемый шаг." + case "QUESTION": + text = "Я отвечаю во встроенном режиме Домового. Если для полного ответа не хватает внешней модели, объясню это прямо и продолжу с тем, что можно проверить локально." + } } return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: text, FinishReason: cognitive.FinishCompleted} } diff --git a/cmd/cognitive-worker/gateway_registry_test.go b/cmd/cognitive-worker/gateway_registry_test.go index d5edbe8..daa7c8e 100644 --- a/cmd/cognitive-worker/gateway_registry_test.go +++ b/cmd/cognitive-worker/gateway_registry_test.go @@ -12,6 +12,7 @@ import ( "git.cin.su/m/Assistant/contracts/capability" "git.cin.su/m/Assistant/contracts/cognitive" + "git.cin.su/m/Assistant/contracts/interaction" ) type registryStub struct { @@ -168,6 +169,14 @@ func TestNoActiveBindingUsesBuiltInProviderWithoutInvocation(t *testing.T) { } } +func TestBuiltInAdvisoryUsesTrustedInformationIntentInsteadOfMessageKeywords(t *testing.T) { + request := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", Message: "arbitrary text", Meaning: cognitive.TaskMeaning{InformationRequestKind: interaction.InformationRequestCognitiveRecovery}} + response := builtinAdvisory(request) + if !strings.Contains(response.AdvisoryText, "восстановление ИИ") { + t.Fatalf("unexpected recovery response: %q", response.AdvisoryText) + } +} + func TestNoActiveBindingInvokesBuiltInLocalProvider(t *testing.T) { registry := ®istryStub{err: errors.New("no active binding")} provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", AdvisoryText: "локальный ответ", FinishReason: cognitive.FinishCompleted}} diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index b7a9c6b..53c236a 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -385,7 +385,8 @@ func main() { if responseErr == nil && responseResult.Handled { helpPath = work.HelpPath{} } - if isCapabilityQuestion(utterance.Content) { + switch semantic.InformationRequestKind { + case interaction.InformationRequestCapabilityInventory: kinds, err := capabilityRepository.AvailableCapabilityKinds(r.Context(), session.PrincipalID) if err == nil && len(kinds) > 0 { planningReply = "Сейчас у меня подключены реальные capabilities: " + humanCapabilityList(kinds) + ". Для рассуждений я использую отдельный Cognitive Gateway/Router, если локальный CANARY доступен. Возможности вроде отправки почты пока будут фиксироваться как capability gap, а не как выполненное действие." @@ -393,15 +394,13 @@ func main() { planningReply = "Я умею записывать диалог и намерения, работать с текущим контекстом, строить планы и обращаться к Cognitive Gateway при доступности локального CANARY. Реальные action-capabilities подключаются через registry; если capability нет, я фиксирую gap." } needsCognition = false - } - if isModelInventoryQuestion(semantic) { + case interaction.InformationRequestCognitiveModelInventory: candidates, inventoryErr := capabilityRepository.LoadCognitiveCandidates(r.Context(), capability.CapabilityID("creator-advisory"), session.PrincipalID) planningReply = humanCognitiveModelInventory(candidates, inventoryErr) needsCognition = false - } - if isCognitiveChannelQuestion(utterance.Content) { + case interaction.InformationRequestCognitiveChannelStatus, interaction.InformationRequestCognitiveRecovery: health, healthErr := capabilityRepository.LoadHealth(r.Context(), capability.CapabilityID("cognitive-proposal")) - planningReply = humanCognitiveChannelReply(utterance.Content, health, healthErr) + planningReply = humanCognitiveChannelReply(semantic.InformationRequestKind, semantic.Interpretation.SpeechAct, health, healthErr) needsCognition = false } if helpPath.Reply != "" { @@ -505,6 +504,7 @@ func main() { CapabilityKind: capabilityKind, CapabilityHints: toCapabilityKinds(semantic.CapabilityHints), RequestedOutcome: semantic.Interpretation.RequestedOutcome, + InformationRequestKind: semantic.InformationRequestKind, InterpretationConfidenceMillis: semantic.Interpretation.ConfidenceMillis, CapabilityGapID: capabilityGapID, DelegationID: string(delegationID), @@ -1153,15 +1153,6 @@ func envOr(name, fallback string) string { return fallback } -func isCapabilityQuestion(message string) bool { - lower := strings.ToLower(message) - return strings.Contains(lower, "что ты умеешь") || strings.Contains(lower, "что умеешь") || strings.Contains(lower, "какие возможности") -} - -func isModelInventoryQuestion(result interactiondomain.SemanticResult) bool { - return result.InformationRequestKind == interactiondomain.InformationRequestCognitiveModelInventory -} - func humanCognitiveModelInventory(candidates []capability.ResolvedCognitiveBinding, inventoryErr error) string { if inventoryErr != nil { return "Я не смогла прочитать реестр подключённых моделей. Встроенный канал Домового остаётся доступен; настройки можно проверить повторно после восстановления реестра. Ключи и пароли не выводятся в чат." @@ -1236,21 +1227,10 @@ func humanAuthMode(mode capability.AuthMode) string { } } -func isCognitiveChannelQuestion(message string) bool { - lower := strings.ToLower(message) - markers := []string{"локальн", "модель", "когнитивн", "канал мышления", "проверь канал", "настройк"} - for _, marker := range markers { - if strings.Contains(lower, marker) { - return true - } - } - return false -} - -func humanCognitiveChannelReply(message string, health capability.Health, healthErr error) string { - changeRequested := strings.Contains(strings.ToLower(message), "переключ") || strings.Contains(strings.ToLower(message), "настрой") || strings.Contains(strings.ToLower(message), "измени") || strings.Contains(strings.ToLower(message), "замени") +func humanCognitiveChannelReply(kind interaction.InformationRequestKind, act interaction.SpeechAct, health capability.Health, healthErr error) string { + recoveryRequested := kind == interaction.InformationRequestCognitiveRecovery || act == interaction.SpeechActRequestToInvestigate || act == interaction.SpeechActDirective if healthErr != nil { - if changeRequested { + if recoveryRequested { return "Я не вижу подтверждённого состояния локального канала. Домовой всё равно рядом: опишите, что изменилось, и я помогу подготовить аварийную настройку." } return "Я не вижу подтверждённого состояния локального канала, но Домовой работает. Можно продолжать разговор и проверить канал ещё раз." @@ -1266,13 +1246,16 @@ func humanCognitiveChannelReply(message string, health capability.Health, health default: observation = "Состояние основного локального канала пока не подтверждено." } - if changeRequested { + if recoveryRequested { if health.State == capability.HealthUnavailable { - return observation + " Но Домовой жив. Опишите новую локальную модель или канал — я помогу настроить аварийный режим и перед применением покажу итоговую конфигурацию." + return observation + " Домовой жив: начну с проверки встроенного маршрута, затем зафиксирую варианты аварийного восстановления без передачи секретов." } - return observation + " Я не меняю модель прямо из разговора: в обычном режиме изменение выполняется в консоли с подтверждением. После изменения я увижу новую проверку канала автоматически." + return observation + " Я не меняю модель прямо из разговора: сначала покажу наблюдаемое состояние и подготовлю следующий проверяемый шаг восстановления." } if health.State == capability.HealthUnavailable { + if recoveryRequested { + return observation + " Домовой не остановился: начну с проверки встроенного маршрута, затем зафиксирую доступные варианты восстановления без передачи секретов." + } return observation + " Но Домовой не остановился: продолжаю принимать сообщения и помогу восстановить локальный канал." } return observation + " Это наблюдение о готовности capability, а не прямой доступ к модели." diff --git a/cmd/core-runtime/main_test.go b/cmd/core-runtime/main_test.go index 0af9bea..e35ddcd 100644 --- a/cmd/core-runtime/main_test.go +++ b/cmd/core-runtime/main_test.go @@ -28,17 +28,14 @@ func TestBearerTokenRequiresBearerScheme(t *testing.T) { } func TestCognitiveChannelQuestionUsesHumanSafeHealthReply(t *testing.T) { - if !isCognitiveChannelQuestion("Проверь локальную модель") { - t.Fatal("expected local model question to be recognized") - } - reply := humanCognitiveChannelReply("Проверь локальную модель", capability.Health{State: capability.HealthAvailable, CapabilityID: "provider-secret-model"}, nil) + reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveChannelStatus, interaction.SpeechActQuestion, capability.Health{State: capability.HealthAvailable, CapabilityID: "provider-secret-model"}, nil) if !strings.Contains(reply, "доступен") || strings.Contains(reply, "provider-secret-model") { t.Fatalf("unexpected safe reply: %q", reply) } } func TestCognitiveConfigurationRequestDoesNotPretendToMutate(t *testing.T) { - reply := humanCognitiveChannelReply("Переключи локальную модель", capability.Health{State: capability.HealthUnavailable}, nil) + reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveRecovery, interaction.SpeechActDirective, capability.Health{State: capability.HealthUnavailable}, nil) if !strings.Contains(reply, "Домовой жив") || !strings.Contains(reply, "аварийн") { t.Fatalf("unexpected configuration reply: %q", reply) } @@ -57,7 +54,7 @@ func TestModelInventoryQuestionUsesReadOnlyFastPath(t *testing.T) { if err != nil { t.Fatalf("interpretation failed for %q: %v", message, err) } - if !isModelInventoryQuestion(result) { + if result.InformationRequestKind != interaction.InformationRequestCognitiveModelInventory { t.Fatalf("semantic inventory request %q was not recognized: kind=%q act=%q hints=%v", message, result.InformationRequestKind, result.Interpretation.SpeechAct, result.CapabilityHints) } } @@ -65,7 +62,7 @@ func TestModelInventoryQuestionUsesReadOnlyFastPath(t *testing.T) { if err != nil { t.Fatal(err) } - if isModelInventoryQuestion(ordinary) { + if ordinary.InformationRequestKind == interaction.InformationRequestCognitiveModelInventory { t.Fatal("configuration mutation must not use read-only model inventory path") } } diff --git a/cmd/creator-web/chat_test.go b/cmd/creator-web/chat_test.go index d4538d2..564805e 100644 --- a/cmd/creator-web/chat_test.go +++ b/cmd/creator-web/chat_test.go @@ -88,7 +88,7 @@ func TestChatMessageUsesCoreAndLocalOnlyCognitiveGateway(t *testing.T) { t.Fatalf("unexpected core chat request: %#v", request) } w.WriteHeader(http.StatusCreated) - _ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, InterpretationState: interaction.InterpretationInterpreted, NeedsCognition: true, FallbackText: "fallback"}) + _ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, InterpretationState: interaction.InterpretationInterpreted, NeedsCognition: true, FallbackText: "fallback", InformationRequestKind: interaction.InformationRequestCognitiveChannelStatus}) default: t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path) } @@ -134,6 +134,9 @@ func TestChatMessageUsesCoreAndLocalOnlyCognitiveGateway(t *testing.T) { if cognitiveRequest.ActorID != "actor-1" || cognitiveRequest.PrincipalID != "principal-1" || cognitiveRequest.ScopeID != "domovoy-global-scope" || cognitiveRequest.Message != "Кто ты?" { t.Fatalf("unexpected trusted gateway request: %#v", cognitiveRequest) } + if cognitiveRequest.Meaning.InformationRequestKind != interaction.InformationRequestCognitiveChannelStatus { + t.Fatalf("information intent was not preserved: %#v", cognitiveRequest.Meaning) + } } func TestChatMessageReturnsHumanCapabilityMetadata(t *testing.T) { diff --git a/cmd/creator-web/server.go b/cmd/creator-web/server.go index 2584303..493bcb7 100644 --- a/cmd/creator-web/server.go +++ b/cmd/creator-web/server.go @@ -383,7 +383,7 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) { cognition = "unavailable" requestID, idErr := ids.New() if idErr == nil { - trusted := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID), ActorID: session.ActorID, PrincipalID: session.PrincipalID, ScopeID: "domovoy-global-scope", Message: browserRequest.Message, Context: []cognitive.ContextExcerpt{}, Locale: "ru-RU", MaxOutputBytes: 4096, DataPolicy: capability.DataPolicyLocalOnly, EgressAcknowledged: false, Meaning: cognitive.TaskMeaning{SpeechAct: string(saved.SpeechAct), CapabilityKind: saved.CapabilityKind, CapabilityHints: saved.CapabilityHints, RequestedOutcome: saved.RequestedOutcome, ConfidenceMillis: saved.InterpretationConfidenceMillis}} + trusted := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID), ActorID: session.ActorID, PrincipalID: session.PrincipalID, ScopeID: "domovoy-global-scope", Message: browserRequest.Message, Context: []cognitive.ContextExcerpt{}, Locale: "ru-RU", MaxOutputBytes: 4096, DataPolicy: capability.DataPolicyLocalOnly, EgressAcknowledged: false, Meaning: cognitive.TaskMeaning{SpeechAct: string(saved.SpeechAct), CapabilityKind: saved.CapabilityKind, CapabilityHints: saved.CapabilityHints, RequestedOutcome: saved.RequestedOutcome, InformationRequestKind: saved.InformationRequestKind, ConfidenceMillis: saved.InterpretationConfidenceMillis}} payload, err = json.Marshal(trusted) if err == nil { var advisory cognitive.GatewayAdvisoryResponse diff --git a/contracts/cognitive/types.go b/contracts/cognitive/types.go index 6c3d7fd..465b097 100644 --- a/contracts/cognitive/types.go +++ b/contracts/cognitive/types.go @@ -4,6 +4,7 @@ import ( "git.cin.su/m/Assistant/contracts/authority" "git.cin.su/m/Assistant/contracts/capability" "git.cin.su/m/Assistant/contracts/identity" + "git.cin.su/m/Assistant/contracts/interaction" "git.cin.su/m/Assistant/platform/ids" ) @@ -86,11 +87,12 @@ type GatewayAdvisoryRequest struct { // browser-controlled provider selector; the Capability owner still resolves // the actual binding. type TaskMeaning struct { - SpeechAct string - CapabilityKind capability.CapabilityKind - CapabilityHints []capability.CapabilityKind - RequestedOutcome string - ConfidenceMillis int64 + SpeechAct string + CapabilityKind capability.CapabilityKind + CapabilityHints []capability.CapabilityKind + RequestedOutcome string + InformationRequestKind interaction.InformationRequestKind + ConfidenceMillis int64 } type GatewayAdvisoryPolicyRequest struct { diff --git a/contracts/interaction/types.go b/contracts/interaction/types.go index 686bb96..3a28841 100644 --- a/contracts/interaction/types.go +++ b/contracts/interaction/types.go @@ -24,47 +24,52 @@ type ClarificationState string type SemanticApplicability string type SemanticCandidateSource string type SemanticCandidateSelectionState string +type InformationRequestKind string const ( - ChannelWebChat Channel = "WEB_CHAT" - ConversationOpen ConversationState = "OPEN" - ConversationArchived ConversationState = "ARCHIVED" - SpeechActDirective SpeechAct = "DIRECTIVE" - SpeechActAssertion SpeechAct = "ASSERTION" - SpeechActDecision SpeechAct = "DECISION" - SpeechActPreference SpeechAct = "PREFERENCE" - SpeechActQuestion SpeechAct = "QUESTION" - SpeechActHypothesis SpeechAct = "HYPOTHESIS" - SpeechActCorrection SpeechAct = "CORRECTION" - SpeechActRequestToInvestigate SpeechAct = "REQUEST_TO_INVESTIGATE" - SpeechActConfirmation SpeechAct = "CONFIRMATION" - SpeechActRejection SpeechAct = "REJECTION" - SpeechActCancellation SpeechAct = "CANCELLATION" - SpeechActConversational SpeechAct = "CONVERSATIONAL" - SpeechActUnknown SpeechAct = "UNKNOWN" - InterpretationRecorded InterpretationState = "RECORDED" - InterpretationInterpreted InterpretationState = "INTERPRETED" - RelationCorrects InteractionRelationKind = "CORRECTS" - RelationRespondsTo InteractionRelationKind = "RESPONDS_TO" - RelationRequestsCognition InteractionRelationKind = "REQUESTS_COGNITION" - RelationCognitionFallback InteractionRelationKind = "COGNITION_FALLBACK" - RelationSupersedes InteractionRelationKind = "SUPERSEDES" - RelationClarifies InteractionRelationKind = "CLARIFIES" - ProjectRolePrimary ProjectReferenceRole = "PRIMARY" - ProjectRoleRelated ProjectReferenceRole = "RELATED" - ProjectRoleReferenceSource ProjectReferenceRole = "REFERENCE_SOURCE" - ClarificationNotRequired ClarificationState = "NOT_REQUIRED" - ClarificationRequired ClarificationState = "REQUIRED" - SemanticApplicable SemanticApplicability = "APPLICABLE" - SemanticLowConfidence SemanticApplicability = "LOW_CONFIDENCE" - SemanticMixedIntent SemanticApplicability = "MIXED_INTENT" - SemanticNeedsBroaderInterpret SemanticApplicability = "NEEDS_BROADER_INTERPRETATION" - SemanticCandidateClarify SemanticApplicability = "CLARIFICATION_REQUIRED" - CandidateSourceDeterministic SemanticCandidateSource = "DETERMINISTIC_V2" - CandidateSourceCognitive SemanticCandidateSource = "COGNITIVE_V1" - CandidateSelected SemanticCandidateSelectionState = "SELECTED" - CandidateObserved SemanticCandidateSelectionState = "OBSERVED" - CandidateRejected SemanticCandidateSelectionState = "REJECTED" + ChannelWebChat Channel = "WEB_CHAT" + ConversationOpen ConversationState = "OPEN" + ConversationArchived ConversationState = "ARCHIVED" + SpeechActDirective SpeechAct = "DIRECTIVE" + SpeechActAssertion SpeechAct = "ASSERTION" + SpeechActDecision SpeechAct = "DECISION" + SpeechActPreference SpeechAct = "PREFERENCE" + SpeechActQuestion SpeechAct = "QUESTION" + SpeechActHypothesis SpeechAct = "HYPOTHESIS" + SpeechActCorrection SpeechAct = "CORRECTION" + SpeechActRequestToInvestigate SpeechAct = "REQUEST_TO_INVESTIGATE" + SpeechActConfirmation SpeechAct = "CONFIRMATION" + SpeechActRejection SpeechAct = "REJECTION" + SpeechActCancellation SpeechAct = "CANCELLATION" + SpeechActConversational SpeechAct = "CONVERSATIONAL" + SpeechActUnknown SpeechAct = "UNKNOWN" + InterpretationRecorded InterpretationState = "RECORDED" + InterpretationInterpreted InterpretationState = "INTERPRETED" + RelationCorrects InteractionRelationKind = "CORRECTS" + RelationRespondsTo InteractionRelationKind = "RESPONDS_TO" + RelationRequestsCognition InteractionRelationKind = "REQUESTS_COGNITION" + RelationCognitionFallback InteractionRelationKind = "COGNITION_FALLBACK" + RelationSupersedes InteractionRelationKind = "SUPERSEDES" + RelationClarifies InteractionRelationKind = "CLARIFIES" + ProjectRolePrimary ProjectReferenceRole = "PRIMARY" + ProjectRoleRelated ProjectReferenceRole = "RELATED" + ProjectRoleReferenceSource ProjectReferenceRole = "REFERENCE_SOURCE" + ClarificationNotRequired ClarificationState = "NOT_REQUIRED" + ClarificationRequired ClarificationState = "REQUIRED" + SemanticApplicable SemanticApplicability = "APPLICABLE" + SemanticLowConfidence SemanticApplicability = "LOW_CONFIDENCE" + SemanticMixedIntent SemanticApplicability = "MIXED_INTENT" + SemanticNeedsBroaderInterpret SemanticApplicability = "NEEDS_BROADER_INTERPRETATION" + SemanticCandidateClarify SemanticApplicability = "CLARIFICATION_REQUIRED" + CandidateSourceDeterministic SemanticCandidateSource = "DETERMINISTIC_V2" + CandidateSourceCognitive SemanticCandidateSource = "COGNITIVE_V1" + CandidateSelected SemanticCandidateSelectionState = "SELECTED" + CandidateObserved SemanticCandidateSelectionState = "OBSERVED" + CandidateRejected SemanticCandidateSelectionState = "REJECTED" + InformationRequestCapabilityInventory InformationRequestKind = "CAPABILITY_INVENTORY" + InformationRequestCognitiveModelInventory InformationRequestKind = "COGNITIVE_MODEL_INVENTORY" + InformationRequestCognitiveChannelStatus InformationRequestKind = "COGNITIVE_CHANNEL_STATUS" + InformationRequestCognitiveRecovery InformationRequestKind = "COGNITIVE_RECOVERY" ) type Conversation struct { @@ -172,6 +177,7 @@ type ChatMessageResponse struct { CapabilityKind capability.CapabilityKind CapabilityHints []capability.CapabilityKind RequestedOutcome string + InformationRequestKind InformationRequestKind InterpretationConfidenceMillis int64 CapabilityGapID capability.CapabilityGapID DelegationID string diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 5e6152a..f97dbad 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/ARCH-000012-accept-external-audit-exploration.json", - "task_id": "ARCH-000012", + "task_file": "dev/tasks/DEV-000094-semantic-local-recovery-routing.json", + "task_id": "DEV-000094", "task_revision": 1 } diff --git a/dev/tasks/DEV-000094-semantic-local-recovery-routing.json b/dev/tasks/DEV-000094-semantic-local-recovery-routing.json new file mode 100644 index 0000000..738dc51 --- /dev/null +++ b/dev/tasks/DEV-000094-semantic-local-recovery-routing.json @@ -0,0 +1,41 @@ +{ + "task_id": "DEV-000094", + "task_revision": 1, + "architecture_revision": "ADR-0018", + "base_repo_revision": "39f9081", + "risk_tier": "E", + "intent": "Preserve the centrally derived informational intent from Interaction through Core and the local cognitive gateway, so local conversation and provider-recovery responses are selected by meaning rather than duplicated raw-text checks.", + "requirement_ids": ["ADR-0018", "FD-001", "FD-009", "FD-015", "INV-EPI-001", "Local conversation remains useful during provider loss", "Semantic intent is not reparsed by each runtime"], + "allowed_change_scope": { + "modules": ["interaction", "core-runtime", "creator-web", "cognitive-worker", "development-governance"], + "paths": [ + "contracts/interaction/types.go", + "contracts/cognitive/types.go", + "modules/interaction/domain/chat.go", + "modules/interaction/domain/chat_test.go", + "modules/interaction/domain/semantics.go", + "cmd/core-runtime/main.go", + "cmd/core-runtime/main_test.go", + "cmd/core-runtime/cognitive_interpretation.go", + "cmd/creator-web/server.go", + "cmd/creator-web/chat_test.go", + "cmd/cognitive-worker/gateway.go", + "cmd/cognitive-worker/gateway_registry_test.go", + "dev/ACTIVE_TASK.json", + "dev/tasks/DEV-000094-semantic-local-recovery-routing.json" + ] + }, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 1, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0}, + "contracts": {"consume": ["ADR-0018 Exploration profile", "existing Creator chat and gateway contracts"], "may_change": ["interaction ChatMessageResponse informational intent field", "cognitive TaskMeaning informational intent field"]}, + "semantic_owner": "interaction for informational intent; Cognitive Gateway only consumes the typed value for an ephemeral local answer", + "canonical_state": ["existing Interaction utterance interpretation only"], + "state_transitions": ["utterance -> one informational intent classification", "typed intent -> Core read-only answer or local advisory response", "provider loss -> recovery-oriented local response without an external action"], + "side_effects_and_privileges": ["No external mutation", "No new privilege", "No provider invocation outside the Cognitive Gateway", "No credential or secret rendering"], + "persistence_and_migrations": ["No schema migration; informational intent is an ephemeral response field derived from existing utterance semantics"], + "failure_retry_restart": ["Gateway loss preserves Core's typed read-only replies", "local provider loss returns a typed built-in response", "no automatic external-provider retry is introduced"], + "assumptions": ["A bounded deterministic classifier is allowed as the Interaction-owned fallback when richer cognition is absent", "the first delivery only provides read-only conversation and recovery guidance; it does not create a Work task or action proposal automatically", "docker-test remains prohibited"], + "acceptance_scenarios": ["Model inventory, capability inventory and cognitive-channel/recovery questions receive a single typed classification", "Core no longer reparses those informational requests from raw text", "Creator passes the typed intent to the local gateway", "gateway built-in advisory does not branch on raw message keywords", "ordinary local fallback remains meaningful when a local provider fails"], + "required_tests": ["go test ./modules/interaction/domain", "go test ./cmd/core-runtime", "go test ./cmd/creator-web", "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"] +} diff --git a/modules/interaction/domain/chat.go b/modules/interaction/domain/chat.go index 327d01c..82ee152 100644 --- a/modules/interaction/domain/chat.go +++ b/modules/interaction/domain/chat.go @@ -445,16 +445,30 @@ func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []strin return values } -func informationRequestKind(message string) InformationRequestKind { +func informationRequestKind(message string, candidate interaction.SemanticCandidate) interaction.InformationRequestKind { tokens := semanticTokens(message) - if len(tokens) == 0 || containsStem(tokens[0], "настро") || !containsStemInTokens(tokens, "модел", "ии") { + if len(tokens) == 0 { return "" } - inventoryCue := containsStemInTokens(tokens, "список", "доступ", "подключ") || (containsMeaningfulInterrogative(tokens) && containsStemInTokens(tokens, "настро")) - if !inventoryCue { - return "" + 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, "напиш", "покаж", "перечисл") + if cognitiveSubject && inventoryRequest && inventoryCue { + return interaction.InformationRequestCognitiveModelInventory } - return InformationRequestCognitiveModelInventory + capabilitySubject := containsStemInTokens(tokens, "уме", "возможност", "способн", "capabilit") + if inquiry && capabilitySubject { + return interaction.InformationRequestCapabilityInventory + } + recoveryCue := containsStemInTokens(tokens, "восстанов", "исправ", "почин", "подключ", "настро", "недоступ", "нет") + if cognitiveSubject && recoveryCue { + return interaction.InformationRequestCognitiveRecovery + } + if cognitiveSubject && inquiry { + return interaction.InformationRequestCognitiveChannelStatus + } + return "" } func boundedConfidence(primaryScore, secondaryScore int, scores map[interaction.SpeechAct]int) int64 { diff --git a/modules/interaction/domain/chat_test.go b/modules/interaction/domain/chat_test.go index b1b5253..d0eda32 100644 --- a/modules/interaction/domain/chat_test.go +++ b/modules/interaction/domain/chat_test.go @@ -103,6 +103,27 @@ func TestDeterministicCandidateTreatsModelRequestAsDirectiveNeedingCognition(t * } } +func TestInformationRequestsAreClassifiedOnceByInteraction(t *testing.T) { + cases := []struct { + message string + kind interaction.InformationRequestKind + }{ + {"какие модели доступны?", interaction.InformationRequestCognitiveModelInventory}, + {"что ты умеешь?", interaction.InformationRequestCapabilityInventory}, + {"проверь состояние локального канала", interaction.InformationRequestCognitiveChannelStatus}, + {"нет связи с ИИ, восстанови канал", interaction.InformationRequestCognitiveRecovery}, + } + for _, test := range cases { + result, err := InterpretUtterance(interaction.HumanUtterance{Content: test.message}, nil, nil, time.Unix(10, 0)) + if err != nil { + t.Fatalf("interpret %q: %v", test.message, err) + } + if result.InformationRequestKind != test.kind { + t.Fatalf("message=%q kind=%q want=%q", test.message, result.InformationRequestKind, test.kind) + } + } +} + func TestDeterministicCandidateKeepsFirstPersonCompletedWorkAsAssertion(t *testing.T) { candidate := BuildDeterministicCandidate("Я всё уже настроил.", "", time.Unix(10, 0).UnixMilli()) if candidate.PrimarySpeechAct != interaction.SpeechActAssertion { diff --git a/modules/interaction/domain/semantics.go b/modules/interaction/domain/semantics.go index 249bed9..a9a71da 100644 --- a/modules/interaction/domain/semantics.go +++ b/modules/interaction/domain/semantics.go @@ -19,13 +19,9 @@ type SemanticResult struct { Reply string NeedsCognition bool CapabilityHints []string - InformationRequestKind InformationRequestKind + InformationRequestKind interaction.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 { @@ -54,7 +50,7 @@ func InterpretUtterance(utterance interaction.HumanUtterance, projects []situati Candidates: []interaction.SemanticCandidate{deterministicCandidate}, NeedsCognition: selectedCandidate.NeedsCognition, CapabilityHints: append([]string(nil), selectedCandidate.CapabilityHints...), - InformationRequestKind: informationRequestKind(utterance.Content), + InformationRequestKind: informationRequestKind(utterance.Content, selectedCandidate), } explicitFocus := explicitProjectFocus(utterance.Content, projects) projectRefs, err := projectReferences(utterance, projects, explicitFocus, previousFocus, millis)