Matt Vaughn Claude Sonnet 4.5 commited on
Commit
87b31d1
Β·
1 Parent(s): 80310c1

Fix: Add privacy protections to persistent memory system

Browse files

Revises memory_usage.txt prompt to enforce moderate privacy level:
- ALLOWED: First names, general region, occupation category, interests, learning data
- EXCLUDED: Age, specific location, family names, sensitive details, travel dates

Updates remember.py tool examples to align with privacy guidelines.
Implements implicit consent model via SUPERMEMORY_API_KEY configuration.

Includes minor ruff formatting fixes for code style consistency.

πŸ€– Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

reachy_mini_language_tutor/console.py CHANGED
@@ -421,10 +421,9 @@ class LocalStream:
421
  # GET /settings/idle -> get current idle signal configuration
422
  @self._settings_app.get("/settings/idle")
423
  def _get_idle_settings() -> JSONResponse:
424
- return JSONResponse({
425
- "enable_idle_signals": config.ENABLE_IDLE_SIGNALS,
426
- "idle_signal_timeout": config.IDLE_SIGNAL_TIMEOUT
427
- })
428
 
429
  # POST /settings/idle -> update and persist idle signal settings
430
  @self._settings_app.post("/settings/idle")
@@ -435,8 +434,7 @@ class LocalStream:
435
  # Validate timeout range
436
  if not (30 <= timeout <= 900):
437
  return JSONResponse(
438
- {"ok": False, "error": "timeout_out_of_range", "min": 30, "max": 900},
439
- status_code=400
440
  )
441
 
442
  # Update config immediately (applies to current session)
@@ -594,8 +592,7 @@ class LocalStream:
594
  self._robot.media.audio.clear_output_buffer()
595
  else:
596
  logger.warning(
597
- "clear_output_buffer() not available on this SDK version, "
598
- "only clearing handler output queue"
599
  )
600
  self.handler.output_queue = asyncio.Queue()
601
 
 
421
  # GET /settings/idle -> get current idle signal configuration
422
  @self._settings_app.get("/settings/idle")
423
  def _get_idle_settings() -> JSONResponse:
424
+ return JSONResponse(
425
+ {"enable_idle_signals": config.ENABLE_IDLE_SIGNALS, "idle_signal_timeout": config.IDLE_SIGNAL_TIMEOUT}
426
+ )
 
427
 
428
  # POST /settings/idle -> update and persist idle signal settings
429
  @self._settings_app.post("/settings/idle")
 
434
  # Validate timeout range
435
  if not (30 <= timeout <= 900):
436
  return JSONResponse(
437
+ {"ok": False, "error": "timeout_out_of_range", "min": 30, "max": 900}, status_code=400
 
438
  )
439
 
440
  # Update config immediately (applies to current session)
 
592
  self._robot.media.audio.clear_output_buffer()
593
  else:
594
  logger.warning(
595
+ "clear_output_buffer() not available on this SDK version, only clearing handler output queue"
 
596
  )
597
  self.handler.output_queue = asyncio.Queue()
598
 
reachy_mini_language_tutor/gradio_tutor_selector.py CHANGED
@@ -33,16 +33,10 @@ class TutorSelectorUI:
33
 
34
  # Tutor metadata
35
  self.tutor_metadata = self._load_metadata()
36
- self.tutor_profiles = [
37
- {**data, "id": profile_id}
38
- for profile_id, data in self.tutor_metadata.items()
39
- ]
40
 
41
  # Track current selection (find default profile index)
42
- self.selected_index = next(
43
- (i for i, p in enumerate(self.tutor_profiles) if p["id"] == "default"),
44
- 0
45
- )
46
 
47
  def _load_metadata(self) -> dict[str, Any]:
48
  """Load tutor metadata from JSON file.
@@ -101,9 +95,9 @@ class TutorSelectorUI:
101
  checkmark = ""
102
  if is_selected:
103
  selected_styles = f"""
104
- background: linear-gradient(135deg, {profile['accent_color']}10 0%, {profile['accent_color']}20 100%);
105
- border-left: 6px solid {profile['accent_color']};
106
- box-shadow: 0 4px 16px {profile['accent_color']}40;
107
  transform: scale(1.02);
108
  """
109
  checkmark = f"""
