Route household knowledge through cognition
CI / test (push) Successful in 1m50s
CI / test (pull_request) Successful in 1m16s

This commit is contained in:
m
2026-08-26 04:31:18 +03:00
parent f316f7ec3f
commit 4e3d153557
6 changed files with 136 additions and 14 deletions
@@ -82,6 +82,54 @@ func (c cognitiveInterpretationClient) ReviewResponse(ctx context.Context, sessi
return strings.TrimSpace(result.AdvisoryText), nil
}
// AnswerStableKnowledge asks the local cognitive route for a short answer to a
// non-current question only after evidence retrieval did not produce a usable
// result. The model decides whether its knowledge is stable enough; it must
// explicitly decline changing or source-dependent facts instead of guessing.
func (c cognitiveInterpretationClient) AnswerStableKnowledge(ctx context.Context, session identity.CreatorSession, userMessage, personalMemory string, meaning cognitive.TaskMeaning) (string, error) {
requestID, err := ids.New()
if err != nil {
return "", err
}
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})
}
target := *c.baseURL
target.Path = "/v1/advisories"
payload, err := json.Marshal(cognitive.GatewayAdvisoryRequest{
Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID),
ActorID: session.ActorID, PrincipalID: session.PrincipalID, ScopeID: authority.ScopeID("domovoy-global-scope"),
Message: prompt, Context: context, Locale: "ru", MaxOutputBytes: 1024, DataPolicy: capability.DataPolicyLocalOnly,
Meaning: cognitive.TaskMeaning{SpeechAct: "STABLE_KNOWLEDGE_ANSWER", CapabilityHints: meaning.CapabilityHints, RequestedOutcome: meaning.RequestedOutcome, InformationRequestKind: meaning.InformationRequestKind, ConfidenceMillis: meaning.ConfidenceMillis},
})
if err != nil {
return "", err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
if err != nil {
return "", err
}
request.Header.Set("Content-Type", "application/json")
response, err := c.client.Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("cognitive stable knowledge status %d", response.StatusCode)
}
var result cognitive.GatewayAdvisoryResponse
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return "", err
}
if result.FinishReason != cognitive.FinishCompleted || strings.TrimSpace(result.AdvisoryText) == "" {
return "", fmt.Errorf("cognitive stable knowledge incomplete: %s", result.FinishReason)
}
return strings.TrimSpace(result.AdvisoryText), nil
}
func newCognitiveInterpretationClient(rawURL string, client *http.Client) (cognitiveInterpretationClient, error) {
base, err := url.Parse(rawURL)
if err != nil || base.Scheme == "" || base.Host == "" {
@@ -45,6 +45,28 @@ func TestReviewResponseUsesCognitiveGatewayAndReturnsOnlyDraftAnswer(t *testing.
}
}
func TestAnswerStableKnowledgeUsesLocalOnlyCognitiveRoute(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request cognitive.GatewayAdvisoryRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatalf("decode stable knowledge request: %v", err)
}
if request.DataPolicy != capability.DataPolicyLocalOnly || request.Meaning.SpeechAct != "STABLE_KNOWLEDGE_ANSWER" {
t.Fatalf("unexpected stable knowledge request: %+v", request)
}
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "Санкт-Петербург — город в России.", FinishReason: cognitive.FinishCompleted})
}))
defer server.Close()
client, err := newCognitiveInterpretationClient(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
answer, err := client.AnswerStableKnowledge(context.Background(), identity.CreatorSession{ActorID: "actor", PrincipalID: "principal"}, "Питер — это где?", "", cognitive.TaskMeaning{SpeechAct: "QUESTION"})
if err != nil || answer != "Санкт-Петербург — город в России." {
t.Fatalf("answer=%q err=%v", answer, err)
}
}
func TestGenerateImageUsesDedicatedCognitiveMediaRoute(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/v1/multimodal/generate-image" {
+25 -9
View File
@@ -821,8 +821,18 @@ func main() {
Plan: epistemics.ResearchPlan{ID: epistemics.ResearchPlanID(fmt.Sprintf("plan:%s:%d", planID, index)), Question: searchQuery, SourceClasses: []string{"web-search"}, FreshnessPolicy: "current"},
PropositionKey: fmt.Sprintf("question:%s:%d", utterance.ID, index), Query: searchQuery,
})
cancelResearch()
reply, backed := renderResearchReply(searchQuery, result, researchErr)
if !backed && cognitiveAvailable {
answerCtx, cancelAnswer := context.WithTimeout(r.Context(), 12*time.Second)
answer, answerErr := cognitiveClient.AnswerStableKnowledge(answerCtx, session, part, personalMemory.ContextText(), cognitive.TaskMeaning{
SpeechAct: string(semantic.Interpretation.SpeechAct), CapabilityHints: toCapabilityKinds(semantic.CapabilityHints), RequestedOutcome: semantic.Interpretation.RequestedOutcome, InformationRequestKind: semantic.InformationRequestKind, ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
})
cancelAnswer()
if answerErr == nil && stableKnowledgeAnswer(answer) {
reply, backed = answer, true
}
}
cancelResearch()
if backed {
researchBacked = true
}
@@ -1791,12 +1801,10 @@ func renderResearchReply(query string, result epistemics.ResearchResult, researc
if nearestFlightQuery(query) && !containsFlightTimeEvidence(strings.Join(lines, " ")) {
return "Нашёл расписание по маршруту, но точное время ближайшего вылета в доступных фрагментах не указано. Откройте источник и выберите ближайший рейс:\n\n" + strings.Join(lines, "\n\n"), true
}
reply := "Нашёл проверяемые данные:\n\n" + strings.Join(lines, "\n\n")
lowerQuery := strings.ToLower(query)
if (strings.Contains(lowerQuery, "цен") || strings.Contains(lowerQuery, "стоим")) && !containsPriceEvidence(reply) {
reply = "Актуальная цена в найденных фрагментах не указана, поэтому не буду придумывать сумму. Вот страницы, где её можно проверить:\n" + reply
if trainScheduleQuery(query) && !containsScheduleTimeEvidence(strings.Join(lines, " ")) {
return "Нашёл страницу расписания по маршруту, но в доступных фрагментах нет точного времени отправления:\n\n" + strings.Join(lines, "\n\n"), true
}
return reply, true
return "Нашёл проверяемые данные:\n\n" + strings.Join(lines, "\n\n"), true
}
func isInternetStatusQuery(query string) bool {
@@ -1967,9 +1975,17 @@ func humanResearchExcerpt(query, raw string) string {
return text
}
func containsPriceEvidence(value string) bool {
pricePattern := regexp.MustCompile(`(?i)(\d[\d\s.,]{2,})(?:\s*(?:₽|руб\.?|р\.?|usd|\$|€))`)
return pricePattern.MatchString(value)
func trainScheduleQuery(query string) bool {
lower := strings.ToLower(query)
return strings.Contains(lower, "поезд") && strings.Contains(lower, "время отправления")
}
func containsScheduleTimeEvidence(value string) bool {
return regexp.MustCompile(`\b(?:[01]?\d|2[0-3]):[0-5]\d\b`).MatchString(value)
}
func stableKnowledgeAnswer(answer string) bool {
return strings.TrimSpace(answer) != "" && strings.TrimSpace(answer) != "NEEDS_VERIFIED_RESEARCH"
}
func reviewPreservesResearchEvidence(reviewed, draft string) bool {
+5 -5
View File
@@ -149,12 +149,12 @@ func TestHumanMemoryInventoryUsesNewestPreference(t *testing.T) {
}
}
func TestContainsPriceEvidenceDistinguishesLinksFromPrices(t *testing.T) {
if containsPriceEvidence("RTX 4090 — market.yandex.ru") {
t.Fatal("a product link is not price evidence")
func TestContainsScheduleTimeEvidence(t *testing.T) {
if containsScheduleTimeEvidence("расписание поездов и цена билета") {
t.Fatal("a price or schedule label is not a departure time")
}
if !containsPriceEvidence("Цена: 189 990 ₽") {
t.Fatal("expected a currency amount to count as price evidence")
if !containsScheduleTimeEvidence("Отправление в 07:45") {
t.Fatal("expected an explicit departure time to be recognized")
}
}
@@ -226,6 +226,16 @@ func TestNearestPlaneUsesTravelContextAndCurrentDay(t *testing.T) {
}
}
func TestTravelResearchQueryUsesKnownOriginForTrainSchedule(t *testing.T) {
now := time.Date(2026, time.August, 26, 9, 0, 0, 0, time.UTC)
history := []interaction.HumanUtterance{{Content: "я сейчас в Москве"}}
got := TravelResearchQuery("найди поезд до Петербурга", history, now)
want := "расписание поездов из Москве в Петербурга время отправления сегодня 26.08.2026"
if got != want {
t.Fatalf("train schedule query lost route context or primary outcome: %q", got)
}
}
func TestIndependentRequestPartsKeepsCoordinatedEntitiesTogether(t *testing.T) {
if got := IndependentRequestParts("какая разница во времени между Москвой и Шанхаем?"); len(got) != 1 {
t.Fatalf("coordinated entities became independent requests: %#v", got)
+26
View File
@@ -46,6 +46,12 @@ func TravelResearchQuery(message string, history []interaction.HumanUtterance, n
if origin != "" && !containsStemInTokens(tokens, "из", "откуда", "от") {
query += " отправление из " + origin
}
} else if trainScheduleRequest(tokens) {
origin := travelContextOrigin(history)
destination := travelContextDestinationFromTokens(tokens)
if origin != "" && destination != "" {
return trainScheduleResearchQuery(origin, destination, now, containsStemInTokens(tokens, "ближ"))
}
} else if travelCue(tokens) && !containsStemInTokens(tokens, "из", "откуда", "от") {
origin := travelContextOrigin(history)
destination := travelContextDestinationFromTokens(tokens)
@@ -61,6 +67,21 @@ func TravelResearchQuery(message string, history []interaction.HumanUtterance, n
return query
}
// trainScheduleResearchQuery formulates an observed schedule request from the
// user's route and remembered origin. It contains no city, provider or answer
// catalogue: actual departure data remains evidence from the selected source.
func trainScheduleResearchQuery(origin, destination string, now time.Time, nearest bool) string {
prefix := "расписание поездов"
if nearest {
prefix = "ближайшие поезда"
}
query := prefix + " из " + origin + " в " + destination + " время отправления"
if !now.IsZero() {
query += " сегодня " + now.Format("02.01.2006")
}
return query
}
// nearestAirTravelSearchQuery is a capability-local search formulation, not a
// provider or place lookup. It removes conversational imperatives while
// preserving the two user-derived route endpoints and the implicit current-day
@@ -401,6 +422,11 @@ func travelCue(tokens []string) bool {
return containsStemInTokens(tokens, "билет", "рейс", "самолет", "самолёт", "вылет", "поезд", "автобус", "перелет", "перелёт")
}
func trainScheduleRequest(tokens []string) bool {
return containsStemInTokens(tokens, "поезд") &&
(containsStemInTokens(tokens, "найд", "ближ", "расписан", "отправ", "прибуд", "едет", "едут") || len(tokens) > 1)
}
func nearestAirTravelRequest(tokens []string) bool {
return containsStemInTokens(tokens, "ближ") && containsStemInTokens(tokens, "самолет", "самолёт", "авиарейс", "перелет", "перелёт")
}