Merge pull request 'Kiwix answers a question it cannot answer, and nothing gates it' (#213) from task/668-title-capital into master
This commit was merged in pull request #213.
This commit is contained in:
@@ -681,8 +681,11 @@ world questions, so she needs to read external sources. What replaces it:
|
||||
returned "Перехват TCP-соединения". "кто написал Войну и мир" returned an episode of
|
||||
Doctor Who. `kiwix.Topic` drops the narrative request, the interrogative and a verb
|
||||
behind one. `kiwix.TitlePath` tries the exact article first, since a ZIM is addressable
|
||||
by title and a wrong title is a 404. Four of eight questions reach the right article
|
||||
where they did not, two were already right, and nothing regressed. Both apply on the
|
||||
by title and a wrong title is a 404. Five of eight questions reach the right article
|
||||
where they did not, one was already right, and nothing regressed. The title needs its
|
||||
leading capital, so `TitleCandidates` tries the spoken form and then the capitalized
|
||||
one. **"столица Франции" is answered by a title redirect to Париж**, which is the case
|
||||
the 2026-08-05 measurement named as unreachable by any lexical signal. Both apply on the
|
||||
verbatim path alone. The rewriter already reduces a question, and reducing twice takes
|
||||
the topic off its input.
|
||||
`Response.Empty()` is the whole gate and there is no quality threshold in front of it:
|
||||
|
||||
@@ -938,10 +938,12 @@ func (h *reactiveHandler) queryKiwix(ctx context.Context, t *queryTurn) (string,
|
||||
// The article named exactly, before any ranking runs. A ZIM is
|
||||
// addressable by title and a wrong title is a 404, so this either
|
||||
// answers or costs one request that says nothing.
|
||||
page, err := h.kiwix.client.Article(ctxK, kiwix.TitlePath(book, topic), h.kiwix.runes)
|
||||
if err == nil && page.Text != "" {
|
||||
log.Printf("voice: kiwix: %q in %q → title hit %q", topic, book, page.Title)
|
||||
return h.kiwixReply(ctx, t, page.Title, page.Text)
|
||||
for _, cand := range kiwix.TitleCandidates(topic) {
|
||||
page, err := h.kiwix.client.Article(ctxK, kiwix.TitlePath(book, cand), h.kiwix.runes)
|
||||
if err == nil && page.Text != "" {
|
||||
log.Printf("voice: kiwix: %q in %q → title hit %q", topic, book, page.Title)
|
||||
return h.kiwixReply(ctx, t, page.Title, page.Text)
|
||||
}
|
||||
}
|
||||
pattern = topic
|
||||
}
|
||||
|
||||
@@ -60,6 +60,10 @@ A ZIM is also addressable by title, which nothing here used. `/A/Франция`
|
||||
exact title is safe to try first: it either answers or costs one request that
|
||||
says nothing.
|
||||
|
||||
The title has to carry its capital. `/A/фотосинтез` is a 404 and
|
||||
`/A/Фотосинтез` is a 200. The spoken form is tried first anyway, so a title
|
||||
that begins lowercase on purpose keeps its chance.
|
||||
|
||||
## What shipped, measured
|
||||
|
||||
`kiwix.Topic` drops the narrative request, the interrogative and a verb sitting
|
||||
@@ -71,25 +75,31 @@ would take the topic off the rewriter's input.
|
||||
| question | before | after |
|
||||
|---|---|---|
|
||||
| что такое TCP? | Перехват TCP-соединения | **TCP** (by title) |
|
||||
| что такое фотосинтез | C4-фотосинтез | **Фотосинтез** |
|
||||
| что такое фотосинтез | C4-фотосинтез | **Фотосинтез** (by title) |
|
||||
| кто такой Линус Торвальдс? | Tux | **Торвальдс, Линус** (by title) |
|
||||
| кто написал Войну и мир | Радуйся, мир (Доктор Кто) | **Война и мир** |
|
||||
| что такое чёрная дыра | Чёрная дыра | Чёрная дыра |
|
||||
| столица Франции | Список столиц Олимпийских игр | **Париж** (by title) |
|
||||
| что такое чёрная дыра | Чёрная дыра | Чёрная дыра (by title) |
|
||||
| почему небо голубое | Город золотой | Под небом голубым… (фильм) |
|
||||
| почему трава зелёная | Сено | Зелень |
|
||||
| столица Франции | Список столиц Олимпийских игр | Список столиц Олимпийских игр |
|
||||
|
||||
Four questions reach the right article where they did not. Two were already
|
||||
right and stay right. Nothing regressed.
|
||||
Five questions reach the right article where they did not. One was already
|
||||
right and stays right. Nothing regressed.
|
||||
|
||||
"столица Франции" is the surprise. The 2026-08-05 measurement named it as the
|
||||
case a quality gate must not break, because the answer is Париж and that word
|
||||
is not in the question. The ZIM holds a title redirect, so asking for the
|
||||
article titled "Столица Франции" returns Париж. Retrieval by title reaches an
|
||||
answer that retrieval by keyword cannot.
|
||||
|
||||
## What is still wrong
|
||||
|
||||
Two of the eight are still not answered, and both are the same shape. The
|
||||
question names no article. "почему небо голубое" is answered by Rayleigh
|
||||
scattering and "столица Франции" by the lead of Франция. Neither title is in
|
||||
the question. Keyword retrieval cannot bridge that and neither can a
|
||||
threshold. The candidates are a semantic index over titles, or asking the
|
||||
resident model for the article title rather than for keywords.
|
||||
question names no article and no redirect covers it. "почему небо голубое" is
|
||||
answered by Rayleigh scattering, and nothing in the question says so. Keyword
|
||||
retrieval cannot bridge that and neither can a threshold. The candidates are a
|
||||
semantic index over titles, or asking the resident model for the article title
|
||||
rather than for keywords.
|
||||
|
||||
`Response.Empty()` is still the whole gate. A wrong article that the search
|
||||
does return is still spoken. What this change buys is that the article is
|
||||
|
||||
@@ -39,9 +39,13 @@ func TestLiveTopicBeatsTheSentence(t *testing.T) {
|
||||
before := firstTitle(ctx, c, q, book)
|
||||
topic := Topic(q)
|
||||
after := ""
|
||||
if page, err := c.Article(ctx, TitlePath(book, topic), 400); err == nil && page.Text != "" {
|
||||
after = page.Title + " (by title)"
|
||||
} else {
|
||||
for _, cand := range TitleCandidates(topic) {
|
||||
if page, err := c.Article(ctx, TitlePath(book, cand), 400); err == nil && page.Text != "" {
|
||||
after = page.Title + " (by title)"
|
||||
break
|
||||
}
|
||||
}
|
||||
if after == "" {
|
||||
after = firstTitle(ctx, c, topic, book)
|
||||
}
|
||||
cancel()
|
||||
|
||||
@@ -61,6 +61,26 @@ func Topic(utterance string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// TitleCandidates is the topic as it might be titled, best first.
|
||||
//
|
||||
// A ZIM title is capitalized and the utterance is not: measured on 2026-08-09,
|
||||
// `/A/фотосинтез` is a 404 and `/A/Фотосинтез` is a 200. The spoken form is
|
||||
// tried first anyway, because a title that begins lowercase on purpose
|
||||
// ("iPhone") would not survive capitalizing it. Both are one request each
|
||||
// against a server on the same box, and a miss is a 404 rather than a wrong
|
||||
// article.
|
||||
func TitleCandidates(topic string) []string {
|
||||
if topic == "" {
|
||||
return nil
|
||||
}
|
||||
r := []rune(topic)
|
||||
up := unicode.ToUpper(r[0])
|
||||
if up == r[0] {
|
||||
return []string{topic}
|
||||
}
|
||||
return []string{topic, string(up) + string(r[1:])}
|
||||
}
|
||||
|
||||
func isCopula(w string) bool {
|
||||
switch w {
|
||||
case "такое", "такой", "такая", "такие", "is", "are", "was", "were":
|
||||
|
||||
@@ -49,3 +49,19 @@ func TestTitlePathEscapesAndUnderscores(t *testing.T) {
|
||||
t.Errorf("TitlePath = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// A ZIM title carries a leading capital and the utterance does not. The spoken
|
||||
// form is still tried first, so a title that begins lowercase on purpose keeps
|
||||
// its chance.
|
||||
func TestTitleCandidatesTryTheSpokenFormFirst(t *testing.T) {
|
||||
got := TitleCandidates("фотосинтез")
|
||||
if len(got) != 2 || got[0] != "фотосинтез" || got[1] != "Фотосинтез" {
|
||||
t.Errorf("TitleCandidates = %q", got)
|
||||
}
|
||||
if got := TitleCandidates("TCP"); len(got) != 1 || got[0] != "TCP" {
|
||||
t.Errorf("an already-capital topic was tried twice: %q", got)
|
||||
}
|
||||
if got := TitleCandidates(""); got != nil {
|
||||
t.Errorf("TitleCandidates(\"\") = %q, want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Raw transcript, two weeks of usage
|
||||
|
||||
YOU: что такое TCP?
|
||||
MAVEN: есть такое: Tcp/Ip — Википедия — TCP позволяет регулировать нагрузку на сеть, а также уменьшать время ожидания данных при передаче на большие расстояния. Более того, TCP гарантирует, что полученные ...
|
||||
[2.1s src=search t=666]
|
||||
|
||||
YOU: кто такой Линус Торвальдс?
|
||||
MAVEN: вот что я нашла: Торвальдс, Линус — Википедия — Ли́нус Бенедикт То́рвальдс (встречается написание Ту́рвальдс, швед. Linus Benedict Torvalds МФА: [ˈliːn.ɵs ˈtuːr.valds] о файле; род. 28 декабря 1969, Хельсинки) — финско-американский программист, создатель ядра Linux.
|
||||
[0.8s src=search t=667]
|
||||
|
||||
YOU: что такое фотосинтез
|
||||
MAVEN: нашла вот это: Фотосинтез — Фотосинтез
|
||||
Схематическое изображение процесса фотосинтеза, происходящего в растениях
|
||||
Упрощённое уравнение фотосинтеза, происходящего в растениях
|
||||
Упрощённая схема фотосинтеза
|
||||
Фотоси́нтез (от др.-греч. φῶς — « свет » и σύνθεσις — «соединённый», «складной», «связывание», « синтез ») — сл…
|
||||
[0.6s src=kiwix t=668]
|
||||
|
||||
YOU: кто написал Войну и мир
|
||||
MAVEN: вот что я нашла: Война и мир — Война и мир
|
||||
Война и мир
|
||||
Война и миръ
|
||||
Обложка издания 1873 года
|
||||
Жанр
|
||||
роман-эпопея
|
||||
Автор
|
||||
Лев Николаевич Толстой
|
||||
Язык оригинала
|
||||
русский, фрагменты на французском и немецком языках
|
||||
Дата написания
|
||||
1863—1869, 1873
|
||||
Дата первой публикации
|
||||
1865—1869
|
||||
Предыдущее
|
||||
Казаки
|
||||
Следующее
|
||||
Анна Каренина
|
||||
Тек…
|
||||
[12.4s src=kiwix t=669]
|
||||
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
{"reply": "есть такое: Tcp/Ip — Википедия — TCP позволяет регулировать нагрузку на сеть, а также уменьшать время ожидания данных при передаче на большие расстояния. Более того, TCP гарантирует, что полученные ...", "source": "search", "trace": "666", "secs": 2.1235475060530007, "day": 0, "n": 1, "utterance": "что такое TCP?"}
|
||||
{"reply": "вот что я нашла: Торвальдс, Линус — Википедия — Ли́нус Бенедикт То́рвальдс (встречается написание Ту́рвальдс, швед. Linus Benedict Torvalds МФА: [ˈliːn.ɵs ˈtuːr.valds] о файле; род. 28 декабря 1969, Хельсинки) — финско-американский программист, создатель ядра Linux.", "source": "search", "trace": "667", "secs": 0.7689587379572913, "day": 0, "n": 2, "utterance": "кто такой Линус Торвальдс?"}
|
||||
{"reply": "нашла вот это: Фотосинтез — Фотосинтез\nСхематическое изображение процесса фотосинтеза, происходящего в растениях\nУпрощённое уравнение фотосинтеза, происходящего в растениях\nУпрощённая схема фотосинтеза\nФотоси́нтез (от др.-греч. φῶς — « свет » и σύνθεσις — «соединённый», «складной», «связывание», « синтез ») — сл…", "source": "kiwix", "trace": "668", "secs": 0.575586927938275, "day": 0, "n": 3, "utterance": "что такое фотосинтез"}
|
||||
{"reply": "вот что я нашла: Война и мир — Война и мир\nВойна и мир\nВойна и миръ\nОбложка издания 1873 года\nЖанр\nроман-эпопея\nАвтор\nЛев Николаевич Толстой\nЯзык оригинала\nрусский, фрагменты на французском и немецком языках\nДата написания\n1863—1869, 1873\nДата первой публикации\n1865—1869\nПредыдущее\nКазаки\nСледующее\nАнна Каренина\nТек…", "source": "kiwix", "trace": "669", "secs": 12.44599153404124, "day": 0, "n": 4, "utterance": "кто написал Войну и мир"}
|
||||
Reference in New Issue
Block a user