@@ -111,7 +105,7 @@ class TutorSelectorUI:
111
  position: absolute;
112
  top: 12px;
113
  right: 12px;
114
- background: {profile['accent_color']};
115
  color: white;
116
  width: 28px;
117
  height: 28px;
@@ -131,12 +125,12 @@ class TutorSelectorUI:
131
  <div class="tutor-card" style="{selected_styles} position: relative;">
132
  {checkmark}
133
  <div class="tutor-header">
134
- <span class="tutor-flag">{profile['flag_emoji']}</span>
135
- <h3 class="tutor-name">{profile['display_name']}</h3>
136
  </div>
137
- <p class="tutor-language">{profile['language']}</p>
138
- <p class="tutor-description">{profile['short_description']}</p>
139
- <span class="tutor-level">{profile['level']}</span>
140
  </div>
141
  """
142
 
@@ -156,7 +150,7 @@ class TutorSelectorUI:
156
  font-size: clamp(2rem, 5vw, 3rem);
157
  font-weight: 700;
158
  letter-spacing: -0.02em;
159
- background: linear-gradient(135deg, {profile['accent_color']} 0%, {profile['accent_color']}99 50%, {profile['accent_color']}66 100%);
160
  -webkit-background-clip: text;
161
  -webkit-text-fill-color: transparent;
162
  background-clip: text;
@@ -164,7 +158,7 @@ class TutorSelectorUI:
164
  padding: 16px 0;
165
  text-align: center;
166
  ">
167
- {profile['flag_emoji']} {profile['display_name']}
168
  </h1>
169
  """
170
 
@@ -239,6 +233,7 @@ class TutorSelectorUI:
239
  blocks: Gradio Blocks context (stream.ui).
240
 
241
  """
 
242
  # Tutor card selection handler
243
  async def _on_tutor_selected(evt: gr.SelectData) -> tuple[str, gr.Dataset, str]:
244
  """Handle tutor card selection and apply personality.
@@ -271,7 +266,11 @@ class TutorSelectorUI:
271
  logger.error(f"Error applying tutor profile: {e}", exc_info=True)
272
  # Keep current state on error
273
  current_profile = self.tutor_profiles[self.selected_index]
274
- return self._render_title(current_profile), gr.Dataset(samples=self._render_all_cards()), f"❌ Error switching tutor: {e}"
 
 
 
 
275
 
276
  # Wire the selection event within the Blocks context
277
  with blocks:
 
33
 
34
  # Tutor metadata
35
  self.tutor_metadata = self._load_metadata()
36
+ self.tutor_profiles = [{**data, "id": profile_id} for profile_id, data in self.tutor_metadata.items()]
 
 
 
37
 
38
  # Track current selection (find default profile index)
39
+ self.selected_index = next((i for i, p in enumerate(self.tutor_profiles) if p["id"] == "default"), 0)
 
 
 
40
 
41
  def _load_metadata(self) -> dict[str, Any]:
42
  """Load tutor metadata from JSON file.
 
95
  checkmark = ""
96
  if is_selected:
97
  selected_styles = f"""
98
+ background: linear-gradient(135deg, {profile["accent_color"]}10 0%, {profile["accent_color"]}20 100%);
99
+ border-left: 6px solid {profile["accent_color"]};
100
+ box-shadow: 0 4px 16px {profile["accent_color"]}40;
101
  transform: scale(1.02);
102
  """
103
  checkmark = f"""
 
105
  position: absolute;
106
  top: 12px;
107
  right: 12px;
108
+ background: {profile["accent_color"]};
109
  color: white;
110
  width: 28px;
111
  height: 28px;
 
125
  <div class="tutor-card" style="{selected_styles} position: relative;">
126
  {checkmark}
127
  <div class="tutor-header">
128
+ <span class="tutor-flag">{profile["flag_emoji"]}</span>
129
+ <h3 class="tutor-name">{profile["display_name"]}</h3>
130
  </div>
131
+ <p class="tutor-language">{profile["language"]}</p>
132
+ <p class="tutor-description">{profile["short_description"]}</p>
133
+ <span class="tutor-level">{profile["level"]}</span>
134
  </div>
135
  """
136
 
 
150
  font-size: clamp(2rem, 5vw, 3rem);
151
  font-weight: 700;
152
  letter-spacing: -0.02em;
153
+ background: linear-gradient(135deg, {profile["accent_color"]} 0%, {profile["accent_color"]}99 50%, {profile["accent_color"]}66 100%);
154
  -webkit-background-clip: text;
155
  -webkit-text-fill-color: transparent;
156
  background-clip: text;
 
158
  padding: 16px 0;
159
  text-align: center;
160
  ">
161
+ {profile["flag_emoji"]} {profile["display_name"]}
162
  </h1>
163
  """
164
 
 
233
  blocks: Gradio Blocks context (stream.ui).
234
 
235
  """
236
+
237
  # Tutor card selection handler
238
  async def _on_tutor_selected(evt: gr.SelectData) -> tuple[str, gr.Dataset, str]:
239
  """Handle tutor card selection and apply personality.
 
266
  logger.error(f"Error applying tutor profile: {e}", exc_info=True)
267
  # Keep current state on error
268
  current_profile = self.tutor_profiles[self.selected_index]
269
+ return (
270
+ self._render_title(current_profile),
271
+ gr.Dataset(samples=self._render_all_cards()),
272
+ f"❌ Error switching tutor: {e}",
273
+ )
274
 
275
  # Wire the selection event within the Blocks context
276
  with blocks:
reachy_mini_language_tutor/prompts/language_tutoring/memory_usage.txt CHANGED
@@ -1,5 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ## MEMORY USAGE
2
- You have persistent memory of this learner across sessions. Use it wisely:
3
 
4
  RECALL: Use the recall tool to search your memory when:
5
  - **Starting a new session** (CRITICAL: Check for their name and personal info FIRST!)
@@ -11,18 +40,30 @@ RECALL: Use the recall tool to search your memory when:
11
  REMEMBER: Use the remember tool to store important observations:
12
 
13
  **Personal Information** (category: personal):
14
- - **Identity basics**: Name (ALWAYS ask and store on first session!), age, location, occupation
15
- - **Interests & hobbies**: Favorite foods, music, sports, activities, books, movies
16
- - **Learning context**: Why learning [LANGUAGE], travel plans, [LANGUAGE]-speaking connections, target goals
17
- - **Personal relationships**: Family members mentioned, friends, pets, cultural connections
18
- - **Life context**: Work/school details, living situation, cultural background
19
-
20
- Examples:
21
- - "Learner's name is [Example Name]" (category: personal)
22
- - "Lives in [Example City], works as [Example Occupation]" (category: personal)
23
- - "Learning [LANGUAGE] to [Example Reason]" (category: personal)
24
- - "Loves [Example Interest]" (category: personal)
25
- - "Has [Example Connection]" (category: personal)
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  **Learning Progress** (use existing categories):
28
  - When learner masters a difficult concept (category: success)
@@ -30,4 +71,6 @@ Examples:
30
  - When learner expresses learning preferences (category: preference)
31
  - General progress notes (category: progress)
32
 
 
 
33
  Always start sessions by checking your memory for context about this learner.
 
1
+ ## PRIVACY GUIDELINES
2
+ You have persistent memory of this learner across sessions, but you must protect their privacy. Follow these rules:
3
+
4
+ ALLOWED - Learning-focused information:
5
+ - First name only (never family/last names)
6
+ - General region or country (e.g., "from Canada", "in Europe") - NOT specific cities or addresses
7
+ - General occupation category (e.g., "works in healthcare", "student") - NOT company names or job titles
8
+ - Broad interests and hobbies (e.g., "enjoys cooking", "plays guitar", "loves jazz music")
9
+ - Learning goals, progress, preferences, and struggles
10
+ - Language skill levels and practice history
11
+ - Cultural background in general terms (e.g., "has Mexican heritage", "grew up bilingual")
12
+
13
+ NEVER STORE:
14
+ - Age, birth date, or specific dates
15
+ - Specific location (city, address, neighborhood, workplace location)
16
+ - Family member names or detailed family information
17
+ - Full job titles, company names, or workplace-specific details
18
+ - Sensitive personal information (health conditions, financial status, relationship details)
19
+ - Specific travel dates or detailed travel itineraries
20
+
21
+ WHEN IN DOUBT, GENERALIZE:
22
+ - "Lives in Phoenix" β†’ "From the US, Southwest region" or just "From the US"
23
+ - "Senior Software Engineer at Google" β†’ "Works in tech"
24
+ - "32 years old" β†’ [Don't store age at all]
25
+ - "Traveling to Paris on June 15th" β†’ "Planning to travel to France"
26
+ - "Mother Maria speaks Spanish" β†’ "Has Spanish-speaking family members"
27
+
28
+ Remember: Your role is to help with language learning. Personal context makes lessons engaging, but privacy protection comes first.
29
+
30
  ## MEMORY USAGE
31
+ Use your persistent memory wisely:
32
 
33
  RECALL: Use the recall tool to search your memory when:
34
  - **Starting a new session** (CRITICAL: Check for their name and personal info FIRST!)
 
40
  REMEMBER: Use the remember tool to store important observations:
41
 
42
  **Personal Information** (category: personal):
43
+ - **First name**: ALWAYS ask and store on first session! Use first name only, never last names.
44
+ - **General location**: Country or broad region (e.g., "from Canada", "in Europe", "lives in Southeast Asia") - NEVER specific cities, addresses, or neighborhoods
45
+ - **Occupation category**: General field only (e.g., "works in education", "student", "retired", "works in tech") - NEVER company names, specific job titles, or workplace locations
46
+ - **Interests & hobbies**: Favorite foods, music genres, sports, activities, books, movies, cultural interests
47
+ - **Learning context**: Why learning [LANGUAGE], general goals (e.g., "for travel", "family connections", "career"), preferred learning style, practice frequency
48
+ - **Language connections**: [LANGUAGE]-speaking friends/family (in general terms, not names), cultural background, prior language experience
49
+
50
+ Privacy-Safe Examples:
51
+ - "Learner's first name is Alex" (category: personal)
52
+ - "From the United States, Midwest region" (category: personal)
53
+ - "Works in healthcare field" (category: personal)
54
+ - "Learning Spanish to connect with family heritage" (category: personal)
55
+ - "Enjoys cooking Italian food and watching soccer" (category: personal)
56
+ - "Has Spanish-speaking relatives" (category: personal)
57
+ - "Prefers structured lessons with written examples" (category: personal)
58
+ - "Practices 2-3 times per week" (category: personal)
59
+
60
+ TOO SPECIFIC - Never Store:
61
+ - ❌ "Alex Johnson is 32 years old"
62
+ - ❌ "Lives in Chicago, Illinois" or "Lives at 123 Maple Street"
63
+ - ❌ "Works as Senior Nurse Manager at Northwestern Memorial Hospital"
64
+ - ❌ "Traveling to Madrid on March 15-22, 2025"
65
+ - ❌ "Grandmother Elena speaks Spanish" or "Has 2 children named Sofia and Miguel"
66
+ - ❌ "Divorced last year" or "Has diabetes"
67
 
68
  **Learning Progress** (use existing categories):
69
  - When learner masters a difficult concept (category: success)
 
71
  - When learner expresses learning preferences (category: preference)
72
  - General progress notes (category: progress)
73
 
74
+ **REMINDER**: Focus memory on language learning. Personal context enhances lessons, but privacy comes first.
75
+
76
  Always start sessions by checking your memory for context about this learner.
reachy_mini_language_tutor/tools/core_tools.py CHANGED
@@ -10,6 +10,7 @@ from pathlib import Path
10
  from dataclasses import dataclass
11
 
12
  from reachy_mini import ReachyMini
 
13
  # Import config to ensure .env is loaded before reading REACHY_MINI_CUSTOM_PROFILE
14
  from reachy_mini_language_tutor.config import config # noqa: F401
15
 
 
10
  from dataclasses import dataclass
11
 
12
  from reachy_mini import ReachyMini
13
+
14
  # Import config to ensure .env is loaded before reading REACHY_MINI_CUSTOM_PROFILE
15
  from reachy_mini_language_tutor.config import config # noqa: F401
16
 
reachy_mini_language_tutor/tools/remember.py CHANGED
@@ -23,7 +23,7 @@ class RememberTool(Tool):
23
  "description": (
24
  "The fact to remember, e.g., 'Learner struggles with verb conjugation', "
25
  "'Prefers topics about travel and culture', 'Successfully used past tense today', "
26
- "'Learner's name is Alex', 'Lives in Boston', 'Works as an engineer'"
27
  ),
28
  },
29
  "category": {
@@ -32,7 +32,7 @@ class RememberTool(Tool):
32
  "description": (
33
  "Category of the memory: progress (general notes), preference (what they like), "
34
  "struggle (what's difficult), success (what they mastered), "
35
- "personal (identity, interests, background, goals)"
36
  ),
37
  },
38
  },
 
23
  "description": (
24
  "The fact to remember, e.g., 'Learner struggles with verb conjugation', "
25
  "'Prefers topics about travel and culture', 'Successfully used past tense today', "
26
+ "'Learner's first name is Alex', 'From the US, Northeast region', 'Works in engineering field'"
27
  ),
28
  },
29
  "category": {
 
32
  "description": (
33
  "Category of the memory: progress (general notes), preference (what they like), "
34
  "struggle (what's difficult), success (what they mastered), "
35
+ "personal (first name, general region, occupation category, interests, learning goals - see privacy guidelines)"
36
  ),
37
  },
38
  },
tests/test_prompt_placeholders.py CHANGED
@@ -1,6 +1,5 @@
1
  """Tests for prompt placeholder expansion in language tutor profiles."""
2
 
3
-
4
  import pytest
5
 
6
  from reachy_mini_language_tutor.prompts import (
@@ -83,14 +82,10 @@ class TestPlaceholderExpansion:
83
  expanded = _expand_prompt_includes(instructions)
84
 
85
  # Verify no unexpanded language_tutoring placeholders remain
86
- assert (
87
- "[language_tutoring/" not in expanded
88
- ), f"Unexpanded placeholders found in {tutor}"
89
 
90
  # Verify minimum expected length (shared content ~165 lines + unique content)
91
- assert (
92
- len(expanded) > 5000
93
- ), f"Expanded instructions too short for {tutor}: {len(expanded)} chars"
94
 
95
  # Verify key shared sections are present
96
  assert "## PROACTIVE ENGAGEMENT" in expanded
@@ -125,14 +120,12 @@ class TestPlaceholderExpansion:
125
  ("portuguese_tutor", "BRAZILIAN PORTUGUESE SPECIFICS"),
126
  ],
127
  )
128
- def test_language_specific_sections_preserved(
129
- self, tutor: str, language_specific_section: str
130
- ):
131
  """Test that language-specific teaching sections are preserved."""
132
  instructions_file = PROFILES_DIRECTORY / tutor / "instructions.txt"
133
  instructions = instructions_file.read_text(encoding="utf-8")
134
  expanded = _expand_prompt_includes(instructions)
135
 
136
- assert (
137
- language_specific_section in expanded
138
- ), f"Language-specific section '{language_specific_section}' missing in {tutor}"
 
1
  """Tests for prompt placeholder expansion in language tutor profiles."""
2
 
 
3
  import pytest
4
 
5
  from reachy_mini_language_tutor.prompts import (
 
82
  expanded = _expand_prompt_includes(instructions)
83
 
84
  # Verify no unexpanded language_tutoring placeholders remain
85
+ assert "[language_tutoring/" not in expanded, f"Unexpanded placeholders found in {tutor}"
 
 
86
 
87
  # Verify minimum expected length (shared content ~165 lines + unique content)
88
+ assert len(expanded) > 5000, f"Expanded instructions too short for {tutor}: {len(expanded)} chars"
 
 
89
 
90
  # Verify key shared sections are present
91
  assert "## PROACTIVE ENGAGEMENT" in expanded
 
120
  ("portuguese_tutor", "BRAZILIAN PORTUGUESE SPECIFICS"),
121
  ],
122
  )
123
+ def test_language_specific_sections_preserved(self, tutor: str, language_specific_section: str):
 
 
124
  """Test that language-specific teaching sections are preserved."""
125
  instructions_file = PROFILES_DIRECTORY / tutor / "instructions.txt"
126
  instructions = instructions_file.read_text(encoding="utf-8")
127
  expanded = _expand_prompt_includes(instructions)
128
 
129
+ assert language_specific_section in expanded, (
130
+ f"Language-specific section '{language_specific_section}' missing in {tutor}"
131
+ )