Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb76171af4 | ||
|
|
213f06661d | ||
|
|
d350b9a565 | ||
|
|
f862c3ac13 | ||
|
|
5e18eb6082 | ||
|
|
ec86c983e3 | ||
|
|
ea854d5278 | ||
|
|
bbecadcec2 | ||
|
|
1c89b30512 |
@@ -91,7 +91,7 @@ func (c cognitiveInterpretationClient) AnswerStableKnowledge(ctx context.Context
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
prompt := "Ты отвечаешь как Домовой на обычный вопрос пользователя. Сначала пойми цель вопроса и верни короткий естественный ответ по устойчивому общеизвестному знанию. Начинай сразу с ответа и не представляйся, если пользователь не спросил об имени. Не называй модель или внутреннюю систему. Если истинность ответа зависит от текущего наблюдения, внешнего источника или инструмента, верни ровно NEEDS_VERIFIED_RESEARCH. Не придумывай факты.\n\nВопрос пользователя:\n" + userMessage
|
||||
prompt := "Ты отвечаешь как Домовой на обычное сообщение пользователя. Сначала пойми цель: на вопрос дай короткий естественный ответ по устойчивому общеизвестному знанию, а безопасную просьбу создать текстовый или структурный результат выполни прямо в ответе. Начинай сразу с содержания и не представляйся, если пользователь не спросил об имени. Не называй модель или внутреннюю систему. Если истинность ответа зависит от текущего наблюдения, внешнего источника или инструмента, верни ровно NEEDS_VERIFIED_RESEARCH. Не придумывай факты и не утверждай выполнение внешнего действия.\n\nСообщение пользователя:\n" + userMessage
|
||||
context := []cognitive.ContextExcerpt{}
|
||||
if strings.TrimSpace(personalMemory) != "" {
|
||||
context = append(context, cognitive.ContextExcerpt{ID: "personal-memory", SourceKind: "derived-personal-memory", SourceID: "interaction:human-utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: personalMemory})
|
||||
|
||||
@@ -866,7 +866,8 @@ func main() {
|
||||
needsCognition = false
|
||||
}
|
||||
}
|
||||
if planningReply == "" && needsCognition && semantic.Interpretation.SpeechAct == interaction.SpeechActQuestion && !hasChatRoute(routePlan, chatRouteResearch) && !creativeRequest {
|
||||
canGenerateTextReply := shouldGenerateNonMutatingTextDirective(semantic, routePlan, helpPath, creativeRequest) && planningReply == deterministicReply
|
||||
if (planningReply == "" && needsCognition && semantic.Interpretation.SpeechAct == interaction.SpeechActQuestion || canGenerateTextReply) && !hasChatRoute(routePlan, chatRouteResearch) && !creativeRequest {
|
||||
answerCtx, cancelAnswer := context.WithTimeout(r.Context(), 12*time.Second)
|
||||
answer, answerErr := cognitiveClient.AnswerStableKnowledge(answerCtx, session, effectiveTaskMessage, personalMemory.ContextText(), cognitive.TaskMeaning{
|
||||
SpeechAct: string(semantic.Interpretation.SpeechAct), CapabilityHints: toCapabilityKinds(semantic.CapabilityHints), RequestedOutcome: semantic.Interpretation.RequestedOutcome, InformationRequestKind: semantic.InformationRequestKind, ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
|
||||
@@ -999,7 +1000,13 @@ func main() {
|
||||
ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
|
||||
})
|
||||
cancelReview()
|
||||
stableSemanticReply := mediaAttempted || identityReply != "" || memoryInstructionReply != "" || creativeReply != "" || (creativeFallbackReply != "" && !cognitiveAvailable) || semantic.InformationRequestKind != "" || locationReply != "" || deterministicSpeechAct == interaction.SpeechActConversational || deterministicSpeechAct == interaction.SpeechActConfirmation || deterministicSpeechAct == interaction.SpeechActRejection
|
||||
// A source-backed research result is owned by the research capability,
|
||||
// but its wording is not canonical state. Let the cognitive editor turn
|
||||
// it into a useful human answer while reviewPreservesResearchEvidence
|
||||
// still requires the source evidence to remain present. Information
|
||||
// requests without research (identity, channel state and so on) remain
|
||||
// stable semantic replies and cannot be rewritten here.
|
||||
stableSemanticReply := mediaAttempted || identityReply != "" || memoryInstructionReply != "" || creativeReply != "" || (creativeFallbackReply != "" && !cognitiveAvailable) || (semantic.InformationRequestKind != "" && !(researchAttempted && researchBacked)) || locationReply != "" || deterministicSpeechAct == interaction.SpeechActConversational || deterministicSpeechAct == interaction.SpeechActConfirmation || deterministicSpeechAct == interaction.SpeechActRejection
|
||||
if reviewErr != nil {
|
||||
log.Printf("cognitive response review unavailable: %v", reviewErr)
|
||||
} else if reviewMayReplacePlanningReply(stableSemanticReply, stableKnowledgeReply, researchAttempted, researchBacked, reviewedReply, planningReply) {
|
||||
@@ -1803,6 +1810,12 @@ func renderResearchReply(query string, result epistemics.ResearchResult, researc
|
||||
excerpt = string([]rune(excerpt)[:limit]) + "…"
|
||||
}
|
||||
lines = append(lines, excerpt+"\nИсточник: "+researchSourceLabel(finding))
|
||||
// A nearest-flight request needs one route-relevant schedule result.
|
||||
// Appending weaker matches makes unrelated category pages look like
|
||||
// alternatives even after the relevance check has accepted the route.
|
||||
if nearestFlightQuery(query) {
|
||||
break
|
||||
}
|
||||
if len(lines) == 2 {
|
||||
break
|
||||
}
|
||||
@@ -2018,6 +2031,21 @@ func stableKnowledgeAnswer(answer string) bool {
|
||||
return strings.TrimSpace(answer) != "" && strings.TrimSpace(answer) != "NEEDS_VERIFIED_RESEARCH"
|
||||
}
|
||||
|
||||
// shouldGenerateNonMutatingTextDirective identifies a request that can be
|
||||
// completed entirely in the reply. It excludes specialized routes and durable
|
||||
// work, so cognition can produce content but cannot stand in for an executor.
|
||||
func shouldGenerateNonMutatingTextDirective(semantic interactiondomain.SemanticResult, routes []chatRoute, helpPath work.HelpPath, creativeRequest bool) bool {
|
||||
if semantic.Interpretation.SpeechAct != interaction.SpeechActDirective || creativeRequest || helpPath.NeedsDurableWork {
|
||||
return false
|
||||
}
|
||||
// ANSWER and COGNITION are reply-only help paths. They do not execute an
|
||||
// external action, unlike direct capabilities, gaps or durable work.
|
||||
if helpPath.Kind != work.HelpPathAnswer && helpPath.Kind != work.HelpPathCognition && helpPath.Kind != "" {
|
||||
return false
|
||||
}
|
||||
return !hasChatRoute(routes, chatRouteResearch) && !hasChatRoute(routes, chatRouteMedia) && !hasChatRoute(routes, chatRouteAgent)
|
||||
}
|
||||
|
||||
func reviewPreservesResearchEvidence(reviewed, draft string) bool {
|
||||
if strings.Contains(draft, "http://") || strings.Contains(draft, "https://") {
|
||||
return strings.Contains(reviewed, "http://") || strings.Contains(reviewed, "https://")
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
||||
)
|
||||
|
||||
@@ -46,6 +47,17 @@ func TestNearestFlightReplyFiltersUnrelatedSearchSnippets(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNearestFlightReplyDoesNotAppendWeakerSecondMatch(t *testing.T) {
|
||||
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{
|
||||
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://schedule.example", Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание авиарейсов Москва — Казань, время вылета и прилёта."}},
|
||||
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://category.example", Title: "Категория направлений"}, Excerpt: epistemics.SourceExcerpt{Text: "Список близлежащих городов и ориентировочные цены."}},
|
||||
}}
|
||||
reply, backed := renderResearchReply("ближайшие авиарейсы Москва Казань сегодня", result, nil)
|
||||
if !backed || !strings.Contains(reply, "Рейсы Москва") || strings.Contains(reply, "Категория направлений") {
|
||||
t.Fatalf("weaker second finding leaked into nearest-flight reply: %q", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNearestFlightEvidenceRequiresBothRouteEndpointsWhenKnown(t *testing.T) {
|
||||
query := "ближайшие авиарейсы Москва Казань сегодня"
|
||||
matched := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание авиарейсов из Москвы в Казань."}}
|
||||
@@ -138,6 +150,30 @@ func TestResponseReviewPreservesAttemptedResearchOutcome(t *testing.T) {
|
||||
if !reviewMayReplacePlanningReply(true, true, false, false, "Санкт-Петербург — город в России.", "Меня зовут Домовой. Санкт-Петербург — город в России.") {
|
||||
t.Fatal("stable-knowledge draft must be eligible for semantic quality review")
|
||||
}
|
||||
// The caller must not mark a source-backed research result as immutable
|
||||
// semantic state: it may be rewritten, but only with its evidence retained.
|
||||
if !reviewMayReplacePlanningReply(false, false, true, true, "Из Москвы в Сочи можно добраться самолётом или поездом. Источник: https://example.test", "Сырые результаты. Источник: https://example.test") {
|
||||
t.Fatal("source-backed research draft must be eligible for human-facing review")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonMutatingTextDirectiveCanUseCognitiveReply(t *testing.T) {
|
||||
semantic := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{SpeechAct: interaction.SpeechActDirective}}
|
||||
if !shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{}, false) {
|
||||
t.Fatal("ordinary formatting directive was not eligible for a cognitive reply")
|
||||
}
|
||||
if !shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{Kind: work.HelpPathCognition}, false) {
|
||||
t.Fatal("reply-only cognition path was incorrectly rejected")
|
||||
}
|
||||
if shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{Kind: work.HelpPathDirectCapability}, false) {
|
||||
t.Fatal("direct capability was incorrectly replaced by a textual reply")
|
||||
}
|
||||
if shouldGenerateNonMutatingTextDirective(semantic, []chatRoute{{Kind: chatRouteMedia}}, work.HelpPath{}, false) {
|
||||
t.Fatal("media route was incorrectly replaced by a textual reply")
|
||||
}
|
||||
if shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{NeedsDurableWork: true}, false) {
|
||||
t.Fatal("durable work was incorrectly replaced by a textual reply")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBearerTokenRequiresBearerScheme(t *testing.T) {
|
||||
|
||||
@@ -196,7 +196,7 @@ func BuildDeterministicCandidate(message string, corrects interaction.HumanUtter
|
||||
}
|
||||
if primary == interaction.SpeechActQuestion && !hasQuestionMark && !containsMeaningfulInterrogative(tokens) &&
|
||||
!startsWithModalRequest(tokens) && !startsWithExplanatoryRequest(tokens, "расска", "раска", "объяс", "опиш", "покаж", "подскаж") {
|
||||
if len(tokens) > 0 && containsStem(tokens[0], "сдел", "исправ", "добав", "удал", "напом", "подключ", "обрати", "спрос", "посчитай", "рассчитай", "использ") {
|
||||
if len(tokens) > 0 && containsStem(tokens[0], "сдел", "исправ", "добав", "удал", "напом", "подключ", "обрати", "спрос", "посчитай", "рассчитай", "использ", "вывед") {
|
||||
primary, secondary = interaction.SpeechActDirective, interaction.SpeechActUnknown
|
||||
} else {
|
||||
primary, secondary = interaction.SpeechActAssertion, interaction.SpeechActUnknown
|
||||
@@ -279,7 +279,7 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo
|
||||
questionWords := map[string]struct{}{"кто": {}, "что": {}, "как": {}, "почему": {}, "зачем": {}, "где": {}, "когда": {}, "сколько": {}, "какой": {}, "какая": {}, "какие": {}, "чей": {}}
|
||||
conversationWords := []string{"привет", "здравствуйте", "спасибо", "ку", "здравствуй"}
|
||||
investigateStems := []string{"разбер", "проверь", "проанализ", "исслед", "выясн", "уточн", "подум", "понят", "посмотри", "попроб", "повтор", "снов"}
|
||||
directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "спрос", "найд", "посчитай", "рассчитай", "ответь"}
|
||||
directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "спрос", "найд", "посчитай", "рассчитай", "ответь", "вывед"}
|
||||
explanatoryRequestStems := []string{"расска", "раска", "объяс", "опиш", "покаж", "подскаж", "сообщ"}
|
||||
preferenceStems := []string{"предпоч", "хочу", "лучше", "не используй"}
|
||||
hypothesisStems := []string{"думаю", "кажется", "гипотез", "похоже"}
|
||||
@@ -494,6 +494,13 @@ func needsCognitionForCandidate(primary, secondary interaction.SpeechAct, tokens
|
||||
if primary == interaction.SpeechActDirective && (containsInterrogative(tokens) || containsModelReference(tokens) || containsStemInTokens(tokens, "спрос", "обрати", "посчитай", "рассчитай")) {
|
||||
return true
|
||||
}
|
||||
// A harmless imperative can ask for a textual or structural result even
|
||||
// without a question mark. Treat it as a cognition-worthy request instead
|
||||
// of silently recording it as an assertion. Consequential instructions are
|
||||
// kept on their explicit confirmation/capability paths.
|
||||
if primary == interaction.SpeechActDirective && !likelyConsequential(tokens) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -904,7 +911,7 @@ func followsNibud(tokens []string) bool {
|
||||
}
|
||||
|
||||
func startsWithDirectiveWhatNibud(tokens []string) bool {
|
||||
if len(tokens) < 2 || !containsStem(tokens[0], "сдел", "исправ", "добав", "удал", "напом", "подключ", "обрати", "спрос", "посчитай", "рассчитай", "использ") {
|
||||
if len(tokens) < 2 || !containsStem(tokens[0], "сдел", "исправ", "добав", "удал", "напом", "подключ", "обрати", "спрос", "посчитай", "рассчитай", "использ", "вывед") {
|
||||
return false
|
||||
}
|
||||
for index, token := range tokens {
|
||||
|
||||
@@ -112,6 +112,16 @@ func TestDeterministicCandidateTreatsModelRequestAsDirectiveNeedingCognition(t *
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeterministicCandidateTreatsFormattingImperativeAsCognitionWorthyDirective(t *testing.T) {
|
||||
candidate := BuildDeterministicCandidate("выведи пустую таблицу из трех колонок и пяти строк", "", time.Unix(10, 0).UnixMilli())
|
||||
if candidate.PrimarySpeechAct != interaction.SpeechActDirective {
|
||||
t.Fatalf("formatting request was not recognized as a directive: %+v", candidate)
|
||||
}
|
||||
if !candidate.NeedsCognition {
|
||||
t.Fatalf("formatting request must reach cognitive reasoning: %+v", candidate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAWordModelDoesNotTriggerModelInventoryOrVerification(t *testing.T) {
|
||||
candidate := BuildDeterministicCandidate("модель должна понимать смысл вопроса", "", time.Unix(10, 0).UnixMilli())
|
||||
for _, hint := range candidate.CapabilityHints {
|
||||
|
||||
Reference in New Issue
Block a user