Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
988a5f1
Created basic test case structure
LeikeBaus May 19, 2026
7e19663
Added tests for finding note, appointment and reminder
LeikeBaus May 20, 2026
95b2b4f
Merge remote-tracking branch 'origin/feat/delete-items-by-voice' into…
LeikeBaus May 21, 2026
f0c3b64
Added tests for dispatch and created constants
LeikeBaus May 21, 2026
fc8629d
Merge pull request #25 from LeikeBaus/feat/unittest
LeikeBaus May 21, 2026
e39a947
Add confirmed note deletion by keyword
LikeARealGinger May 21, 2026
75010dc
Refactor note deletion and include deleted note title and id
LikeARealGinger May 21, 2026
5404b63
Added test cases when keyword doesn't match
LeikeBaus May 22, 2026
90f8bda
Add deletion of Appointment by keyword
Stipinator May 23, 2026
27cf91c
Add deletion of Reminder by keyword
Stipinator May 23, 2026
946e762
Add localized delete confirmation choices
LikeARealGinger May 25, 2026
ab8ef53
refactor assisntant.py dispatcher for confirmation handling
MarcelloHut May 27, 2026
03f2402
add further dispatch confirmations (eng, ita)
MarcelloHut May 27, 2026
785ddbd
implement logic for pending delete handling; add handling of delete t…
MarcelloHut May 27, 2026
0681f97
add further unit tests for coverage
MarcelloHut May 27, 2026
3735914
Merge pull request #27 from LeikeBaus/develop/Delete-Handling-and-Sup…
LeikeBaus May 27, 2026
4524e9a
Use localized delete confirmation choices
LikeARealGinger May 27, 2026
b3dc810
Creat_Pop-Up_delete_confirm
Stipinator May 27, 2026
96c85f9
move delet_note for overview
Stipinator May 27, 2026
2788bf0
Merge pull request #28 from LeikeBaus/delete_confimation_improval
MarcelloHut May 27, 2026
a830e75
Merge pull request #29 from LeikeBaus/develop/delete-items-by-voice
LeikeBaus May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,54 @@
},
},
},
{
"type": "function",
"function": {
"name": "delete_note",
"description": "Delete a saved note, found by keyword.",
"parameters": {
"type": "object",
"properties": {
"keyword": {"type": "string", "description": "Keyword from the note title or content"},
"confirmed": {"type": "boolean", "description": "True only after the user confirmed deletion",
"default": False},
},
"required": ["keyword"],
},
},
},
{
"type": "function",
"function": {
"name": "delete_appointment",
"description": "Delete a saved appointment, found by keyword.",
"parameters": {
"type": "object",
"properties": {
"keyword": {"type": "string", "description": "Keyword from the appointment title or description"},
"confirmed": {"type": "boolean", "description": "True only after the user confirmed deletion",
"default": False},
},
"required": ["keyword"],
},
},
},
{
"type": "function",
"function": {
"name": "delete_reminder",
"description": "Delete a saved reminder, found by keyword.",
"parameters": {
"type": "object",
"properties": {
"keyword": {"type": "string", "description": "Keyword from the reminder message"},
"confirmed": {"type": "boolean", "description": "True only after the user confirmed deletion",
"default": False},
},
"required": ["keyword"],
},
},
},
{
"type": "function",
"function": {
Expand Down Expand Up @@ -239,6 +287,27 @@ def _dispatch(fc: dict) -> str:
else:
return locales.get("list_not_found", title=args.get("list_title", ""))

elif name == "delete_note":
keyword = args.get("keyword", "")
note = db.find_note_by_keyword(keyword)
if not note:
return locales.get("note_not_found", keyword=keyword)
return f"__confirm_delete__:note:{note['id']}"

elif name == "delete_appointment":
keyword = args.get("keyword", "")
appointment = db.find_appointment_by_keyword(keyword)
if not appointment:
return locales.get("appointment_not_found", keyword=keyword)
return f"__confirm_delete__:appointment:{appointment['id']}"

elif name == "delete_reminder":
keyword = args.get("keyword", "")
reminder = db.find_reminder_by_keyword(keyword)
if not reminder:
return locales.get("reminder_not_found", keyword=keyword)
return f"__confirm_delete__:reminder:{reminder['id']}"

elif name == "create_appointment":
aid = db.create_appointment(
title=args.get("title", ""),
Expand Down
54 changes: 54 additions & 0 deletions database.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,24 @@ def find_list_by_title(title: str) -> dict | None:
return None


def find_note_by_keyword(keyword: str) -> dict | None:
"""Find a note by fuzzy title/content match. Returns dict or None."""
target = keyword.strip().lower()
if not target:
return None
keyword_pattern = f"%{target}%"
with _lock:
c = _conn()
row = c.execute(
"SELECT * FROM notes"
" WHERE lower(title) LIKE ? OR lower(content) LIKE ?"
" ORDER BY updated_at DESC",
(keyword_pattern, keyword_pattern),
).fetchone()
c.close()
return dict(row) if row else None


def get_all_notes() -> list[dict]:
with _lock:
c = _conn()
Expand Down Expand Up @@ -216,6 +234,24 @@ def get_appointments(from_dt: str | None = None, to_dt: str | None = None) -> li
return [dict(r) for r in rows]


def find_appointment_by_keyword(keyword: str) -> dict | None:
"""Find an appointment by fuzzy title/description match. Returns dict or None."""
target = keyword.strip().lower()
if not target:
return None
keyword_pattern = f"%{target}%"
with _lock:
c = _conn()
row = c.execute(
"SELECT * FROM appointments"
" WHERE lower(title) LIKE ? OR lower(description) LIKE ?"
" ORDER BY dt ASC",
(keyword_pattern, keyword_pattern),
).fetchone()
c.close()
return dict(row) if row else None


def delete_appointment(aid: int):
with _lock:
c = _conn()
Expand Down Expand Up @@ -292,6 +328,24 @@ def get_pending_reminders() -> list[dict]:
return [dict(r) for r in rows]


def find_reminder_by_keyword(keyword: str) -> dict | None:
"""Find a reminder by fuzzy message match. Returns dict or None."""
target = keyword.strip().lower()
if not target:
return None
keyword_pattern = f"%{target}%"
with _lock:
c = _conn()
row = c.execute(
"SELECT * FROM reminders"
" WHERE lower(message) LIKE ?"
" ORDER BY remind_at ASC",
(keyword_pattern,)
).fetchone()
c.close()
return dict(row) if row else None


def mark_reminder_notified(rid: int):
with _lock:
c = _conn()
Expand Down
94 changes: 90 additions & 4 deletions locales.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,55 @@

import config

LocaleValue = str | tuple[str, ...]

# ── String tables ─────────────────────────────────────────────────────────

_STRINGS: dict[str, dict[str, str]] = {
_STRINGS: dict[str, dict[str, LocaleValue]] = {
"en": {
# assistant.py — dispatch confirmations
"note_saved": "Note saved (#{nid})",
"list_saved": "List '{title}' saved ({count} items)",
"added_to_list": "Added to '{title}'",
"list_not_found": "List '{title}' not found",
"note_not_found": "Note matching '{keyword}' not found",
"note_deleted": "Note '{title}' deleted (#{nid})",
"appointment_created": "Appointment created: {title} ({dt})",
"appointment_not_found": "Appointment matching '{keyword}' not found",
"appointment_deleted": "Appointment '{title}' deleted (#{aid})",
"reminder_not_found": "Reminder matching '{keyword}' not found",
"reminder_deleted": "Reminder '{message}' deleted (#{rid})",
"reminder_set": "Reminder set: {dt}",
"delete_confirm_prompt": "Say yes within {seconds}s to delete this {item}",
"delete_confirm_repeat": "Please say yes or no ({seconds}s left)",
"delete_confirm_timeout": "Delete confirmation timed out",
"delete_cancelled": "Delete cancelled",
"delete_item_missing": "{item} was not found",
"delete_item_note": "note",
"delete_item_appointment": "appointment",
"delete_item_reminder": "reminder",
"confirm_delete_title": "Confirm delete {item}",
"field_name": "Name",
"field_created": "Created",
"field_event": "Event",
"field_remind": "Remind At",
"confirm_delete_warning": "This action cannot be undone.",
"btn_cancel": "Cancel",
"btn_delete": "Delete",
"listening_for_confirm": "Listening for voice confirmation...",
"unknown_command": "Unknown command: {name}",
"error": "Error: {detail}",
"not_understood": "I didn't understand the command",
"delete_confirmations": (
"yes", "yeah", "yep", "yup", "sure", "ok", "okay",
"confirm", "confirmed", "please do", "do it", "go ahead",
"delete", "delete it",
),
"delete_rejections": (
"no", "nope", "nah", "cancel", "stop", "abort", "do not",
"don't", "dont", "keep it", "never mind", "nevermind",
"leave it",
),

# assistant.py — system prompt fragments
"system_prompt": (
Expand Down Expand Up @@ -97,11 +132,44 @@
"list_saved": "Lista '{title}' salvata ({count} elementi)",
"added_to_list": "Aggiunto a '{title}'",
"list_not_found": "Lista '{title}' non trovata",
"note_not_found": "Nota con '{keyword}' non trovata",
"note_deleted": "Nota '{title}' eliminata (#{nid})",
"appointment_created": "Appuntamento creato: {title} ({dt})",
"appointment_not_found": "Appuntamento con '{keyword}' non trovato",
"appointment_deleted": "Appuntamento '{title}' eliminato (#{aid})",
"reminder_not_found": "Reminder con '{keyword}' non trovato",
"reminder_deleted": "Reminder '{message}' eliminato (#{rid})",
"reminder_set": "Reminder impostato: {dt}",
"delete_confirm_prompt": "Di' si entro {seconds}s per eliminare questo {item}",
"delete_confirm_repeat": "Di' si o no ({seconds}s rimasti)",
"delete_confirm_timeout": "Conferma eliminazione scaduta",
"delete_cancelled": "Eliminazione annullata",
"delete_item_missing": "{item} non trovato",
"delete_item_note": "nota",
"delete_item_appointment": "appuntamento",
"delete_item_reminder": "reminder",
"confirm_delete_title": "Conferma eliminazione {item}",
"field_name": "Nome",
"field_created": "Creato",
"field_event": "Evento",
"field_remind": "Ricorda il",
"confirm_delete_warning": "Questa azione non può essere annullata.",
"btn_cancel": "Annulla",
"btn_delete": "Elimina",
"listening_for_confirm": "In ascolto della conferma vocale...",
"unknown_command": "Comando sconosciuto: {name}",
"error": "Errore: {detail}",
"not_understood": "Non ho capito il comando",
"delete_confirmations": (
"si", "sì", "certo", "certamente", "ok", "okay",
"va bene", "confermo", "conferma", "procedi",
"elimina", "eliminalo", "cancella", "cancellalo",
),
"delete_rejections": (
"no", "nope", "annulla", "stop", "ferma", "fermati",
"aspetta", "lascia", "lascia stare", "lascia perdere",
"non eliminare", "non cancellare", "mantieni",
),

"system_prompt": (
"You are Writher, a voice assistant for productivity. "
Expand Down Expand Up @@ -172,17 +240,35 @@

# ── Public API ────────────────────────────────────────────────────────────

def _lookup(key: str) -> LocaleValue:
lang = getattr(config, "LANGUAGE", _FALLBACK)
table = _STRINGS.get(lang, _STRINGS[_FALLBACK])
return table.get(key, _STRINGS[_FALLBACK].get(key, key))


def get(key: str, **kwargs) -> str:
"""Return the localised string for *key*, formatted with *kwargs*.

Falls back to English if the key is missing in the active language.
"""
lang = getattr(config, "LANGUAGE", _FALLBACK)
table = _STRINGS.get(lang, _STRINGS[_FALLBACK])
template = table.get(key, _STRINGS[_FALLBACK].get(key, key))
template = _lookup(key)
if not isinstance(template, str):
return key
if kwargs:
try:
return template.format(**kwargs)
except (KeyError, IndexError):
return template
return template


def get_choices(key: str) -> tuple[str, ...]:
"""Return the localised choice list for *key*.

Use this for non-display locale entries, such as spoken confirmation
variants for destructive actions.
"""
choices = _lookup(key)
if isinstance(choices, tuple):
return choices
return (choices,)
Loading