fix(ai-bot): web search query follows the user's language and the synth notes go English, so news isn't Russia-slanted and Grok stops denying web access

This commit is contained in:
heaven 2026-06-03 01:15:39 +03:00
parent a3dbe0df78
commit c0658c38ec
3 changed files with 26 additions and 13 deletions

View file

@ -323,8 +323,15 @@ func (b *Bot) genWebThenGrok(ctx context.Context, body string, isDM bool, msgs [
// happened (citation_count is recorded for telemetry); the user wants the answer, not
// Google's internal redirect links. Real source attribution (resolving redirects to
// domains) is a separate, deferred feature.
//
// The note is also AUTHORITATIVE about the data being current and provided: the system
// prompt's "don't claim you have internet access if you don't" rule otherwise wins on a
// fast (reasoning_effort=none) Grok call, so it ignored the injected digest and replied
// "I don't have live web access" despite being handed fresh news. The note now explicitly
// lifts that rule for this turn (the data IS provided), so Grok answers from it instead of
// denying it. The grok_direct "no internet" honesty is untouched — only this web turn.
func webSynthMessages(base []Message, wc WebContext) []Message {
facts := "Свежие данные из веба — ответь на их основе, кратко и по делу, без URL и ссылок:\n" + wc.Digest
facts := "Fresh web-search results for the user's request (current as of now) — answer strictly from them as up-to-date facts, briefly and to the point, with no URLs or links. The data is provided to you, so do NOT say you have no internet access or that you can't fetch anything fresh:\n" + wc.Digest
return insertSystemNote(base, facts)
}
@ -332,7 +339,7 @@ func webSynthMessages(base []Message, wc WebContext) []Message {
// RECENCY query: the user wanted fresh facts but we couldn't fetch them, so the model
// must flag that its answer is from training knowledge and may be out of date.
func hedgeMessages(base []Message) []Message {
return insertSystemNote(base, "Нет доступа к свежим источникам прямо сейчас — отвечай по знаниям на момент обучения и честно предупреди, что данные могут быть устаревшими.")
return insertSystemNote(base, "No access to fresh sources right now — answer from your training knowledge and honestly warn that the data may be out of date.")
}
// factualAbstainMessages is the degrade hedge for a STATIC verifiable-fact miss (§4.4):
@ -341,7 +348,7 @@ func hedgeMessages(base []Message) []Message {
// rather than ship a confident guess — the exact failure (the hallucinated film cast)
// this redesign exists to stop.
func factualAbstainMessages(base []Message) []Message {
return insertSystemNote(base, "Не удалось проверить факты через веб. Если ответ зависит от конкретных имён, дат, годов, чисел или состава — честно скажи, что не уверен в точной фактуре и можешь ошибаться; НЕ выдавай догадку за факт.")
return insertSystemNote(base, "Couldn't verify the facts via the web. If the answer depends on specific names, dates, years, numbers, or a cast, honestly say you're not sure of the exact details and may be wrong; do NOT pass a guess off as fact.")
}
// factualMiss reports whether a web degrade should use the abstain hedge (a static

View file

@ -181,8 +181,8 @@ func TestGenerateWebDegradesToGrok(t *testing.T) {
if res.cost.WebTool != 0 || res.cost.Grounding != 0 {
t.Fatalf("web cost = %+v, want 0 (fetch failed before billing)", res.cost)
}
// Recency miss → staleness hedge ("устаревшими"), not the factual-abstain hedge.
if !hedgeContains(grok.lastReq.Messages, "устаревш") {
// Recency miss → staleness hedge ("out of date"), not the factual-abstain hedge.
if !hedgeContains(grok.lastReq.Messages, "out of date") {
t.Fatalf("freshness degrade should use the staleness hedge; messages = %+v", grok.lastReq.Messages)
}
}
@ -385,10 +385,10 @@ func TestGenerateWebDegradeFactualAbstain(t *testing.T) {
if res.route != routeGrokDirect || !res.fallback {
t.Fatalf("res route=%q fallback=%v, want grok_direct fallback", res.route, res.fallback)
}
if !hedgeContains(grok.lastReq.Messages, "Не удалось проверить") {
if !hedgeContains(grok.lastReq.Messages, "Couldn't verify the facts") {
t.Fatalf("factual miss should use the abstain hedge; messages = %+v", grok.lastReq.Messages)
}
if hedgeContains(grok.lastReq.Messages, "устаревш") {
if hedgeContains(grok.lastReq.Messages, "out of date") {
t.Fatalf("factual miss must NOT use the staleness hedge")
}
}
@ -494,9 +494,11 @@ func TestGenerateTerminalErrorPropagates(t *testing.T) {
}
}
// TestWebSynthMessagesNoRawURLs guards the source-leak fix: the grounded digest is
// injected, but the raw gemini-grounding redirect URLs must NOT reach the synth prompt
// (Grok was pasting vertexaisearch.../grounding-api-redirect/... links into the reply).
// TestWebSynthMessagesNoRawURLs guards the web-synth note: the grounded digest is injected,
// the raw gemini-grounding redirect URLs must NOT reach the prompt (Grok was pasting
// vertexaisearch.../grounding-api-redirect/... links into the reply), and the note is
// authoritative enough that Grok uses the data instead of denying web access ("I don't
// have live web access" despite being handed fresh news).
func TestWebSynthMessagesNoRawURLs(t *testing.T) {
wc := WebContext{
Digest: "Титаник вышел в 1997, режиссёр Джеймс Кэмерон.",
@ -505,7 +507,7 @@ func TestWebSynthMessagesNoRawURLs(t *testing.T) {
out := webSynthMessages(msgs("в каком году титаник"), wc)
var note string
for _, m := range out {
if m.Role == "system" && strings.Contains(m.Content, "Свежие данные") {
if m.Role == "system" && strings.Contains(m.Content, "web-search results") {
note = m.Content
}
}
@ -518,6 +520,10 @@ func TestWebSynthMessagesNoRawURLs(t *testing.T) {
if strings.Contains(note, "vertexaisearch") || strings.Contains(note, "grounding-api-redirect") || strings.Contains(note, "http") {
t.Fatalf("raw citation URL leaked into the synth prompt: %q", note)
}
// The note must counter the "no internet access" rule so Grok actually uses the data.
if !strings.Contains(note, "no internet access") {
t.Fatalf("note must lift the no-internet rule for the web turn: %q", note)
}
}
func hedgeContains(ms []Message, sub string) bool {

View file

@ -54,7 +54,7 @@ const routerStageTimeout = 4 * time.Second
// classifierPrompt asks Gemini an EPISTEMIC-RISK question (not a topic label) and
// resolves follow-ups from the short conversation that is appended after it (rcx). Kept
// terse to bound tokens; extractJSON tolerates code fences.
const classifierPrompt = `You are a routing classifier for a Russian-speaking chat assistant. You do NOT answer the question. Read the short conversation; the LAST user line is the message to route, earlier lines are context to resolve pronouns and follow-ups. Reply with ONLY one JSON object, no prose.
const classifierPrompt = `You are a routing classifier for a multilingual chat assistant. You do NOT answer the question. Read the short conversation; the LAST user line is the message to route, earlier lines are context to resolve pronouns and follow-ups. Reply with ONLY one JSON object, no prose.
Your main job is an EPISTEMIC judgement, not a topic label: if the assistant answered the LAST message purely from its own memory (no web), how likely is it to state a WRONG checkable fact a name, a film/book cast, a date or release year, a number, a price, a score, a population, a who-did-what about a SPECIFIC named person/film/company/place/event? Such facts are exactly what a model misremembers and states confidently.
@ -64,7 +64,7 @@ Decide:
- "entity_obscure": true if the salient entity is plausibly long-tail / not a household name (a minor film, a non-famous person, a niche product) these are where memory fails hardest.
- "time_sensitive": true if the answer can change over time (news, prices, weather, standings, "current"/"latest"/"now").
- "trivial": true ONLY for a bare greeting, acknowledgement, or tiny arithmetic with no real question.
- "search_query": a SELF-CONTAINED web search query for this message, with follow-ups resolved from context (a bare "2024 года" after discussing a film becomes "<film name> 2024 фильм актёрский состав"). Empty string ONLY if both needs_web and verifiable are false.
- "search_query": a SELF-CONTAINED web search query for this message, written in the LANGUAGE of the user's latest message (an English message an English query; a Russian one a Russian query) so the results match the user's language and region instead of defaulting to one country. Resolve follow-ups from context (a bare "2024 года" after discussing a film becomes "<film name> 2024 фильм актёрский состав"). For broad/region-neutral requests (e.g. "interesting news") keep it general and international, don't narrow it to a single country. Empty string ONLY if both needs_web and verifiable are false.
- "confidence": 0.0-1.0, your honest certainty in needs_web.
Schema: {"needs_web":bool,"verifiable":bool,"entity_obscure":bool,"time_sensitive":bool,"trivial":bool,"search_query":"<query or empty>","confidence":0.0-1.0}