Keep chat concise and route clarification research #280

Merged
m merged 2 commits from codex/chat-semantic-inventory into main 2026-08-24 19:28:03 +00:00
7 changed files with 35 additions and 18 deletions
+1 -1
View File
@@ -125,7 +125,7 @@ func (a *OpenAICompatibleLocalAdapter) Advise(ctx context.Context, request cogni
if err := ValidateAdvisoryRequest(request); err != nil || request.ProviderModelID == "" {
return cognitive.AdvisoryResponse{}, ErrInvalidAdvisory
}
payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Ты отвечаешь от имени Домового — локального помощника пользователя. Не называй себя именем модели, поставщиком или внутренним runtime. Если спрашивают имя, ответь: «Меня зовут Домовой». Понимай смысл сообщения и помогай довести цель до полезного результата. Отвечай только некатегоричным текстом совета; не утверждай, что внешнее действие уже выполнено."}, {Role: "user", Content: localAdvisoryMessage(request)}}, Temperature: 0, Stream: false, MaxTokens: request.MaxOutputBytes / 4, ChatTemplateKwargs: compatibleTemplateKwargs{EnableThinking: false}}
payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Ты отвечаешь от имени Домового — локального помощника пользователя. Не называй себя именем модели, поставщиком или внутренним runtime. Если спрашивают имя, ответь: «Меня зовут Домовой». Понимай смысл сообщения и помогай довести цель до полезного результата. По умолчанию отвечай кратко, примерно одним-тремя предложениями; подробности давай только по просьбе. Отвечай только некатегоричным текстом совета; не утверждай, что внешнее действие уже выполнено."}, {Role: "user", Content: localAdvisoryMessage(request)}}, Temperature: 0, Stream: false, MaxTokens: request.MaxOutputBytes / 4, ChatTemplateKwargs: compatibleTemplateKwargs{EnableThinking: false}}
var result compatibleChatResponse
status, err := a.requestJSON(ctx, http.MethodPost, "/chat/completions", payload, &result)
if err != nil {
+7 -7
View File
@@ -186,22 +186,22 @@ func selectionObjectiveForMeaning(meaning cognitive.TaskMeaning, message string)
}
func builtinAdvisory(request cognitive.GatewayAdvisoryRequest) cognitive.GatewayAdvisoryResponse {
text := "Я рядом и продолжаю работу во встроенном режиме Домового. Внешний когнитивный маршрут сейчас недоступен, поэтому опираюсь на уже установленный смысл сообщения и локальные возможности."
text := "Я — Домовой. Сейчас могу ответить коротко; если нужен подробный разбор, попросите его отдельно."
switch request.Meaning.InformationRequestKind {
case interaction.InformationRequestCognitiveModelInventory:
text = "Запрос понят как проверка моделей. Встроенный режим Домового доступен; я могу показать подключённые модели по назначению, качеству, скорости и стоимости. Ключи и пароли в разговор не нужны."
text = "Понял: проверю подключённые модели и коротко сообщу результат. Ключи и пароли не показываю."
case interaction.InformationRequestCapabilityInventory:
text = "Запрос понят как проверка возможностей. Я могу объяснить доступные capabilities и отличить то, что уже подключено, от того, что пока является только планом или capability gap."
text = "Понял: коротко перечислю доступные возможности и отдельно отмечу недоступные."
case interaction.InformationRequestCognitiveChannelStatus:
text = "Запрос понят как проверка когнитивного канала. Локальный режим остаётся доступен; я объясню наблюдаемое состояние и что можно сделать без ожидания внешней модели."
text = "Локальный канал Домового доступен; подробности состояния дам по запросу."
case interaction.InformationRequestCognitiveRecovery:
text = "Запрос понят как восстановление ИИ. Начинаю с локального маршрута: фиксирую, что недоступно, проверяю безопасные варианты восстановления и сформулирую следующий обратимый шаг."
text = "Понял: проверю встроенный маршрут и предложу следующий рабочий шаг."
default:
switch request.Meaning.SpeechAct {
case "REQUEST_TO_INVESTIGATE":
text = "Я принял это как исследование. Даже без внешней модели могу сформулировать гипотезы, отделить известное от неизвестного и предложить следующий проверяемый шаг."
text = "Понял задачу. Проверю доступный путь и коротко сообщу, что удалось установить."
case "QUESTION":
text = "Я — Домовой и отвечаю во встроенном режиме. Если для полного ответа не хватает внешней модели, объясню это прямо и продолжу искать проверяемый путь к цели с доступными локальными и подключёнными источниками."
text = "Я — Домовой. Отвечу коротко, а подробности добавлю, если попросите."
}
}
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: text, FinishReason: cognitive.FinishCompleted}
@@ -185,7 +185,7 @@ 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, "восстановление ИИ") {
if !strings.Contains(response.AdvisoryText, "встроенный маршрут") || !strings.Contains(response.AdvisoryText, "рабочий шаг") {
t.Fatalf("unexpected recovery response: %q", response.AdvisoryText)
}
}
+14 -7
View File
@@ -1458,19 +1458,26 @@ func hasCapabilityHint(hints []string, wanted string) bool {
func renderResearchReply(result epistemics.ResearchResult, researchErr error) (string, bool) {
if researchErr != nil {
return "Я не смог получить проверяемые данные из research-источника, поэтому не буду придумывать ответ.", false
return "Сейчас не удалось получить данные из подключённого источника. Попробую снова через доступный маршрут.", false
}
lines := make([]string, 0, len(result.Findings))
lines := make([]string, 0, 2)
for _, finding := range result.Findings {
if finding.State != epistemics.FindingSupported || strings.TrimSpace(finding.Excerpt.Text) == "" || strings.TrimSpace(finding.Source.Locator) == "" {
continue
}
lines = append(lines, "- "+finding.Excerpt.Text+"\n Источник: "+finding.Source.Locator)
excerpt := strings.TrimSpace(finding.Excerpt.Text)
if len([]rune(excerpt)) > 280 {
excerpt = string([]rune(excerpt)[:280]) + "…"
}
lines = append(lines, "- "+excerpt+"\n Источник: "+finding.Source.Locator)
if len(lines) == 2 {
break
}
}
if len(lines) == 0 {
return "Я не получил достаточно согласованных данных из проверяемых источников, поэтому не буду выдавать предположение как факт.", false
return "Проверяемых данных пока не найдено. Могу повторить поиск или уточнить, что именно проверить.", false
}
return "Нашёл точные выдержки из подключённого источника. Я не заменяю их догадкой:\n" + strings.Join(lines, "\n"), true
return "Проверил источник:\n" + strings.Join(lines, "\n"), true
}
func humanCognitiveModelInventory(candidates []capability.ResolvedCognitiveBinding, inventoryErr error) string {
@@ -1583,9 +1590,9 @@ func humanCognitiveChannelReply(kind interaction.InformationRequestKind, act int
func humanCognitiveDegradedReply(message string) string {
if strings.TrimSpace(message) == "" {
return "Основной канал мышления сейчас недоступен. Я рядом и продолжаю работать в локальном аварийном режиме."
return "Сейчас когнитивный канал недоступен; Домовой остаётся на связи."
}
return "Основной канал мышления сейчас недоступен. Домовой жив: сообщение принято, а я могу помочь разобраться с локальным каналом и настройкой восстановления."
return "Сейчас когнитивный канал недоступен. Повторю запрос через доступный маршрут, когда он восстановится."
}
func humanCapabilityList(kinds []capability.CapabilityKind) string {
+1 -1
View File
@@ -43,7 +43,7 @@ func TestCognitiveConfigurationRequestDoesNotPretendToMutate(t *testing.T) {
func TestCognitiveDegradedReplyKeepsDomovoyAlive(t *testing.T) {
reply := humanCognitiveDegradedReply("Помоги настроить локальную модель")
if !strings.Contains(reply, "Домовой жив") || !strings.Contains(reply, "принято") {
if !strings.Contains(reply, "когнитивный канал") || !strings.Contains(reply, "Повторю") {
t.Fatalf("unexpected degraded reply: %q", reply)
}
}
+1 -1
View File
@@ -213,7 +213,7 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo
}
questionWords := map[string]struct{}{"кто": {}, "что": {}, "как": {}, "почему": {}, "зачем": {}, "где": {}, "когда": {}, "сколько": {}, "какой": {}, "какая": {}, "какие": {}, "чей": {}}
conversationWords := []string{"привет", "здравствуйте", "спасибо", "ку", "здравствуй"}
investigateStems := []string{"разбер", "проверь", "проанализ", "исслед", "выясн", "подум", "понят", "посмотри"}
investigateStems := []string{"разбер", "проверь", "проанализ", "исслед", "выясн", "уточн", "подум", "понят", "посмотри"}
directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "спрос", "посчитай", "рассчитай", "ответь"}
explanatoryRequestStems := []string{"расска", "раска", "объяс", "опиш", "покаж", "подскаж", "сообщ"}
preferenceStems := []string{"предпоч", "хочу", "лучше", "не используй"}
+10
View File
@@ -154,6 +154,16 @@ func TestSourceDependentQuestionRequestsWebResearchWithoutEntityBranch(t *testin
}
}
func TestRequestToClarifyCurrentFactRequestsWebResearch(t *testing.T) {
candidate := BuildDeterministicCandidate("уточни погоду в питере", "", time.Unix(10, 0).UnixMilli())
for _, hint := range candidate.CapabilityHints {
if hint == "WEB_RESEARCH" {
return
}
}
t.Fatalf("clarification request for current fact was not routed to WEB_RESEARCH: %+v", candidate)
}
func TestDeterministicCandidateKeepsFirstPersonCompletedWorkAsAssertion(t *testing.T) {
candidate := BuildDeterministicCandidate("Я всё уже настроил.", "", time.Unix(10, 0).UnixMilli())
if candidate.PrimarySpeechAct != interaction.SpeechActAssertion {