DEV-000274 Truth-first chat and intent-aware model routing #252
@@ -163,8 +163,8 @@ func interpretationPrompt(request cognitive.GatewayInterpretationRequest) string
|
||||
Allowed speech acts: DIRECTIVE, ASSERTION, DECISION, PREFERENCE, QUESTION, HYPOTHESIS, CORRECTION, REQUEST_TO_INVESTIGATE, CONFIRMATION, REJECTION, CANCELLATION, CONVERSATIONAL, UNKNOWN.
|
||||
Allowed applicability: APPLICABLE, LOW_CONFIDENCE, MIXED_INTENT, NEEDS_BROADER_INTERPRETATION, CLARIFICATION_REQUIRED.
|
||||
Allowed clarification: NOT_REQUIRED or REQUIRED.
|
||||
Capability hints may only contain known capability kinds like COGNITIVE_REASONING, MODIFY_CODE, RUN_TESTS, SEND_EMAIL, SCHEDULE_REMINDER, VERIFY_FACT, INSPECT_PROJECT, READ_DATABASE, QUERY_LOGS.
|
||||
Interpret the human utterance semantically. Do not claim action execution. Do not treat punctuation alone as decisive. If uncertain, set applicability accordingly.
|
||||
Capability hints may only contain known capability kinds like COGNITIVE_REASONING, WEB_RESEARCH, MODIFY_CODE, RUN_TESTS, SEND_EMAIL, SCHEDULE_REMINDER, VERIFY_FACT, INSPECT_PROJECT, READ_DATABASE, QUERY_LOGS.
|
||||
Interpret the human utterance semantically. Do not claim action execution. A question asking for a current, external or otherwise source-dependent fact should receive WEB_RESEARCH; do not answer that fact from model memory. A request to discuss hypotheses or options is different and may receive COGNITIVE_REASONING. Do not treat punctuation alone as decisive. If uncertain, set applicability accordingly.
|
||||
Message: %q
|
||||
Deterministic primary: %s
|
||||
Deterministic secondary: %s
|
||||
|
||||
@@ -151,6 +151,44 @@ func TestChatMessageUsesCoreAndLocalOnlyCognitiveGateway(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatMessageDoesNotExposeUnverifiedQuestionAsFact(t *testing.T) {
|
||||
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/v1/creator/sessions/current":
|
||||
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages":
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, NeedsCognition: true, FallbackText: "небезопасная модельная догадка"})
|
||||
default:
|
||||
t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer core.Close()
|
||||
cognitiveRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("unverified question must not reach advisory: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer cognitiveRuntime.Close()
|
||||
handler, err := newHandler(core.URL, "http://continuity", "http://commit", core.Client(), cognitiveRuntime.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/chat/messages", strings.NewReader(`{"message":"Какая погода?"}`))
|
||||
request.Header.Set("Authorization", "Bearer session-token")
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var response browserChatResponse
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.Cognition != "verification_required" || strings.Contains(response.ReplyText, "небезопасная") || !strings.Contains(response.ReplyText, "не буду придумывать") {
|
||||
t.Fatalf("unexpected truth-first response: %#v", response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatMessageReturnsHumanCapabilityMetadata(t *testing.T) {
|
||||
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
|
||||
@@ -467,7 +467,11 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
|
||||
saved.NeedsCognition = false
|
||||
}
|
||||
}
|
||||
if saved.NeedsCognition && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired {
|
||||
if saved.SpeechAct == interaction.SpeechActQuestion && saved.InformationRequestKind == "" && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired && !asksAttention(browserRequest.Message) {
|
||||
reply = truthfulVerificationRequiredReply()
|
||||
cognition = "verification_required"
|
||||
saved.NeedsCognition = false
|
||||
} else if saved.NeedsCognition && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired {
|
||||
cognition = "unavailable"
|
||||
requestID, idErr := ids.New()
|
||||
if idErr == nil {
|
||||
@@ -508,6 +512,10 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func truthfulVerificationRequiredReply() string {
|
||||
return "Я не могу сейчас подтвердить этот факт по проверенному источнику, поэтому не буду придумывать ответ. Для проверки используйте официальный сайт соответствующей службы или надёжный сервис с текущими данными; когда подключён research-маршрут, Домовой сможет проверить это сам."
|
||||
}
|
||||
|
||||
func localChatContext(saved interaction.ChatMessageResponse) []cognitive.ContextExcerpt {
|
||||
if strings.TrimSpace(saved.ActiveProjectName) == "" {
|
||||
return []cognitive.ContextExcerpt{}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000273-live-audit-status-reconciliation.json",
|
||||
"task_id": "DEV-000273",
|
||||
"task_file": "dev/tasks/DEV-000274-truth-first-question-gate.json",
|
||||
"task_id": "DEV-000274",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000274",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "5205845",
|
||||
"risk_tier": "E",
|
||||
"intent": "Make the default chat path refuse unverified factual answers instead of exposing model speculation; preserve explicitly classified internal status paths and leave a general research capability for the next routing stage.",
|
||||
"requirement_ids": ["truth-first default", "no fabricated factual answers", "semantic routing without entity word lists"],
|
||||
"allowed_change_scope": {"modules": ["creator-web", "cognitive-worker", "interaction"], "paths": ["cmd/creator-web/server.go", "cmd/creator-web/chat_test.go", "cmd/cognitive-worker/interpretation.go", "cmd/cognitive-worker/interpretation_test.go", "modules/interaction/domain/chat.go", "modules/interaction/domain/chat_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000274-truth-first-question-gate.json", "docs/PROJECT_STATUS.md", "docs/evidence/**"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "contracts/**", "deploy/**", "LLM-local/**"]},
|
||||
"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 semantic speech act and information request", "existing local cognitive gateway"], "may_change": ["truthful chat presentation gate", "interpretation prompt guidance"]},
|
||||
"semantic_owner": "interaction semantics and creator-web presentation boundary",
|
||||
"canonical_state": ["existing semantic interpretation", "existing verified internal status paths"],
|
||||
"state_transitions": ["unverified question -> truthful verification-required response", "explicitly classified internal status -> existing status response"],
|
||||
"side_effects_and_privileges": ["none; no provider call is made for an unverified factual question"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["missing research capability never becomes a fabricated answer"],
|
||||
"assumptions": ["a later task will connect WEB_RESEARCH to a read-only source connector", "explicit user requests to discuss hypotheses/options remain a separate cognitive path"],
|
||||
"acceptance_scenarios": ["weather-like factual question does not expose a model-generated answer without evidence", "response explains that verification is unavailable and names safe source category", "internal classified status path remains functional", "an incidental word such as model does not open an inventory or verification route", "routing uses semantic intent rather than city/entity keyword lists"],
|
||||
"required_tests": ["go test ./cmd/creator-web/... ./cmd/cognitive-worker/...", "go vet ./cmd/creator-web/... ./cmd/cognitive-worker/...", "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", "git diff --check", "CI", "production health smoke"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY"]
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
## Current audit delta — 2026-08-24
|
||||
|
||||
- `CURRENT_MODEL_SCOPE = LOCAL_FIRST_V1`; one locally available Qwen3-8B or Gemma-4 conversational model is sufficient for the first stage, with the local Lifeboat as recovery fallback. External models remain supported as optional capability, but their absence must not block work: reflexes, skills and local executors provide degraded continuation. External/GPU qualification and promotion are intentionally deferred by DEV-000270.
|
||||
- `TRUTH_FIRST_DEFAULT = LIVE`; an unverified factual question is no longer passed to the advisory model as an answer. The response is explicitly marked `verification_required`; internal canonical status paths remain available. Semantic interpretation may select the provider-neutral `WEB_RESEARCH` capability without entity-specific keyword lists. Evidence: `docs/evidence/truth-first-question-gate-2026-08-24-v1.json`.
|
||||
|
||||
- `LAST_COMPLETED = DEV-000260`; the governed self-development path, provider-neutral reachability rating, bounded authenticated operations smoke, responsive static regression guard and deep authenticated acceptance evidence are live. Mobile visual UI remains explicitly unverified because the current browser control surface cannot emulate a 390x844 viewport.
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"evidence_id": "truth-first-question-gate-2026-08-24-v1",
|
||||
"captured_at": "2026-08-24T00:00:00Z",
|
||||
"task_id": "DEV-000274",
|
||||
"default_policy": "A factual question without verified evidence is not answered from model memory.",
|
||||
"unverified_question_behavior": "verification_required",
|
||||
"verified_internal_exception": "Existing canonical internal status routes remain available.",
|
||||
"semantic_routing": "WEB_RESEARCH is a capability hint from semantic interpretation; no city/entity keyword list is used.",
|
||||
"current_limitation": "Automatic web search and source-backed answer composition remain the next research-runtime stage.",
|
||||
"secrets": "No secret values included."
|
||||
}
|
||||
@@ -432,10 +432,11 @@ func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []strin
|
||||
if containsInterrogative(tokens) && containsArithmetic(tokens) {
|
||||
add("COGNITIVE_REASONING")
|
||||
}
|
||||
if containsToken(tokens, "свободно") || containsToken(tokens, "гб") || containsToken(tokens, "модель") {
|
||||
if containsToken(tokens, "свободно") || containsToken(tokens, "гб") {
|
||||
add("VERIFY_FACT")
|
||||
}
|
||||
if containsStemInTokens(tokens, "модел", "ии") && containsStemInTokens(tokens, "настро", "список", "доступ", "подключ") {
|
||||
modelIntent := containsStemInTokens(tokens, "настро", "список", "доступ", "подключ", "сравн", "выбер", "провер", "проверь", "перечисл")
|
||||
if containsStemInTokens(tokens, "модел", "ии") && modelIntent && (primary == interaction.SpeechActQuestion || primary == interaction.SpeechActDirective || primary == interaction.SpeechActRequestToInvestigate || secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActDirective || secondary == interaction.SpeechActRequestToInvestigate) {
|
||||
add("VERIFY_FACT")
|
||||
}
|
||||
values := make([]string, 0, len(seen))
|
||||
|
||||
@@ -103,6 +103,15 @@ func TestDeterministicCandidateTreatsModelRequestAsDirectiveNeedingCognition(t *
|
||||
}
|
||||
}
|
||||
|
||||
func TestAWordModelDoesNotTriggerModelInventoryOrVerification(t *testing.T) {
|
||||
candidate := BuildDeterministicCandidate("модель должна понимать смысл вопроса", "", time.Unix(10, 0).UnixMilli())
|
||||
for _, hint := range candidate.CapabilityHints {
|
||||
if hint == "VERIFY_FACT" {
|
||||
t.Fatalf("an incidental model mention must not trigger fact verification: %+v", candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInformationRequestsAreClassifiedOnceByInteraction(t *testing.T) {
|
||||
cases := []struct {
|
||||
message string
|
||||
|
||||
Reference in New Issue
Block a user