Merge the history side fix (#191)
V-456. A question about what she recorded is answered as her turn, not his. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
This commit is contained in:
+40
-7
@@ -54,30 +54,56 @@ var historyMarkersEn = [][2]string{
|
|||||||
// answers a topic far better than a list of the last five facts does.
|
// answers a topic far better than a list of the last five facts does.
|
||||||
var historyRecall = []string{" про ", " об ", " о ", " about "}
|
var historyRecall = []string{" про ", " об ", " о ", " about "}
|
||||||
|
|
||||||
|
// historySide — whose turn the question asks about. The rows read are the same
|
||||||
|
// either way, because a tapped fact is one act seen from two sides, but the
|
||||||
|
// sentence is not: answering "что ты записала сегодня?" with "ты говорил…"
|
||||||
|
// hands the question back instead of answering it (Vikunja #456).
|
||||||
|
type historySide int
|
||||||
|
|
||||||
|
const (
|
||||||
|
historyAskedHim historySide = iota // "что я тебе говорил"
|
||||||
|
historyAskedHer // "что ты записала сегодня"
|
||||||
|
)
|
||||||
|
|
||||||
// isHistoryQuery reports whether he is asking what he told her.
|
// isHistoryQuery reports whether he is asking what he told her.
|
||||||
func isHistoryQuery(u string) bool {
|
func isHistoryQuery(u string) bool {
|
||||||
|
_, ok := historyAsks(u)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// historyAsks reports whether this is a history question, and whose turn it is
|
||||||
|
// about.
|
||||||
|
func historyAsks(u string) (historySide, bool) {
|
||||||
s := " " + strings.ToLower(strings.TrimSpace(u)) + " "
|
s := " " + strings.ToLower(strings.TrimSpace(u)) + " "
|
||||||
if s == " " {
|
if s == " " {
|
||||||
return false
|
return historyAskedHim, false
|
||||||
}
|
}
|
||||||
for _, r := range historyRecall {
|
for _, r := range historyRecall {
|
||||||
if strings.Contains(s, r) {
|
if strings.Contains(s, r) {
|
||||||
return false
|
return historyAskedHim, false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, pair := range historyMarkersEn {
|
for _, pair := range historyMarkersEn {
|
||||||
if strings.Contains(s, pair[0]) && strings.Contains(s, pair[1]) {
|
if strings.Contains(s, pair[0]) && strings.Contains(s, pair[1]) {
|
||||||
return true
|
if strings.Contains(pair[0], "you") {
|
||||||
|
return historyAskedHer, true
|
||||||
|
}
|
||||||
|
return historyAskedHim, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toks := historyTokens(s)
|
toks := historyTokens(s)
|
||||||
if !hasAny(toks, "что", "чего") {
|
if !hasAny(toks, "что", "чего") {
|
||||||
return false
|
return historyAskedHim, false
|
||||||
}
|
}
|
||||||
|
// His side is tested first: "отмечать" is on both verb lists, so "что я
|
||||||
|
// отметил" must not read as a question about her.
|
||||||
if hasAny(toks, firstPersonSubjects...) && hasVerbForm(toks, historySpokenVerbs) {
|
if hasAny(toks, firstPersonSubjects...) && hasVerbForm(toks, historySpokenVerbs) {
|
||||||
return true
|
return historyAskedHim, true
|
||||||
}
|
}
|
||||||
return hasAny(toks, secondPersonSubjects...) && hasVerbForm(toks, historyRecordedVerbs)
|
if hasAny(toks, secondPersonSubjects...) && hasVerbForm(toks, historyRecordedVerbs) {
|
||||||
|
return historyAskedHer, true
|
||||||
|
}
|
||||||
|
return historyAskedHim, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// historyTokens splits an utterance into bare words. The punctuation goes
|
// historyTokens splits an utterance into bare words. The punctuation goes
|
||||||
@@ -142,7 +168,8 @@ const historyWindow = 24 * time.Hour
|
|||||||
// pass: the notes pass would otherwise answer this from whatever note happens
|
// pass: the notes pass would otherwise answer this from whatever note happens
|
||||||
// to be nearest, which reads as an answer and is not one.
|
// to be nearest, which reads as an answer and is not one.
|
||||||
func (h *reactiveHandler) queryHistory(ctx context.Context, t *queryTurn) (string, bool) {
|
func (h *reactiveHandler) queryHistory(ctx context.Context, t *queryTurn) (string, bool) {
|
||||||
if !isHistoryQuery(t.dec.Utterance) {
|
side, ok := historyAsks(t.dec.Utterance)
|
||||||
|
if !ok {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
facts, err := h.api.RecentFacts(ctx, historyScan)
|
facts, err := h.api.RecentFacts(ctx, historyScan)
|
||||||
@@ -164,8 +191,14 @@ func (h *reactiveHandler) queryHistory(ctx context.Context, t *queryTurn) (strin
|
|||||||
if len(said) == 0 {
|
if len(said) == 0 {
|
||||||
// Claim the turn rather than fall through. "ничего не говорил" is the
|
// Claim the turn rather than fall through. "ничего не говорил" is the
|
||||||
// true answer, and recall would answer it with an old note instead.
|
// true answer, and recall would answer it with an old note instead.
|
||||||
|
if side == historyAskedHer {
|
||||||
|
return "за последние сутки я ничего с твоих слов не записывала.", true
|
||||||
|
}
|
||||||
return "за последние сутки ты мне ничего такого не говорил.", true
|
return "за последние сутки ты мне ничего такого не говорил.", true
|
||||||
}
|
}
|
||||||
|
if side == historyAskedHer {
|
||||||
|
return "я записала: " + strings.Join(said, "; "), true
|
||||||
|
}
|
||||||
return "ты говорил: " + strings.Join(said, "; "), true
|
return "ты говорил: " + strings.Join(said, "; "), true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,33 @@ func TestHistoryReadsOnlyWhatHeSaid(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The rows are the same either way, because a tapped fact is one act seen from
|
||||||
|
// two sides. The sentence is not: "что ты записала" answered with "ты говорил"
|
||||||
|
// hands the question back (Vikunja #456).
|
||||||
|
func TestHistoryAnswersTheSideItWasAsked(t *testing.T) {
|
||||||
|
now := time.Date(2026, 8, 4, 20, 0, 0, 0, time.UTC)
|
||||||
|
h, _ := historyHandler(now, ipc.Fact{Key: "water", Value: "выпил", Source: "tap:voice", Ts: now.Add(-time.Hour)})
|
||||||
|
|
||||||
|
his, ok := askHistory(h, "что я тебе говорил?")
|
||||||
|
if !ok || !strings.HasPrefix(his, "ты говорил") {
|
||||||
|
t.Errorf("reply = %q, ok = %v, want his side", his, ok)
|
||||||
|
}
|
||||||
|
hers, ok := askHistory(h, "что ты записала сегодня?")
|
||||||
|
if !ok || !strings.HasPrefix(hers, "я записала") {
|
||||||
|
t.Errorf("reply = %q, ok = %v, want her side", hers, ok)
|
||||||
|
}
|
||||||
|
// "отмечать" is on both verb lists, so his subject has to win.
|
||||||
|
if side, ok := historyAsks("что я отметил?"); !ok || side != historyAskedHim {
|
||||||
|
t.Errorf("historyAsks(что я отметил) = %v, %v", side, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
empty, _ := historyHandler(now)
|
||||||
|
none, ok := askHistory(empty, "что ты записала сегодня?")
|
||||||
|
if !ok || !strings.Contains(none, "не записывала") {
|
||||||
|
t.Errorf("empty reply = %q, ok = %v, want her side", none, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Nothing said is an answer of its own. Falling through would hand the question
|
// Nothing said is an answer of its own. Falling through would hand the question
|
||||||
// to recall, which answers it with an old note.
|
// to recall, which answers it with an old note.
|
||||||
func TestHistorySaysWhenThereIsNothing(t *testing.T) {
|
func TestHistorySaysWhenThereIsNothing(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user