From 92587863fe5ff98175efbd1cb5acaa648dd09fe7 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:34:37 +0100 Subject: [PATCH] fix(bot): stop ending offers and questions with a line about not answering (0.2.2) --- docs/how-it-works.md | 2 +- supermedicaldoctor/__init__.py | 2 +- supermedicaldoctor/bot.py | 9 ++++----- supermedicaldoctor/locales/en.yaml | 13 ++++++------- supermedicaldoctor/locales/es.yaml | 13 ++++++------- supermedicaldoctor/principles/en.md | 2 +- supermedicaldoctor/principles/es.md | 2 +- tests/test_bot.py | 10 ++++------ tests/test_chat.py | 9 ++++----- tests/test_edits.py | 2 +- tests/test_i18n.py | 7 +++++++ tests/test_principles.py | 4 ++-- 12 files changed, 38 insertions(+), 37 deletions(-) diff --git a/docs/how-it-works.md b/docs/how-it-works.md index 7ff6c1e..c8c9960 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -113,7 +113,7 @@ Telegram cannot parse goes again as plain text. The gestures go after the text, so one Telegram refuses never loses the answer. The bot's voice (`telegram.style`) is read by the assistant after the formatting rules. -Every offer says «Si no respondes, no hago nada.» and, untapped, gives way to «Sin respuesta: nada». +An offer nobody taps gives way to «Sin respuesta: nada». ## Approvals and calendars diff --git a/supermedicaldoctor/__init__.py b/supermedicaldoctor/__init__.py index 2691dc9..db29c65 100644 --- a/supermedicaldoctor/__init__.py +++ b/supermedicaldoctor/__init__.py @@ -1,3 +1,3 @@ """SuperMedicalDoctor: a household doctor assistant that lives in a Telegram group.""" -__version__ = "0.2.1" +__version__ = "0.2.2" diff --git a/supermedicaldoctor/bot.py b/supermedicaldoctor/bot.py index 7b10b77..0a53079 100644 --- a/supermedicaldoctor/bot.py +++ b/supermedicaldoctor/bot.py @@ -2970,15 +2970,14 @@ def say( thread: int | None = None, ) -> bool: """An assistant answer: plain text, a reply to the message, split at Telegram's limit. - With choices its last part carries them as buttons, for `users` to tap, and a line - saying nothing happens without an answer; without, one that ends with a question + With choices its last part carries them as buttons, for `users` to tap (a Sí/No pair with + a line saying how to answer something else); without, one that ends with a question carries a selective force_reply. False when a part could not be sent.""" key = secrets.token_hex(4) if offered else None asks = text.rstrip().endswith("?") text = render.brief(text) # a few lines, the rest folded: no page holds the detail here - if offered: - hint = self.t("yes_no_hint" if self.yes_no(offered) else "options_hint") - text = f"{text}\n\n{hint}" + if offered and self.yes_no(offered): + text = f"{text}\n\n{self.t('yes_no_hint')}" parts = render.split(text) sent = None for i, part in enumerate(parts): diff --git a/supermedicaldoctor/locales/en.yaml b/supermedicaldoctor/locales/en.yaml index 2cfaedf..5051b8e 100644 --- a/supermedicaldoctor/locales/en.yaml +++ b/supermedicaldoctor/locales/en.yaml @@ -47,7 +47,7 @@ appt_line: "{when}, {what}" appt_place: " ({place})" at_time: "{date} at {time}" prep_line: "• {text} ({hours} h before)" -appt_offer: "{m}, shall I put it in your calendar? If you do not answer, I do nothing." +appt_offer: "{m}, shall I put it in your calendar?" appt_calendar: "In the calendar" event_title: "Appointment: {what}" appt_calendar_turn: "[asked with the «In the calendar» button of an appointment] Put in my calendar one event «{title}» on {when} ({iso}), at {place}, with the preparation in the description ({prep}) and a reminder 24 h before." @@ -61,7 +61,7 @@ remind_prep_from: "⏰ {m}: from {start}, {text}: {what}, {day} at {time}." today: "today" tomorrow: "tomorrow" on_date: "on {date}" -followup: "{m}, how did the {what} appointment go? If they gave you a report or changes, tell me or send me the PDF.\n\nIf you do not answer, I do nothing." +followup: "{m}, how did the {what} appointment go? If they gave you a report or changes, tell me or send me the PDF." followup_yes: "Yes, I'll tell you" followup_no_log: "Appointment marked as done, with nothing new." tell_me: "{m}, tell me. I am listening." @@ -122,8 +122,7 @@ button_yes: "Yes" button_no: "No" button_approve: "Approve" button_reject: "Reject" -options_hint: "If you do not answer, I do nothing." -yes_no_hint: "Tap Yes or No. If you want something else, reply to this message. If you do not answer, I do nothing." +yes_no_hint: "Tap Yes or No. If you want something else, reply to this message." tap_gone: "This is no longer waiting for an answer." tap_not_yours: "Only {name} can answer this." ask_confirm: "{name}, do you approve?\n{summary}" @@ -134,7 +133,7 @@ ask_edited: "You edited your message, so I did not make this change. Nothing cha ask_preapproved: "Approved with the «In the calendar» button: {what}\n{result}" # --- reminders offered with buttons; /recordatorios ---------------------------------------- -remind_offer: "{name}, shall I remind you?\n{lines}\n\nIf you do not answer, I do nothing." +remind_offer: "{name}, shall I remind you?\n{lines}" remind_here: "Reminder here" remind_calendar: "In the calendar" remind_nothing: "No" @@ -158,7 +157,7 @@ settings_day_before_at: "the evening-before reminder, at {time}" settings_followup_on: "I ask after each appointment" settings_followup_off: "I no longer ask after each appointment" settings_instructions: "my standing instructions" -instr_ask: "{name}, this removes or changes lines of my instructions. Shall I leave them like this?\n{lines}\n\nIf you do not answer, I do nothing." +instr_ask: "{name}, this removes or changes lines of my instructions. Shall I leave them like this?\n{lines}" instr_kept: "OK, I keep my instructions as they were." edit_refused: "I could not make that change to the files: it is not allowed here or I did not understand it. I touched nothing." edit_check_failed: "That change left the data with errors, so I touched nothing." @@ -166,7 +165,7 @@ edited: "Updated:" edit_file: "• {path}" edit_deleted: "• {path}: deleted" edit_undo_said: "changes to {paths}" -edit_ask: "{name}, shall I make this change?\n{lines}\n\nIf you do not answer, I do nothing." +edit_ask: "{name}, shall I make this change?\n{lines}" edit_kept: "OK, I leave it as it was." edit_moved: "Those files changed since I asked, so I touched nothing. Ask me again." instr_moved: "My instructions changed since I asked, so I touched nothing. Ask me again." diff --git a/supermedicaldoctor/locales/es.yaml b/supermedicaldoctor/locales/es.yaml index c7f292e..d69e57c 100644 --- a/supermedicaldoctor/locales/es.yaml +++ b/supermedicaldoctor/locales/es.yaml @@ -47,7 +47,7 @@ appt_line: "{when}, {what}" appt_place: " ({place})" at_time: "{date} a las {time}" prep_line: "• {text} ({hours} h antes)" -appt_offer: "{m}, ¿la pongo en tu calendario? Si no respondes, no hago nada." +appt_offer: "{m}, ¿la pongo en tu calendario?" appt_calendar: "En el calendario" event_title: "Cita: {what}" appt_calendar_turn: "[pedido con el botón «En el calendario» de una cita] Pon en mi calendario un evento «{title}» el {when} ({iso}), en {place}, con la preparación en la descripción ({prep}) y un aviso 24 h antes." @@ -61,7 +61,7 @@ remind_prep_from: "⏰ {m}: a partir de las {start}, {text}: {what}, {day} a las today: "hoy" tomorrow: "mañana" on_date: "el {date}" -followup: "{m}, ¿qué tal la cita de {what}? Si te dieron un informe o cambios, cuéntamelo o mándame el PDF.\n\nSi no respondes, no hago nada." +followup: "{m}, ¿qué tal la cita de {what}? Si te dieron un informe o cambios, cuéntamelo o mándame el PDF." followup_yes: "Sí, te cuento" followup_no_log: "Cita marcada como hecha, sin más novedades." tell_me: "{m}, cuéntame. Te leo." @@ -122,8 +122,7 @@ button_yes: "Sí" button_no: "No" button_approve: "Aprobar" button_reject: "Rechazar" -options_hint: "Si no respondes, no hago nada." -yes_no_hint: "Toca Sí o No. Si quieres otra cosa, responde a este mensaje. Si no respondes, no hago nada." +yes_no_hint: "Toca Sí o No. Si quieres otra cosa, responde a este mensaje." tap_gone: "Esto ya no está esperando respuesta." tap_not_yours: "Esto solo lo puede responder {name}." ask_confirm: "{name}, ¿lo apruebas?\n{summary}" @@ -134,7 +133,7 @@ ask_edited: "Has editado tu mensaje, así que no he hecho este cambio. No he cam ask_preapproved: "Aprobado con el botón «En el calendario»: {what}\n{result}" # --- recordatorios ofrecidos con botones; /recordatorios ----------------------------------- -remind_offer: "{name}, ¿quieres que te lo recuerde?\n{lines}\n\nSi no respondes, no hago nada." +remind_offer: "{name}, ¿quieres que te lo recuerde?\n{lines}" remind_here: "Recordatorio aquí" remind_calendar: "En el calendario" remind_nothing: "No" @@ -158,7 +157,7 @@ settings_day_before_at: "el aviso de la víspera, a las {time}" settings_followup_on: "pregunto después de cada cita" settings_followup_off: "ya no pregunto después de cada cita" settings_instructions: "mis instrucciones" -instr_ask: "{name}, esto quita o cambia líneas de mis instrucciones. ¿Lo dejo así?\n{lines}\n\nSi no respondes, no hago nada." +instr_ask: "{name}, esto quita o cambia líneas de mis instrucciones. ¿Lo dejo así?\n{lines}" instr_kept: "Vale, dejo mis instrucciones como estaban." edit_refused: "No he podido hacer ese cambio en los archivos: aquí no está permitido o no lo he entendido bien. No he tocado nada." edit_check_failed: "Ese cambio dejaba los datos con errores, así que no he tocado nada." @@ -166,7 +165,7 @@ edited: "Actualizado:" edit_file: "• {path}" edit_deleted: "• {path}: borrado" edit_undo_said: "cambios en {paths}" -edit_ask: "{name}, ¿hago este cambio?\n{lines}\n\nSi no respondes, no hago nada." +edit_ask: "{name}, ¿hago este cambio?\n{lines}" edit_kept: "Vale, lo dejo como estaba." edit_moved: "Esos archivos han cambiado desde que lo pregunté, así que no he tocado nada. Pídemelo otra vez." instr_moved: "Mis instrucciones han cambiado desde que lo pregunté, así que no he tocado nada. Pídemelo otra vez." diff --git a/supermedicaldoctor/principles/en.md b/supermedicaldoctor/principles/en.md index 6382e84..3bdf836 100644 --- a/supermedicaldoctor/principles/en.md +++ b/supermedicaldoctor/principles/en.md @@ -22,7 +22,7 @@ They hold on every turn. The rules after them say how they apply to this bot; no if it changes anything, say so. The text with a photo or a file is a message. 7. **Reversible now, irreversible with a tap.** Do at once what was asked and can be undone. Anything that leaves the chat (a calendar, a file others see, a purchase) waits until someone taps Approve. - An offer nobody answers does nothing ("If you do not answer, I do nothing."). + An offer nobody answers does nothing. 8. **Proactive, not pushy.** When you can do something useful that was not asked (a reminder, a calendar entry, a re-check), offer it with buttons, once. If nobody takes it, do not repeat that offer in your next answer. diff --git a/supermedicaldoctor/principles/es.md b/supermedicaldoctor/principles/es.md index 0199d78..f0ffa9d 100644 --- a/supermedicaldoctor/principles/es.md +++ b/supermedicaldoctor/principles/es.md @@ -22,7 +22,7 @@ Valen en cada turno. Las reglas que vienen después dicen cómo se aplican en es algo, dilo. El texto que acompaña a una foto o a un archivo es un mensaje. 7. **Lo reversible, ya; lo irreversible, con un toque.** Haz en el momento lo que te piden y se puede deshacer. Lo que sale del chat (un calendario, un archivo que ven otros, una compra) espera a que - alguien pulse Aprobar. Una oferta que nadie contesta no hace nada («Si no respondes, no hago nada.»). + alguien pulse Aprobar. Una oferta que nadie contesta no hace nada. 8. **Proactivo, sin insistir.** Cuando puedas hacer algo útil que no te han pedido (un recordatorio, una cita en el calendario, volver a comprobar algo), ofrécelo con botones, una vez. Si nadie la coge, no repitas esa oferta en la respuesta siguiente. diff --git a/tests/test_bot.py b/tests/test_bot.py index 65bf5fd..a10bcc5 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -259,9 +259,7 @@ def test_a_new_appointment_is_offered_for_its_persons_calendar(make_bot): plugin(bot).REPLIES[:] = [record("Vale.", ADD)] say(bot, "x", user=PABLO, mention=True) offer = last_sent(bot) - assert offer["text"].endswith( - "@lucia_tg, ¿la pongo en tu calendario? Si no respondes, no hago nada." - ) + assert offer["text"].endswith("@lucia_tg, ¿la pongo en tu calendario?") data = buttons(offer) assert [t for t, _ in data] == ["En el calendario", "No"] tap(bot, data[0][1], user=PABLO) @@ -421,7 +419,7 @@ def test_the_ask_after_an_appointment_and_its_no_marks_it_done(make_bot, data_re bot.now.at = at(TODAY.replace(day=12), 12, 30) bot.tick(bot.now()) ask = next(p for p in sent(bot) if "¿qué tal la cita de Analítica de control?" in p["text"]) - assert ask["text"].endswith("Si no respondes, no hago nada.") + assert ask["text"].endswith("cuéntamelo o mándame el PDF.") yes, no = buttons(ask) assert (yes[0], no[0]) == ("Sí, te cuento", "No") tap(bot, no[1], user=PABLO) @@ -467,7 +465,7 @@ def test_a_reminder_for_someone_else_is_offered_to_them(make_bot): offer = last_sent(bot) assert offer["text"] == ( 'Pablo, ¿quieres que te lo recuerde?\n' - "jueves 11 de marzo a las 8:00: Toma el omega\n\nSi no respondes, no hago nada." + "jueves 11 de marzo a las 8:00: Toma el omega" ) here = buttons(offer)[0][1] tap(bot, here, user=LUCIA) @@ -562,7 +560,7 @@ def test_a_whole_text_that_drops_lines_is_asked_and_nothing_changes_without_si(m ) assert "− Regla 1 de la casa." in ask["text"] and "− Regla 10 de la casa." in ask["text"] assert "+ Contesta siempre con la fuente." in ask["text"] - assert ask["text"].endswith("Si no respondes, no hago nada.") + assert ask["text"].endswith("+ Contesta siempre con la fuente.") yes, no = buttons(ask) assert (yes[0], no[0]) == ("Sí", "No") tap(bot, yes[1], user=LUCIA) # not an admin diff --git a/tests/test_chat.py b/tests/test_chat.py index ccb06a8..f2cf50a 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -54,8 +54,7 @@ def test_choices_come_as_buttons_and_a_tap_is_that_persons_next_message(make_bot say(bot, "me han puesto hierro", mention=True) answer = last_sent(bot) assert answer["text"] == ( - "¿Lo apunto?\n\nToca Sí o No. Si quieres otra cosa, responde a este mensaje. Si no " - "respondes, no hago nada." + "¿Lo apunto?\n\nToca Sí o No. Si quieres otra cosa, responde a este mensaje." ) yes = buttons(answer)[0][1] tap(bot, yes, user=PABLO) @@ -64,12 +63,12 @@ def test_choices_come_as_buttons_and_a_tap_is_that_persons_next_message(make_bot assert contents(plugin(bot).CALLS[-1])[-1] == "Lucía: Sí" -def test_choices_carry_only_the_no_answer_line_under_them(make_bot): +def test_choices_carry_no_line_under_them(make_bot): bot = make_bot() plugin(bot).REPLIES[:] = ["¿Cuál?\n\n```options\n- Mañana\n- Tarde\n```"] say(bot, "x", mention=True) - # the buttons say what they do: no line inviting something else - assert last_sent(bot)["text"] == "¿Cuál?\n\nSi no respondes, no hago nada." + # the buttons say what they do, and untapped they expire: no line under the question + assert last_sent(bot)["text"] == "¿Cuál?" def test_choices_nobody_taps_expire_into_nothing(make_bot): diff --git a/tests/test_edits.py b/tests/test_edits.py index 0412639..5eaaadc 100644 --- a/tests/test_edits.py +++ b/tests/test_edits.py @@ -147,7 +147,7 @@ def test_a_proposed_edit_waits_for_si_and_no_leaves_it(make_bot, data_repo): say(bot, "hoy he ido al centro", user=LUCIA, mention=True) ask = last_sent(bot) assert ask["text"].startswith("@lucia_tg, ¿hago este cambio?\n• people/lucia/notes.md\n+ ") - assert ask["text"].endswith("Si no respondes, no hago nada.") + assert "no respondes" not in ask["text"] assert data_repo.remote_log() == before no = buttons(ask)[1][1] tap(bot, no, user=PABLO) diff --git a/tests/test_i18n.py b/tests/test_i18n.py index 6800507..e608656 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -74,6 +74,13 @@ def test_every_english_key_exists_with_the_same_placeholders(locale): assert shape(en[key]) == shape(other[key]), key +@pytest.mark.parametrize("locale", ["en", "es"]) +def test_no_message_says_what_happens_without_an_answer(locale): + # an untapped offer just expires; no message adds a line saying so + for path, s in walk(_load(locale)): + assert "no respondes" not in s.lower() and "do not answer" not in s.lower(), path + + def test_the_shape_check_can_fail(): """Positive control: a placeholder renamed in a translation is caught.""" assert shape("{m}, cuéntame") != shape("{name}, cuéntame") diff --git a/tests/test_principles.py b/tests/test_principles.py index 9654a3c..c2eea5b 100644 --- a/tests/test_principles.py +++ b/tests/test_principles.py @@ -9,8 +9,8 @@ # The shared assistant principles, byte for byte. A change to them lands here on purpose, never by # accident: copy the new text into supermedicaldoctor/principles/ and update the hash. SHA256 = { - "en": "713784a662236621bf63709f07ac811199b2bf779e95b986a475e2087ab1713a", - "es": "dbd6efd497a264a8dd43e6f5b0ceeb78d3b97c5d61536fceb03505c970937bd1", + "en": "1132dd4ab7570d7bdf96146532e2f8d71052091ef657c9332aff5a9fe3a6cae3", + "es": "1d47e3c8292543d302add5d0caab2ccda46b95ec1ba49d661d035f6f31e8c312", } HEADING = {"en": "## Principles", "es": "## Principios"} PEOPLE = {"lucia": "Lucía", "pablo": "Pablo"}