personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor facets instruction config: use boolean values and fix facet param

- Change facets schema from strings to: false | true | "full"
- false: skip facet context
- true: include with names only (default)
- "full": include with full descriptions

- Fix bug: pass facet param to compose_instructions() in assemble_inputs()
so multi-facet generators get focused facet context

- Add detailed param to facet_summary() for short vs full mode

- Update all 21 muse/*.md generators to use new boolean format

- Add test for facet_summary short mode

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

+122 -73
+2 -2
docs/APPS.md
··· 366 366 { 367 367 "instructions": { 368 368 "system": "journal", 369 - "facets": "short", 369 + "facets": true, 370 370 "sources": {"audio": true, "screen": true, "agents": false} 371 371 } 372 372 } 373 373 ``` 374 374 375 375 - `system` - System prompt file name (loads from `think/{name}.txt`) 376 - - `facets` - `"none"` | `"short"` | `"detailed"` for unfocused facet summaries 376 + - `facets` - `false` | `true` | `"full"` - whether to include facet context (true=names only, "full"=with descriptions) 377 377 - `sources` - Generators only: which content types to cluster. Values can be: 378 378 - `false` - don't load this source type 379 379 - `true` - load if available
+1 -1
muse/activity.md
··· 8 8 "output": "md", 9 9 "instructions": { 10 10 "sources": {"audio": true, "screen": true, "agents": false}, 11 - "facets": "short" 11 + "facets": true 12 12 } 13 13 14 14 }
+1 -1
muse/activity_state.md
··· 13 13 "max_output_tokens": 512, 14 14 "instructions": { 15 15 "sources": {"audio": true, "screen": true, "agents": false}, 16 - "facets": "none" 16 + "facets": false 17 17 } 18 18 19 19 }
+1 -1
muse/daily_schedule.md
··· 11 11 "max_output_tokens": 512, 12 12 "instructions": { 13 13 "sources": {"audio": true, "screen": false, "agents": {"screen": true}}, 14 - "facets": "short" 14 + "facets": true 15 15 } 16 16 17 17 }
+1 -1
muse/decisions.md
··· 10 10 "output": "md", 11 11 "instructions": { 12 12 "sources": {"audio": true, "screen": false, "agents": {"screen": true}}, 13 - "facets": "short" 13 + "facets": true 14 14 } 15 15 16 16 }
+1 -1
muse/documentation.md
··· 10 10 "output": "md", 11 11 "instructions": { 12 12 "sources": {"audio": true, "screen": false, "agents": {"screen": true}}, 13 - "facets": "short" 13 + "facets": true 14 14 } 15 15 16 16 }
+1 -1
muse/entities.md
··· 11 11 "output": "md", 12 12 "instructions": { 13 13 "sources": {"audio": true, "screen": true, "agents": false}, 14 - "facets": "none" 14 + "facets": false 15 15 } 16 16 17 17 }
+1 -1
muse/facets.md
··· 11 11 "output": "json", 12 12 "instructions": { 13 13 "sources": {"audio": false, "screen": false, "agents": true}, 14 - "facets": "short" 14 + "facets": true 15 15 } 16 16 17 17 }
+1 -1
muse/files.md
··· 11 11 "output": "md", 12 12 "instructions": { 13 13 "sources": {"audio": true, "screen": false, "agents": {"screen": true}}, 14 - "facets": "short" 14 + "facets": true 15 15 } 16 16 17 17 }
+1 -1
muse/flow.md
··· 10 10 "output": "md", 11 11 "instructions": { 12 12 "sources": {"audio": true, "screen": false, "agents": {"screen": true}}, 13 - "facets": "short" 13 + "facets": true 14 14 } 15 15 16 16 }
+1 -1
muse/followups.md
··· 10 10 "output": "md", 11 11 "instructions": { 12 12 "sources": {"audio": true, "screen": false, "agents": {"screen": true}}, 13 - "facets": "short" 13 + "facets": true 14 14 } 15 15 16 16 }
+1 -1
muse/knowledge_graph.md
··· 10 10 "output": "md", 11 11 "instructions": { 12 12 "sources": {"audio": true, "screen": false, "agents": {"screen": true}}, 13 - "facets": "short" 13 + "facets": true 14 14 } 15 15 16 16 }
+1 -1
muse/media.md
··· 11 11 "output": "md", 12 12 "instructions": { 13 13 "sources": {"audio": true, "screen": false, "agents": {"screen": true}}, 14 - "facets": "short" 14 + "facets": true 15 15 } 16 16 17 17 }
+1 -1
muse/meetings.md
··· 10 10 "output": "md", 11 11 "instructions": { 12 12 "sources": {"audio": true, "screen": false, "agents": {"screen": true}}, 13 - "facets": "short" 13 + "facets": true 14 14 } 15 15 16 16 }
+1 -1
muse/messages.md
··· 10 10 "output": "md", 11 11 "instructions": { 12 12 "sources": {"audio": true, "screen": false, "agents": {"screen": true}}, 13 - "facets": "short" 13 + "facets": true 14 14 } 15 15 16 16 }
+1 -1
muse/opportunities.md
··· 10 10 "output": "md", 11 11 "instructions": { 12 12 "sources": {"audio": true, "screen": false, "agents": {"screen": true}}, 13 - "facets": "short" 13 + "facets": true 14 14 } 15 15 16 16 }
+1 -1
muse/research.md
··· 10 10 "output": "md", 11 11 "instructions": { 12 12 "sources": {"audio": true, "screen": false, "agents": {"screen": true}}, 13 - "facets": "short" 13 + "facets": true 14 14 } 15 15 16 16 }
+1 -1
muse/schedule.md
··· 9 9 "output": "md", 10 10 "instructions": { 11 11 "sources": {"audio": true, "screen": false, "agents": {"screen": true}}, 12 - "facets": "short" 12 + "facets": true 13 13 } 14 14 15 15 }
+1 -1
muse/screen.md
··· 8 8 "output": "md", 9 9 "instructions": { 10 10 "sources": {"audio": true, "screen": true, "agents": false}, 11 - "facets": "short" 11 + "facets": true 12 12 } 13 13 14 14 }
+1 -1
muse/speakers.md
··· 8 8 "color": "#e64a19", 9 9 "instructions": { 10 10 "sources": {"audio": "required", "screen": true, "agents": false}, 11 - "facets": "short" 11 + "facets": true 12 12 } 13 13 14 14 }
+1 -1
muse/timeline.md
··· 10 10 "output": "md", 11 11 "instructions": { 12 12 "sources": {"audio": true, "screen": false, "agents": {"screen": true}}, 13 - "facets": "short" 13 + "facets": true 14 14 } 15 15 16 16 }
+1 -1
muse/tools.md
··· 11 11 "output": "md", 12 12 "instructions": { 13 13 "sources": {"audio": true, "screen": false, "agents": {"screen": true}}, 14 - "facets": "short" 14 + "facets": true 15 15 } 16 16 17 17 }
+24
tests/test_facets.py
··· 92 92 assert "A custom test activity" in summary 93 93 94 94 95 + def test_facet_summary_short_mode(monkeypatch): 96 + """Test facet_summary with detailed=False shows names only.""" 97 + monkeypatch.setenv("JOURNAL_PATH", str(FIXTURES_PATH)) 98 + 99 + summary = facet_summary("full-featured", detailed=False) 100 + 101 + # Check title and description still present 102 + assert "# Full Featured Facet" in summary 103 + assert "**Description:** A facet for testing all features" in summary 104 + 105 + # Should NOT have detailed entities section 106 + assert "## Entities" not in summary 107 + # Should have inline entities list 108 + assert "**Entities**:" in summary 109 + 110 + # Should NOT have detailed activities section 111 + assert "## Activities" not in summary 112 + # Should have inline activities list 113 + assert "**Activities**:" in summary 114 + 115 + # Should NOT have activity descriptions 116 + assert "A custom test activity" not in summary 117 + 118 + 95 119 def test_facet_summary_minimal(monkeypatch): 96 120 """Test facet_summary with minimal metadata.""" 97 121 monkeypatch.setenv("JOURNAL_PATH", str(FIXTURES_PATH))
+11 -11
tests/test_think_utils.py
··· 663 663 664 664 def test_returns_defaults_when_no_overrides(self): 665 665 """Test that defaults are returned when overrides is None.""" 666 - defaults = {"system": "journal", "facets": "short"} 666 + defaults = {"system": "journal", "facets": True} 667 667 result = _merge_instructions_config(defaults, None) 668 668 assert result == defaults 669 669 # Should be a copy, not the same object ··· 671 671 672 672 def test_returns_defaults_when_empty_overrides(self): 673 673 """Test that defaults are returned when overrides is empty dict.""" 674 - defaults = {"system": "journal", "facets": "short"} 674 + defaults = {"system": "journal", "facets": True} 675 675 result = _merge_instructions_config(defaults, {}) 676 676 assert result == defaults 677 677 678 678 def test_overrides_system_key(self): 679 679 """Test that system key can be overridden.""" 680 - defaults = {"system": "journal", "facets": "short"} 680 + defaults = {"system": "journal", "facets": True} 681 681 overrides = {"system": "custom_prompt"} 682 682 result = _merge_instructions_config(defaults, overrides) 683 683 assert result["system"] == "custom_prompt" 684 - assert result["facets"] == "short" 684 + assert result["facets"] is True 685 685 686 686 def test_overrides_facets_key(self): 687 687 """Test that facets key can be overridden.""" 688 - defaults = {"system": "journal", "facets": "short"} 689 - overrides = {"facets": "detailed"} 688 + defaults = {"system": "journal", "facets": True} 689 + overrides = {"facets": "full"} 690 690 result = _merge_instructions_config(defaults, overrides) 691 691 assert result["system"] == "journal" 692 - assert result["facets"] == "detailed" 692 + assert result["facets"] == "full" 693 693 694 694 def test_merges_sources_dict(self): 695 695 """Test that sources dict is merged, not replaced.""" ··· 705 705 706 706 def test_ignores_unknown_keys(self): 707 707 """Test that unknown keys in overrides are ignored.""" 708 - defaults = {"system": "journal", "facets": "short"} 708 + defaults = {"system": "journal", "facets": True} 709 709 overrides = {"unknown_key": "value", "another": 123} 710 710 result = _merge_instructions_config(defaults, overrides) 711 711 assert "unknown_key" not in result ··· 805 805 806 806 result = compose_instructions( 807 807 include_datetime=False, 808 - config_overrides={"facets": "none"}, 808 + config_overrides={"facets": False}, 809 809 ) 810 810 811 811 # With no datetime and no facets, extra_context should be empty/None ··· 825 825 826 826 result = compose_instructions( 827 827 include_datetime=False, 828 - config_overrides={"facets": "none"}, 828 + config_overrides={"facets": False}, 829 829 ) 830 830 831 831 extra = result.get("extra_context") or "" ··· 845 845 846 846 result = compose_instructions( 847 847 include_datetime=True, 848 - config_overrides={"facets": "none"}, 848 + config_overrides={"facets": False}, 849 849 ) 850 850 851 851 assert "Current Date and Time" in result["extra_context"]
+1
think/agents.py
··· 683 683 # Extract instructions config for source filtering and system prompt 684 684 instructions_config = meta.get("instructions") 685 685 instructions = compose_instructions( 686 + facet=facet, 686 687 include_datetime=not day, 687 688 config_overrides=instructions_config, 688 689 )
+35 -17
think/facets.py
··· 309 309 return {k: v for k, v in get_facets().items() if not v.get("muted", False)} 310 310 311 311 312 - def facet_summary(facet: str) -> str: 312 + def facet_summary(facet: str, *, detailed: bool = True) -> str: 313 313 """Generate a nicely formatted markdown summary of a facet. 314 314 315 315 Args: 316 316 facet: The facet name to summarize 317 + detailed: If True (default), include full descriptions for entities 318 + and activities. If False, show names only. 317 319 318 320 Returns: 319 321 Formatted markdown string with facet title, description, entities, ··· 369 371 lines.append("") 370 372 371 373 if display_entities: 372 - lines.append("## Entities") 373 - lines.append("") 374 - for entity in display_entities: 375 - entity_type = entity.get("type", "") 376 - formatted_name = _format_entity_name_with_aka(entity) 377 - desc = entity.get("description", "") 374 + if detailed: 375 + lines.append("## Entities") 376 + lines.append("") 377 + for entity in display_entities: 378 + entity_type = entity.get("type", "") 379 + formatted_name = _format_entity_name_with_aka(entity) 380 + desc = entity.get("description", "") 378 381 379 - if desc: 380 - lines.append(f"- **{entity_type}**: {formatted_name} - {desc}") 381 - else: 382 - lines.append(f"- **{entity_type}**: {formatted_name}") 383 - lines.append("") 382 + if desc: 383 + lines.append(f"- **{entity_type}**: {formatted_name} - {desc}") 384 + else: 385 + lines.append(f"- **{entity_type}**: {formatted_name}") 386 + lines.append("") 387 + else: 388 + # Short mode: names only as semicolon-separated list 389 + entity_names = "; ".join( 390 + _format_entity_name_with_aka(e) for e in display_entities 391 + ) 392 + lines.append(f"**Entities**: {entity_names}") 393 + lines.append("") 384 394 385 395 # Load activities if available 386 396 activities = get_facet_activities(facet) 387 397 if activities: 388 - lines.append("## Activities") 389 - lines.append("") 390 - for activity in activities: 391 - lines.append(f"- {_format_activity_line(activity, bold_name=True)}") 392 - lines.append("") 398 + if detailed: 399 + lines.append("## Activities") 400 + lines.append("") 401 + for activity in activities: 402 + lines.append(f"- {_format_activity_line(activity, bold_name=True)}") 403 + lines.append("") 404 + else: 405 + # Short mode: names only as semicolon-separated list 406 + activity_names = "; ".join( 407 + a.get("name", a.get("id", "")) for a in activities 408 + ) 409 + lines.append(f"**Activities**: {activity_names}") 410 + lines.append("") 393 411 394 412 return "\n".join(lines) 395 413
+28 -22
think/utils.py
··· 1050 1050 # Default instruction configuration 1051 1051 _DEFAULT_INSTRUCTIONS = { 1052 1052 "system": "journal", 1053 - "facets": "short", 1053 + "facets": True, 1054 1054 "sources": { 1055 1055 "audio": True, 1056 1056 "screen": True, ··· 1116 1116 Directory to load user_prompt from. If None, uses think/ directory. 1117 1117 facet: 1118 1118 Optional facet name to focus on. When provided, extra_context includes 1119 - detailed information for just this facet instead of all facets. 1119 + only this facet's info (detail level controlled by "facets" setting). 1120 1120 include_datetime: 1121 1121 Whether to include current date/time in extra_context. Default True 1122 1122 for agents (real-time chat), typically False for generators (past analysis). 1123 1123 config_overrides: 1124 1124 Optional dict from .json "instructions" key. Supported keys: 1125 1125 - "system": prompt name for system instruction (default: "journal") 1126 - - "facets": "none" | "short" | "detailed" (default: "short") 1126 + - "facets": false | true | "full" (default: true) 1127 + false = skip facet context 1128 + true = include facet context with names only 1129 + "full" = include facet context with full descriptions 1130 + For faceted generators, shows focused facet; for unfaceted, shows all facets. 1127 1131 - "sources": {"audio": bool, "screen": bool, "agents": bool|dict} 1128 1132 The "agents" source can be: 1129 1133 - bool: True (all agents), False (no agents) ··· 1160 1164 result["user_instruction"] = None 1161 1165 1162 1166 # Build extra_context based on facets setting 1167 + # Values: false (skip), true (names only), "full" (with descriptions) 1163 1168 extra_parts = [] 1164 - facets_mode = cfg.get("facets", "short") 1169 + facets_setting = cfg.get("facets", True) 1170 + facets_full = facets_setting == "full" 1165 1171 1166 - # Focused facet always gets context (facets setting only affects plural) 1167 - if facet: 1168 - try: 1169 - from think.facets import facet_summary 1172 + if facets_setting: 1173 + if facet: 1174 + # Focused facet mode: include only this facet's context 1175 + try: 1176 + from think.facets import facet_summary 1170 1177 1171 - detailed = facet_summary(facet) 1172 - extra_parts.append(f"## Facet Focus\n{detailed}") 1173 - except Exception: 1174 - pass # Ignore if facet can't be loaded 1175 - elif facets_mode != "none": 1176 - # General mode: all facets (controlled by facets setting) 1177 - try: 1178 - from think.facets import facet_summaries 1178 + summary = facet_summary(facet, detailed=facets_full) 1179 + extra_parts.append(f"## Facet Focus\n{summary}") 1180 + except Exception: 1181 + pass # Ignore if facet can't be loaded 1182 + else: 1183 + # General mode: all facets 1184 + try: 1185 + from think.facets import facet_summaries 1179 1186 1180 - detailed = facets_mode == "detailed" 1181 - facets_summary = facet_summaries(detailed=detailed) 1182 - if facets_summary and facets_summary != "No facets found.": 1183 - extra_parts.append(facets_summary) 1184 - except Exception: 1185 - pass # Ignore if facets can't be loaded 1187 + summary = facet_summaries(detailed=facets_full) 1188 + if summary and summary != "No facets found.": 1189 + extra_parts.append(summary) 1190 + except Exception: 1191 + pass # Ignore if facets can't be loaded 1186 1192 1187 1193 # Add current date/time if requested 1188 1194 if include_datetime: