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.
|
||||
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.
|
||||
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)) + " "
|
||||
if s == " " {
|
||||
return false
|
||||
return historyAskedHim, false
|
||||
}
|
||||
for _, r := range historyRecall {
|
||||
if strings.Contains(s, r) {
|
||||
return false
|
||||
return historyAskedHim, false
|
||||
}
|
||||
}
|
||||
for _, pair := range historyMarkersEn {
|
||||
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)
|
||||
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) {
|
||||
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
|
||||
@@ -142,7 +168,8 @@ const historyWindow = 24 * time.Hour
|
||||
// pass: the notes pass would otherwise answer this from whatever note happens
|
||||
// to be nearest, which reads as an answer and is not one.
|
||||
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
|
||||
}
|
||||
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 {
|
||||
// Claim the turn rather than fall through. "ничего не говорил" is the
|
||||
// true answer, and recall would answer it with an old note instead.
|
||||
if side == historyAskedHer {
|
||||
return "за последние сутки я ничего с твоих слов не записывала.", true
|
||||
}
|
||||
return "за последние сутки ты мне ничего такого не говорил.", true
|
||||
}
|
||||
if side == historyAskedHer {
|
||||
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
|
||||
// to recall, which answers it with an old note.
|
||||
func TestHistorySaysWhenThereIsNothing(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user