personal memory agent
0
fork

Configure Feed

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

refactor: rename 'topics/summaries' to 'insights' throughout codebase

Complete vocabulary elevation to establish three-layer architecture:
Captures → Extracts → Insights

Key changes:
- Rename think/topics/ → think/insights/ (template directory)
- Rename think/summarize.py → think/insight.py
- Rename get_topics() → get_insights() in think/utils.py
- Rename scan_summaries/search_summaries → scan_insights/search_insights
- Update CLI command: think-summarize → think-insight
- Update API endpoints: /api/topics → /api/insights
- Update MCP tool: search_summaries → search_insights
- Update stats keys: topics_processed → insights_processed
- Update all path references from "topics/" to "insights/"
- Update documentation (JOURNAL.md, CRUMBS.md, AGENTS.md)
- Update fixtures and tests to use new naming
- Rename indexer database: summaries.sqlite → insights.sqlite

Note: Real journal data at $JOURNAL_PATH needs manual migration of
YYYYMMDD/topics/ directories to insights/ and JSON source field updates.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

+308 -307
+1 -1
AGENTS.md
··· 247 247 * **Journal Data**: Path from `JOURNAL_PATH` env var (set in `.env`) 248 248 * **Config**: `.env` file in project root 249 249 * **Agent Personas**: `muse/agents/*.txt` and `*.json` 250 - * **Topic Templates**: `think/topics/*.txt` and `*.json` 250 + * **Insight Templates**: `think/insights/*.txt` and `*.json` 251 251 252 252 ### Getting Help 253 253 * Run `make help` for available Make targets
+3 -3
CRUMBS.md
··· 6 6 7 7 ```json 8 8 { 9 - "generator": "think/summarize.py", // module or command that produced the file 10 - "output": "20250524/topics/flow.md", // path to the generated artifact 9 + "generator": "think.insight", // module or command that produced the file 10 + "output": "20250524/insights/flow.md", // path to the generated artifact 11 11 "generated_at": "2025-05-24T12:00:01Z", // ISO 8601 timestamp of generation 12 12 "dependencies": [ // list of inputs relied upon 13 13 { 14 14 "type": "file", // a single file dependency 15 - "path": "think/topics/flow.txt", 15 + "path": "think/insights/flow.txt", 16 16 "mtime": 1716543210 // modification time (epoch seconds) 17 17 }, 18 18 {
+7 -7
JOURNAL.md
··· 578 578 "occurrences": [ 579 579 { 580 580 "type": "meeting", 581 - "source": "topics/meetings.md", 581 + "source": "insights/meetings.md", 582 582 "start": "09:00:00", 583 583 "end": "09:30:00", 584 584 "title": "Team stand-up", ··· 618 618 619 619 Post-processing generates day-level insights that synthesize all segments: 620 620 621 - - `topics/flow.md` – day overview and work rhythm analysis 622 - - `topics/knowledge_graph.md` – entity relationships and knowledge network 623 - - `topics/meetings.md` – meeting list used by the calendar web UI 624 - - Additional topic-based insights as configured in `think/topics/` 621 + - `insights/flow.md` – day overview and work rhythm analysis 622 + - `insights/knowledge_graph.md` – entity relationships and knowledge network 623 + - `insights/meetings.md` – meeting list used by the calendar web UI 624 + - Additional topic-based insights as configured in `think/insights/` 625 625 626 - Each insight type has a corresponding template in `think/topics/{name}.txt` that defines how the AI synthesizes extracts into narrative form. 626 + Each insight type has a corresponding template in `think/insights/{name}.txt` that defines how the AI synthesizes extracts into narrative form. 627 627 628 628 #### Provenance 629 629 630 - Most insights are accompanied by a `.crumb` file capturing source dependencies and model information. See **CRUMBS.md** for the format specification. Example: `20250610/topics/flow.md.crumb`. 630 + Most insights are accompanied by a `.crumb` file capturing source dependencies and model information. See **CRUMBS.md** for the format specification. Example: `20250610/insights/flow.md.crumb`.
+47 -47
apps/agents/routes.py
··· 24 24 Returns: 25 25 List of items with id, title, and description (and color for topics) 26 26 """ 27 - # Special handling for topics to get colors from get_topics() 28 - if item_type == "topics": 29 - from think.utils import get_topics 27 + # Special handling for insights to get colors from get_insights() 28 + if item_type == "insights": 29 + from think.utils import get_insights 30 30 31 - topics = get_topics() 31 + insights = get_insights() 32 32 items: list[dict[str, object]] = [] 33 33 34 - for name, info in topics.items(): 34 + for name, info in insights.items(): 35 35 item = { 36 36 "id": name, 37 37 "title": info.get("title", name), ··· 45 45 return items 46 46 47 47 # Standard handling for agents 48 - # Agents are in muse/agents/, topics are in think/topics/ 48 + # Agents are in muse/agents/, insights are in think/insights/ 49 49 if item_type == "agents": 50 50 items_path = os.path.join( 51 51 os.path.dirname(os.path.dirname(__file__)), "..", "muse", item_type ··· 176 176 Returns: 177 177 Tuple of (response dict, status code) 178 178 """ 179 - # Agents are in muse/agents/, topics are in think/topics/ 179 + # Agents are in muse/agents/, insights are in think/insights/ 180 180 if item_type == "agents": 181 181 items_path = os.path.join( 182 182 os.path.dirname(os.path.dirname(__file__)), "..", "muse", item_type ··· 312 312 return {"error": "Title and content are required"}, 400 313 313 314 314 # Path to item files 315 - # Agents are in muse/agents/, topics are in think/topics/ 315 + # Agents are in muse/agents/, insights are in think/insights/ 316 316 if item_type == "agents": 317 317 items_path = os.path.join( 318 318 os.path.dirname(os.path.dirname(__file__)), "..", "muse", item_type ··· 454 454 return jsonify({"error": str(e)}), 500 455 455 456 456 457 - # Topics API endpoints 458 - @agents_bp.route("/api/topics") 459 - def available_topics() -> object: 460 - """Return list of available topic definitions from think/topics/.""" 461 - return jsonify(_list_items("topics")) 457 + # Insights API endpoints 458 + @agents_bp.route("/api/insights") 459 + def available_insights() -> object: 460 + """Return list of available insight definitions from think/insights/.""" 461 + return jsonify(_list_items("insights")) 462 462 463 463 464 - @agents_bp.route("/api/topics/content/<topic_id>") 465 - def topic_content(topic_id: str) -> object: 466 - """Return the .txt content for a topic.""" 467 - response, status = _get_item_content("topics", topic_id) 464 + @agents_bp.route("/api/insights/content/<insight_id>") 465 + def insight_content(insight_id: str) -> object: 466 + """Return the .txt content for an insight.""" 467 + response, status = _get_item_content("insights", insight_id) 468 468 return jsonify(response), status 469 469 470 470 471 - @agents_bp.route("/api/topics/update/<topic_id>", methods=["PUT"]) 472 - def update_topic(topic_id: str) -> object: 473 - """Update a topic's title and content or create a new one.""" 471 + @agents_bp.route("/api/insights/update/<insight_id>", methods=["PUT"]) 472 + def update_insight(insight_id: str) -> object: 473 + """Update an insight's title and content or create a new one.""" 474 474 data = request.get_json() 475 475 476 - # Handle topic-specific fields 476 + # Handle insight-specific fields 477 477 if "color" in data: 478 478 # Save color in JSON config 479 - topics_path = os.path.join( 480 - os.path.dirname(os.path.dirname(__file__)), "..", "think", "topics" 479 + insights_path = os.path.join( 480 + os.path.dirname(os.path.dirname(__file__)), "..", "think", "insights" 481 481 ) 482 - json_path = os.path.join(topics_path, f"{topic_id}.json") 482 + json_path = os.path.join(insights_path, f"{insight_id}.json") 483 483 484 - topic_config = {} 484 + insight_config = {} 485 485 if os.path.isfile(json_path): 486 486 with open(json_path, "r", encoding="utf-8") as f: 487 - topic_config = json.load(f) 487 + insight_config = json.load(f) 488 488 489 - topic_config["color"] = data["color"] 489 + insight_config["color"] = data["color"] 490 490 491 491 with open(json_path, "w", encoding="utf-8") as f: 492 - json.dump(topic_config, f, indent=4) 492 + json.dump(insight_config, f, indent=4) 493 493 494 494 if "disabled" in data: 495 495 # Save disabled state in JSON config 496 - topics_path = os.path.join( 497 - os.path.dirname(os.path.dirname(__file__)), "..", "think", "topics" 496 + insights_path = os.path.join( 497 + os.path.dirname(os.path.dirname(__file__)), "..", "think", "insights" 498 498 ) 499 - json_path = os.path.join(topics_path, f"{topic_id}.json") 499 + json_path = os.path.join(insights_path, f"{insight_id}.json") 500 500 501 - topic_config = {} 501 + insight_config = {} 502 502 if os.path.isfile(json_path): 503 503 with open(json_path, "r", encoding="utf-8") as f: 504 - topic_config = json.load(f) 504 + insight_config = json.load(f) 505 505 506 - topic_config["disabled"] = data["disabled"] 506 + insight_config["disabled"] = data["disabled"] 507 507 508 508 with open(json_path, "w", encoding="utf-8") as f: 509 - json.dump(topic_config, f, indent=4) 509 + json.dump(insight_config, f, indent=4) 510 510 511 - response, status = _update_item("topics", topic_id, data) 511 + response, status = _update_item("insights", insight_id, data) 512 512 return jsonify(response), status 513 513 514 514 515 - @agents_bp.route("/api/topics/toggle/<topic_id>", methods=["POST"]) 516 - def toggle_topic(topic_id: str) -> object: 517 - """Toggle the disabled state of a topic.""" 518 - topics_path = os.path.join( 519 - os.path.dirname(os.path.dirname(__file__)), "..", "think", "topics" 515 + @agents_bp.route("/api/insights/toggle/<insight_id>", methods=["POST"]) 516 + def toggle_insight(insight_id: str) -> object: 517 + """Toggle the disabled state of an insight.""" 518 + insights_path = os.path.join( 519 + os.path.dirname(os.path.dirname(__file__)), "..", "think", "insights" 520 520 ) 521 - json_path = os.path.join(topics_path, f"{topic_id}.json") 521 + json_path = os.path.join(insights_path, f"{insight_id}.json") 522 522 523 523 if not os.path.isfile(json_path): 524 - return jsonify({"error": "Topic not found"}), 404 524 + return jsonify({"error": "Insight not found"}), 404 525 525 526 526 try: 527 527 # Read existing JSON file 528 528 with open(json_path, "r", encoding="utf-8") as f: 529 - topic_config = json.load(f) 529 + insight_config = json.load(f) 530 530 531 531 # Toggle disabled state 532 - topic_config["disabled"] = not topic_config.get("disabled", False) 532 + insight_config["disabled"] = not insight_config.get("disabled", False) 533 533 534 534 # Write back to file 535 535 with open(json_path, "w", encoding="utf-8") as f: 536 - json.dump(topic_config, f, indent=4) 536 + json.dump(insight_config, f, indent=4) 537 537 538 - return jsonify({"success": True, "disabled": topic_config["disabled"]}) 538 + return jsonify({"success": True, "disabled": insight_config["disabled"]}) 539 539 except Exception as e: 540 540 return jsonify({"error": str(e)}), 500 541 541
+15 -15
apps/calendar/routes.py
··· 25 25 day_dir = str(day_path(day)) 26 26 if not os.path.isdir(day_dir): 27 27 return "", 404 28 - from think.utils import get_topics 28 + from think.utils import get_insights 29 29 30 - topics = get_topics() 30 + insights = get_insights() 31 31 files = [] 32 - topics_dir = os.path.join(day_dir, "topics") 33 - if os.path.isdir(topics_dir): 34 - for name in sorted(os.listdir(topics_dir)): 32 + insights_dir = os.path.join(day_dir, "insights") 33 + if os.path.isdir(insights_dir): 34 + for name in sorted(os.listdir(insights_dir)): 35 35 base, ext = os.path.splitext(name) 36 - if ext != ".md" or base not in topics: 36 + if ext != ".md" or base not in insights: 37 37 continue 38 - path = os.path.join(topics_dir, name) 38 + path = os.path.join(insights_dir, name) 39 39 try: 40 40 with open(path, "r", encoding="utf-8") as f: 41 41 text = f.read() ··· 53 53 "label": label, 54 54 "html": html, 55 55 "topic": base, 56 - "color": topics[base]["color"], 56 + "color": insights[base]["color"], 57 57 } 58 58 ) 59 59 title = format_date(day) ··· 77 77 return "", 404 78 78 79 79 from think.indexer import search_events 80 - from think.utils import get_topics 80 + from think.utils import get_insights 81 81 82 - topics = get_topics() 82 + insights = get_insights() 83 83 84 84 # Use search_events to get all events for this day 85 85 _, results = search_events(query="", day=day, limit=1000) ··· 92 92 topic = metadata.get("topic", "other") 93 93 94 94 # Add topic color 95 - topic_color = topics.get(topic, {}).get("color", "#6c757d") 95 + topic_color = insights.get(topic, {}).get("color", "#6c757d") 96 96 97 97 occurrence = { 98 98 "title": event.get("title", ""), ··· 565 565 "day": name, 566 566 "has_transcripts": False, 567 567 "has_todos": False, 568 - "has_topics": False, 568 + "has_insights": False, 569 569 "occurrence_count": 0, 570 570 } 571 571 ··· 583 583 # has_transcripts: check if any audio sessions exist 584 584 day_stats["has_transcripts"] = stats_obj.get("audio_sessions", 0) > 0 585 585 586 - # has_topics: check if topics were processed or topic_data exists 587 - day_stats["has_topics"] = ( 588 - stats_obj.get("topics_processed", 0) > 0 or len(topic_data) > 0 586 + # has_insights: check if insights were processed or topic_data exists 587 + day_stats["has_insights"] = ( 588 + stats_obj.get("insights_processed", 0) > 0 or len(topic_data) > 0 589 589 ) 590 590 591 591 # occurrence_count: sum all topic occurrence counts
+12 -12
apps/search/routes.py
··· 13 13 from think.indexer import ( 14 14 search_entities, 15 15 search_events, 16 - search_summaries, 16 + search_insights, 17 17 search_transcripts, 18 18 ) 19 19 ··· 31 31 return jsonify({"total": 0, "results": []}) 32 32 33 33 from convey.utils import parse_pagination_params 34 - from think.utils import get_topics 34 + from think.utils import get_insights 35 35 36 36 limit, offset = parse_pagination_params(default_limit=20) 37 37 38 - topics = get_topics() 38 + insights = get_insights() 39 39 day = request.args.get("day") 40 40 topic_filter = request.args.get("topic") 41 - total, rows = search_summaries(query, limit, offset, day=day, topic=topic_filter) 41 + total, rows = search_insights(query, limit, offset, day=day, topic=topic_filter) 42 42 results = [] 43 43 for r in rows: 44 44 meta = r.get("metadata", {}) 45 45 topic = meta.get("topic", "") 46 - if topic.startswith("topics/"): 47 - topic = topic[len("topics/") :] # noqa: E203 46 + if topic.startswith("insights/"): 47 + topic = topic[len("insights/") :] # noqa: E203 48 48 if topic.endswith(".md"): 49 49 topic = topic[:-3] 50 50 text = r["text"] ··· 56 56 "day": meta.get("day", ""), 57 57 "date": format_date(meta.get("day", "")), 58 58 "topic": topic, 59 - "color": topics.get(topic, {}).get("color"), 59 + "color": insights.get(topic, {}).get("color"), 60 60 "text": markdown.markdown(text, extensions=["extra"]), 61 61 "score": r.get("score", 0.0), 62 62 } ··· 72 72 return jsonify({"total": 0, "results": []}) 73 73 74 74 from convey.utils import parse_pagination_params 75 - from think.utils import get_topics 75 + from think.utils import get_insights 76 76 77 77 limit, offset = parse_pagination_params(default_limit=10) 78 78 79 - topics = get_topics() 79 + insights = get_insights() 80 80 day = request.args.get("day") 81 81 topic_filter = request.args.get("topic") 82 82 total, rows = search_events(query, limit, offset, day=day, topic=topic_filter) ··· 84 84 for r in rows: 85 85 meta = r.get("metadata", {}) 86 86 topic = meta.get("topic", "") 87 - if topic.startswith("topics/"): 88 - topic = topic[len("topics/") :] # noqa: E203 87 + if topic.startswith("insights/"): 88 + topic = topic[len("insights/") :] # noqa: E203 89 89 if topic.endswith(".md"): 90 90 topic = topic[:-3] 91 91 text = r.get("text", "") ··· 109 109 "day": meta.get("day", ""), 110 110 "date": format_date(meta.get("day", "")), 111 111 "topic": topic, 112 - "color": topics.get(topic, {}).get("color"), 112 + "color": insights.get(topic, {}).get("color"), 113 113 "start": start, 114 114 "length": length, 115 115 "text": markdown.markdown(text, extensions=["extra"]),
+4 -4
apps/stats/dashboard.js
··· 446 446 progressCard('Audio Processing', totals.audio_sessions || 0, totals.unprocessed_files || 0) 447 447 ); 448 448 progressSection.appendChild( 449 - progressCard('Topic Summaries', totals.topics_processed || 0, totals.topics_pending || 0) 449 + progressCard('Insight Summaries', totals.insights_processed || 0, totals.insights_pending || 0) 450 450 ); 451 451 452 452 // Token usage setup ··· 510 510 document.getElementById('topicsSection'), 511 511 stats.topic_counts, 512 512 stats.topic_minutes, 513 - data.topics || {} 513 + data.insights || {} 514 514 ); 515 515 } 516 516 517 517 // Render repairs if needed 518 - const repairs = ['unprocessed_files', 'topics_pending']; 518 + const repairs = ['unprocessed_files', 'insights_pending']; 519 519 const hasRepairs = repairs.some(key => (totals[key] || 0) > 0); 520 520 521 521 if (hasRepairs) { ··· 528 528 const repairGrid = alert.querySelector('#repairGrid'); 529 529 const repairLabels = { 530 530 unprocessed_files: 'Unprocessed Media', 531 - topics_pending: 'Topic Summaries' 531 + insights_pending: 'Insight Summaries' 532 532 }; 533 533 534 534 repairs.forEach(key => {
+2 -2
apps/stats/routes.py
··· 7 7 from flask import Blueprint, jsonify, render_template 8 8 9 9 from convey import state 10 - from think.utils import get_topics 10 + from think.utils import get_insights 11 11 12 12 stats_bp = Blueprint( 13 13 "app:stats", ··· 34 34 except Exception: 35 35 pass 36 36 37 - response["topics"] = get_topics() 37 + response["insights"] = get_insights() 38 38 39 39 return jsonify(response)
+1 -1
fixtures/journal/20240101/occurrences.json
··· 3 3 "occurrences": [ 4 4 { 5 5 "type": "meeting", 6 - "source": "topics/meetings.md", 6 + "source": "insights/meetings.md", 7 7 "start": "09:00:00", 8 8 "end": "09:30:00", 9 9 "title": "Team sync",
+3 -3
fixtures/journal/20240101/ponder_flow.md.crumb
··· 1 1 { 2 - "generator": "think.summarize", 3 - "output": "20240101/topics/flow.md", 2 + "generator": "think.insight", 3 + "output": "20240101/insights/flow.md", 4 4 "generated_at": "2024-01-01T12:00:00Z", 5 - "dependencies": [{"type": "file", "path": "think/topics/flow.txt", "mtime": 0}] 5 + "dependencies": [{"type": "file", "path": "think/insights/flow.txt", "mtime": 0}] 6 6 }
fixtures/journal/20240101/topics/flow.md fixtures/journal/20240101/insights/flow.md
+4 -4
fixtures/journal/20240101/topics/meetings.json fixtures/journal/20240101/insights/meetings.json
··· 3 3 "occurrences": [ 4 4 { 5 5 "type": "meeting", 6 - "source": "topics/meetings.md", 6 + "source": "insights/meetings.md", 7 7 "start": "09:00:00", 8 8 "end": "09:30:00", 9 9 "title": "Team sync", ··· 13 13 }, 14 14 { 15 15 "type": "meeting", 16 - "source": "topics/meetings.md", 16 + "source": "insights/meetings.md", 17 17 "start": "10:30:00", 18 18 "end": "11:30:00", 19 19 "title": "Client review with Acme Corp", ··· 23 23 }, 24 24 { 25 25 "type": "meeting", 26 - "source": "topics/meetings.md", 26 + "source": "insights/meetings.md", 27 27 "start": "14:00:00", 28 28 "end": "15:00:00", 29 29 "title": "Sprint planning", ··· 33 33 }, 34 34 { 35 35 "type": "meeting", 36 - "source": "topics/meetings.md", 36 + "source": "insights/meetings.md", 37 37 "start": "16:00:00", 38 38 "end": "16:30:00", 39 39 "title": "Architecture discussion",
fixtures/journal/20240101/topics/meetings.md fixtures/journal/20240101/insights/meetings.md
+3 -3
fixtures/ponder.crumbs
··· 1 1 { 2 - "generator": "think.summarize", 3 - "output": "20250610/topics/flow.md", 2 + "generator": "think.insight", 3 + "output": "20250610/insights/flow.md", 4 4 "generated_at": "2025-06-10T12:00:01Z", 5 5 "dependencies": [ 6 - {"type": "file", "path": "think/topics/flow.txt", "mtime": 1717999380}, 6 + {"type": "file", "path": "think/insights/flow.txt", "mtime": 1717999380}, 7 7 { 8 8 "type": "glob", 9 9 "pattern": "20250610/*_monitor_*_diff.json",
+12 -12
muse/mcp_tools.py
··· 22 22 from think.facets import facet_summary, log_action 23 23 from think.indexer import search_events as search_events_impl 24 24 from think.indexer import search_news as search_news_impl 25 - from think.indexer import search_summaries as search_summaries_impl 25 + from think.indexer import search_insights as search_insights_impl 26 26 from think.indexer import search_transcripts as search_transcripts_impl 27 27 from think.messages import send_message as send_message_impl 28 28 from think.utils import get_raw_file ··· 52 52 # Tool packs - logical groupings of tools 53 53 TOOL_PACKS = { 54 54 "journal": [ 55 - "search_summaries", 55 + "search_insights", 56 56 "search_transcripts", 57 57 "search_events", 58 58 "search_news", ··· 297 297 298 298 299 299 @register_tool(annotations=HINTS) 300 - def search_summaries( 300 + def search_insights( 301 301 query: str, 302 302 limit: int = 5, 303 303 offset: int = 0, ··· 305 305 topic: str | None = None, 306 306 day: str | None = None, 307 307 ) -> dict[str, Any]: 308 - """Search across journal topic summaries using semantic full-text search. 308 + """Search across journal insight summaries using semantic full-text search. 309 309 310 - This tool searches through pre-processed topic summaries that represent 310 + This tool searches through pre-processed insight summaries that represent 311 311 key themes and subjects from your journal entries. Use this when looking 312 312 for high-level concepts, themes, or when you need an overview of topics 313 313 discussed over time. ··· 321 321 322 322 Returns: 323 323 Dictionary containing: 324 - - total: Total number of matching topics 324 + - total: Total number of matching insights 325 325 - limit: Current limit value used for this query 326 326 - offset: Current offset value used for this query 327 - - results: List of matching topics with day, topic, and text excerpt, ordered by text relevance 327 + - results: List of matching insights with day, topic, and text excerpt, ordered by text relevance 328 328 329 329 Examples: 330 - - search_summaries("machine learning projects") 331 - - search_summaries("team retrospectives", limit=10) 332 - - search_summaries("planning", topic="standup") 333 - - search_summaries("meetings", day="20240101") 330 + - search_insights("machine learning projects") 331 + - search_insights("team retrospectives", limit=10) 332 + - search_insights("planning", topic="standup") 333 + - search_insights("meetings", day="20240101") 334 334 """ 335 335 try: 336 336 kwargs = {} ··· 338 338 kwargs["topic"] = topic 339 339 if day is not None: 340 340 kwargs["day"] = day 341 - total, results = search_summaries_impl(query, limit, offset, **kwargs) 341 + total, results = search_insights_impl(query, limit, offset, **kwargs) 342 342 343 343 items = [] 344 344 for r in results:
+2 -2
pyproject.toml
··· 93 93 sunstone = "think.sunstone:main" 94 94 convey = "convey.cli:main" 95 95 convey-screenshot = "convey.screenshot:main" 96 - think-summarize = "think.summarize:main" 96 + think-insight = "think.insight:main" 97 97 think-cluster = "think.cluster:main" 98 98 think-planner = "think.planner:main" 99 99 think-journal-stats = "think.journal_stats:main" ··· 130 130 131 131 [tool.setuptools.package-data] 132 132 apps = ["*/templates/*.html"] 133 - think = ["*.txt", "topics/*.txt"] 133 + think = ["*.txt", "insights/*.txt"] 134 134 muse = ["agents/*.txt", "agents/*.json"] 135 135 observe = ["*.txt", "*.json"] 136 136 convey = [
+1 -1
tests/test_dream_full.py
··· 24 24 ) 25 25 mod.main() 26 26 assert any(c[0] == "observe-sense" for c in called) 27 - assert any(c[0] == "think-summarize" for c in called) 27 + assert any(c[0] == "think-insight" for c in called)
+4 -4
tests/test_get_topics.py tests/test_get_insights.py
··· 2 2 import os 3 3 4 4 5 - def test_get_topics(): 5 + def test_get_insights(): 6 6 utils = importlib.import_module("think.utils") 7 - topics = utils.get_topics() 8 - assert "flow" in topics 9 - info = topics["flow"] 7 + insights = utils.get_insights() 8 + assert "flow" in insights 9 + info = insights["flow"] 10 10 assert os.path.basename(info["path"]) == "flow.txt" 11 11 assert isinstance(info["color"], str) 12 12 assert isinstance(info["mtime"], int)
+17 -17
tests/test_indexer.py
··· 32 32 "occurrences": [ 33 33 { 34 34 "type": "meeting", 35 - "source": "topics/meetings.md", 35 + "source": "insights/meetings.md", 36 36 "start": "09:00:00", 37 37 "end": "09:30:00", 38 38 "title": "Standup", ··· 41 41 } 42 42 ], 43 43 } 44 - topics_dir = day / "topics" 45 - topics_dir.mkdir() 46 - (topics_dir / "meetings.json").write_text(json.dumps(data)) 44 + insights_dir = day / "insights" 45 + insights_dir.mkdir() 46 + (insights_dir / "meetings.json").write_text(json.dumps(data)) 47 47 mod.scan_events(str(journal), verbose=True) 48 48 total, results = mod.search_events("Standup") 49 49 assert total == 1 ··· 57 57 os.environ["JOURNAL_PATH"] = str(journal) 58 58 day = journal / "20240102" 59 59 day.mkdir() 60 - topics_dir = day / "topics" 61 - topics_dir.mkdir() 62 - (topics_dir / "files.md").write_text("This is a test sentence.\n") 63 - mod.scan_summaries(str(journal), verbose=True) 64 - total, results = mod.search_summaries("test") 60 + insights_dir = day / "insights" 61 + insights_dir.mkdir() 62 + (insights_dir / "files.md").write_text("This is a test sentence.\n") 63 + mod.scan_insights(str(journal), verbose=True) 64 + total, results = mod.search_insights("test") 65 65 assert total == 1 66 - assert results and results[0]["metadata"]["path"] == "20240102/topics/files.md" 66 + assert results and results[0]["metadata"]["path"] == "20240102/insights/files.md" 67 67 assert total == 1 68 - assert results and results[0]["metadata"]["path"] == "20240102/topics/files.md" 68 + assert results and results[0]["metadata"]["path"] == "20240102/insights/files.md" 69 69 70 70 71 71 def test_raw_index(tmp_path): ··· 111 111 os.environ["JOURNAL_PATH"] = str(journal) 112 112 day = journal / "20240104" 113 113 day.mkdir() 114 - topics_dir = day / "topics" 115 - topics_dir.mkdir() 116 - md = topics_dir / "files.md" 114 + insights_dir = day / "insights" 115 + insights_dir.mkdir() 116 + md = insights_dir / "files.md" 117 117 md.write_text("Cached sentence.\n") 118 118 119 119 # First scan indexes the file 120 - assert mod.scan_summaries(str(journal)) is True 120 + assert mod.scan_insights(str(journal)) is True 121 121 122 122 # Second scan without modification should be a no-op 123 - assert mod.scan_summaries(str(journal)) is False 123 + assert mod.scan_insights(str(journal)) is False 124 124 125 125 # Modify file to trigger reindex 126 126 import time as _time 127 127 128 128 _time.sleep(1) 129 129 md.write_text("Updated sentence.\n") 130 - assert mod.scan_summaries(str(journal)) is True 130 + assert mod.scan_insights(str(journal)) is True 131 131 132 132 133 133 def test_search_raws_day(tmp_path):
+3 -3
tests/test_journal_stats.py
··· 21 21 (day / "123456_raw.flac").write_bytes(b"RIFF") 22 22 23 23 (day / "entities.md").write_text("") 24 - (day / "topics").mkdir() 25 - (day / "topics" / "flow.md").write_text("") 24 + (day / "insights").mkdir() 25 + (day / "insights" / "flow.md").write_text("") 26 26 data = { 27 27 "day": "20240101", 28 28 "occurrences": [ ··· 39 39 } 40 40 ], 41 41 } 42 - (day / "topics" / "meetings.json").write_text(json.dumps(data)) 42 + (day / "insights" / "meetings.json").write_text(json.dumps(data)) 43 43 monkeypatch.setenv("JOURNAL_PATH", str(journal)) 44 44 js = stats_mod.JournalStats() 45 45 day_data = js.scan_day("20240101", str(day))
+22 -22
tests/test_summarize_full.py tests/test_insight_full.py
··· 23 23 24 24 25 25 def test_ponder_main(tmp_path, monkeypatch): 26 - mod = importlib.import_module("think.summarize") 26 + mod = importlib.import_module("think.insight") 27 27 day_dir = copy_day(tmp_path) 28 28 prompt = tmp_path / "prompt.txt" 29 29 prompt.write_text("prompt") ··· 55 55 monkeypatch.setenv("GOOGLE_API_KEY", "x") 56 56 57 57 monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) 58 - monkeypatch.setattr("sys.argv", ["think-ponder", "20240101", "-f", str(prompt)]) 58 + monkeypatch.setattr("sys.argv", ["think-insight", "20240101", "-f", str(prompt)]) 59 59 mod.main() 60 60 61 - md = day_dir / "topics" / "prompt.md" 62 - js = day_dir / "topics" / "prompt.json" 61 + md = day_dir / "insights" / "prompt.md" 62 + js = day_dir / "insights" / "prompt.json" 63 63 assert md.read_text() == "summary" 64 64 data = json.loads(js.read_text()) 65 65 assert data["day"] == "20240101" ··· 71 71 72 72 73 73 def test_ponder_extra_instructions(tmp_path, monkeypatch): 74 - mod = importlib.import_module("think.summarize") 74 + mod = importlib.import_module("think.insight") 75 75 day_dir = copy_day(tmp_path) 76 - topic_file = Path(mod.__file__).resolve().parent / "topics" / "flow.txt" 76 + insight_file = Path(mod.__file__).resolve().parent / "insights" / "flow.txt" 77 77 78 78 # Remove existing flow.md to ensure mock content is used 79 - flow_md = day_dir / "topics" / "flow.md" 79 + flow_md = day_dir / "insights" / "flow.md" 80 80 if flow_md.exists(): 81 81 flow_md.unlink() 82 82 ··· 107 107 monkeypatch.setenv("GOOGLE_API_KEY", "x") 108 108 109 109 monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) 110 - monkeypatch.setattr("sys.argv", ["think-ponder", "20240101", "-f", str(topic_file)]) 110 + monkeypatch.setattr("sys.argv", ["think-insight", "20240101", "-f", str(insight_file)]) 111 111 mod.main() 112 112 113 - md = day_dir / "topics" / "flow.md" 114 - js = day_dir / "topics" / "flow.json" 113 + md = day_dir / "insights" / "flow.md" 114 + js = day_dir / "insights" / "flow.json" 115 115 assert md.read_text() == "summary" 116 116 data = json.loads(js.read_text()) 117 117 assert data["day"] == "20240101" 118 118 assert data["occurrences"] 119 - # Facet summaries are prepended to topic-specific occurrence instructions 119 + # Facet summaries are prepended to insight-specific occurrence instructions 120 120 assert captured["extra"] 121 121 assert captured["extra"].startswith("No facets found.") 122 122 123 123 124 124 def test_ponder_skip_occurrences(tmp_path, monkeypatch): 125 - mod = importlib.import_module("think.summarize") 125 + mod = importlib.import_module("think.insight") 126 126 day_dir = copy_day(tmp_path) 127 - topic_file = Path(mod.__file__).resolve().parent / "topics" / "flow.txt" 127 + insight_file = Path(mod.__file__).resolve().parent / "insights" / "flow.txt" 128 128 129 129 # Remove existing flow.md to ensure mock content is used 130 - flow_md = day_dir / "topics" / "flow.md" 130 + flow_md = day_dir / "insights" / "flow.md" 131 131 if flow_md.exists(): 132 132 flow_md.unlink() 133 133 134 - def fake_get_topics(): 134 + def fake_get_insights(): 135 135 utils = importlib.import_module("think.utils") 136 - topics = utils.get_topics() 137 - topics["flow"]["occurrences"] = False 138 - return topics 136 + insights = utils.get_insights() 137 + insights["flow"]["occurrences"] = False 138 + return insights 139 139 140 - monkeypatch.setattr(mod, "get_topics", fake_get_topics) 140 + monkeypatch.setattr(mod, "get_insights", fake_get_insights) 141 141 monkeypatch.setattr( 142 142 mod, 143 143 "send_markdown", ··· 154 154 monkeypatch.setenv("GOOGLE_API_KEY", "x") 155 155 156 156 monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) 157 - monkeypatch.setattr("sys.argv", ["think-ponder", "20240101", "-f", str(topic_file)]) 157 + monkeypatch.setattr("sys.argv", ["think-insight", "20240101", "-f", str(insight_file)]) 158 158 mod.main() 159 159 160 - md = day_dir / "topics" / "flow.md" 161 - js = day_dir / "topics" / "flow.json" 160 + md = day_dir / "insights" / "flow.md" 161 + js = day_dir / "insights" / "flow.json" 162 162 assert md.read_text() == "summary" 163 163 assert not js.exists() 164 164 assert md.with_suffix(md.suffix + ".crumb").is_file()
+9 -9
tests/test_summarize_scan_day.py tests/test_insight_scan_day.py
··· 18 18 shutil.copytree(item, dest / item.name, dirs_exist_ok=True) 19 19 else: 20 20 shutil.copy2(item, dest / item.name) 21 - topics = dest / "topics" 22 - topics.mkdir(exist_ok=True) # Allow existing directory 23 - (topics / "flow.md").write_text("done") 21 + insights = dest / "insights" 22 + insights.mkdir(exist_ok=True) # Allow existing directory 23 + (insights / "flow.md").write_text("done") 24 24 return dest 25 25 26 26 27 27 def test_scan_day(tmp_path, monkeypatch): 28 - mod = importlib.import_module("think.summarize") 28 + mod = importlib.import_module("think.insight") 29 29 day_dir = copy_day(tmp_path) 30 30 monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) 31 31 32 32 info = mod.scan_day("20240101") 33 - assert "topics/flow.md" in info["processed"] 34 - assert "topics/media.md" in info["repairable"] 33 + assert "insights/flow.md" in info["processed"] 34 + assert "insights/media.md" in info["repairable"] 35 35 36 - (day_dir / "topics" / "media.md").write_text("done") 36 + (day_dir / "insights" / "media.md").write_text("done") 37 37 info_after = mod.scan_day("20240101") 38 - assert "topics/media.md" in info_after["processed"] 39 - assert "topics/media.md" not in info_after["repairable"] 38 + assert "insights/media.md" in info_after["processed"] 39 + assert "insights/media.md" not in info_after["repairable"]
+6 -6
think/README.md
··· 14 14 15 15 The package exposes several commands: 16 16 17 - - `think-summarize` builds a Markdown summary of a day's recordings using a Gemini prompt. 17 + - `think-insight` builds a Markdown summary of a day's recordings using a Gemini prompt. 18 18 - `think-cluster` groups audio and screen JSON files into report sections. Use `--start` and 19 19 `--length` to limit the report to a specific time range. 20 20 - `think-dream` runs the above tools for a single day. ··· 23 23 - `muse-cortex` starts a WebSocket API server for managing AI agent instances. 24 24 25 25 ```bash 26 - think-summarize YYYYMMDD [-f PROMPT] [-p] [-c] [--force] [-v] 26 + think-insight YYYYMMDD [-f PROMPT] [-p] [-c] [--force] [-v] 27 27 think-cluster YYYYMMDD [--start HHMMSS --length MINUTES] 28 28 think-dream [--day YYYYMMDD] [--force] [--rebuild] 29 29 think-supervisor [--no-observers] ··· 141 141 same code works with any implementation, allowing you to choose between OpenAI, 142 142 Gemini or Claude at runtime. 143 143 144 - ## Topic map keys 144 + ## Insight map keys 145 145 146 - `think.utils.get_topics()` reads the prompt files under `think/topics` and 147 - returns a dictionary keyed by topic name. Each entry contains: 146 + `think.utils.get_insights()` reads the prompt files under `think/insights` and 147 + returns a dictionary keyed by insight name. Each entry contains: 148 148 149 149 - `path` – the prompt text file path 150 150 - `color` – UI color hex string 151 151 - `mtime` – modification time of the `.txt` file 152 - - Any additional keys from the matching `<topic>.json` metadata file such as 152 + - Any additional keys from the matching `<insight>.json` metadata file such as 153 153 `title`, `description` or `occurrences` 154 154 155 155 ## Cortex API
+11 -10
think/dream.py
··· 4 4 from datetime import datetime, timedelta 5 5 6 6 from think.runner import run_task 7 - from think.utils import day_log, day_path, get_topics, setup_cli 7 + from think.utils import day_log, day_path, get_insights, setup_cli 8 8 9 9 10 10 def run_command(cmd: list[str], day: str) -> bool: ··· 46 46 logging.info("Running segment processing for %s/%s", day, segment) 47 47 target_frequency = "segment" 48 48 # No sense repair for segments (already processed during observation) 49 + 49 50 else: 50 51 logging.info("Running daily processing for %s", day) 51 52 target_frequency = "daily" ··· 55 56 cmd.append("-v") 56 57 commands.append(cmd) 57 58 58 - # Run topics filtered by frequency 59 - topics = get_topics() 60 - for topic_name, topic_data in topics.items(): 61 - # Skip disabled topics 62 - if topic_data.get("disabled", False): 63 - logging.info("Skipping disabled topic: %s", topic_name) 59 + # Run insights filtered by frequency 60 + insights = get_insights() 61 + for insight_name, insight_data in insights.items(): 62 + # Skip disabled insights 63 + if insight_data.get("disabled", False): 64 + logging.info("Skipping disabled insight: %s", insight_name) 64 65 continue 65 66 66 67 # Filter by frequency (defaults to "daily" if not specified) 67 - topic_frequency = topic_data.get("frequency", "daily") 68 - if topic_frequency != target_frequency: 68 + insight_frequency = insight_data.get("frequency", "daily") 69 + if insight_frequency != target_frequency: 69 70 continue 70 71 71 - cmd = ["think-summarize", day, "-f", topic_data["path"], "-p"] 72 + cmd = ["think-insight", day, "-f", insight_data["path"], "-p"] 72 73 if segment: 73 74 cmd.extend(["--segment", segment]) 74 75 if verbose:
+14 -14
think/indexer/__init__.py
··· 1 - """Indexer package for summary outputs, events, transcripts, and entities. 1 + """Indexer package for insights, events, transcripts, and entities. 2 2 3 3 This module provides backward compatibility by re-exporting all the main 4 4 functions from the sub-modules. ··· 38 38 search_news, 39 39 ) 40 40 41 - # Import from summaries 42 - from .summaries import ( 43 - TOPIC_BASENAMES, 44 - TOPIC_DIR, 45 - find_summary_files, 46 - scan_summaries, 47 - search_summaries, 41 + # Import from insights 42 + from .insights import ( 43 + INSIGHT_TYPES, 44 + INSIGHTS_DIR, 45 + find_insight_files, 46 + scan_insights, 47 + search_insights, 48 48 split_sentences, 49 49 ) 50 50 ··· 66 66 "SCHEMAS", 67 67 "get_index", 68 68 "reset_index", 69 - # Summaries 70 - "TOPIC_BASENAMES", 71 - "TOPIC_DIR", 72 - "find_summary_files", 73 - "scan_summaries", 74 - "search_summaries", 69 + # Insights 70 + "INSIGHT_TYPES", 71 + "INSIGHTS_DIR", 72 + "find_insight_files", 73 + "scan_insights", 74 + "search_insights", 75 75 "split_sentences", 76 76 # Events 77 77 "scan_events",
+9 -9
think/indexer/cli.py
··· 9 9 from .core import reset_index 10 10 from .entities import scan_entities, search_entities 11 11 from .events import scan_events, search_events 12 + from .insights import scan_insights, search_insights 12 13 from .news import scan_news, search_news 13 - from .summaries import scan_summaries, search_summaries 14 14 from .transcripts import scan_transcripts, search_transcripts 15 15 16 16 ··· 32 32 ) 33 33 parser.add_argument( 34 34 "--index", 35 - choices=["summaries", "events", "transcripts", "entities", "news"], 35 + choices=["insights", "events", "transcripts", "entities", "news"], 36 36 help="Which index to operate on", 37 37 ) 38 38 parser.add_argument( ··· 114 114 115 115 if args.rescan_all: 116 116 # Rescan all indexes 117 - indexes = ["summaries", "events", "transcripts", "entities", "news"] 117 + indexes = ["insights", "events", "transcripts", "entities", "news"] 118 118 for index_name in indexes: 119 119 if index_name == "transcripts": 120 120 changed = scan_transcripts(journal, verbose=args.verbose) ··· 124 124 changed = scan_events(journal, verbose=args.verbose) 125 125 if changed: 126 126 journal_log(f"indexer {index_name} rescan ok") 127 - elif index_name == "summaries": 128 - changed = scan_summaries(journal, verbose=args.verbose) 127 + elif index_name == "insights": 128 + changed = scan_insights(journal, verbose=args.verbose) 129 129 if changed: 130 130 journal_log(f"indexer {index_name} rescan ok") 131 131 elif index_name == "entities": ··· 161 161 changed = scan_events(journal, verbose=args.verbose) 162 162 if changed: 163 163 journal_log("indexer events rescan ok") 164 - elif args.index == "summaries": 165 - changed = scan_summaries(journal, verbose=args.verbose) 164 + elif args.index == "insights": 165 + changed = scan_insights(journal, verbose=args.verbose) 166 166 if changed: 167 - journal_log("indexer summaries rescan ok") 167 + journal_log("indexer insights rescan ok") 168 168 elif args.index == "entities": 169 169 changed = scan_entities(journal, verbose=args.verbose) 170 170 if changed: ··· 192 192 search_func = search_news 193 193 query_kwargs = {"facet": args.facet, "day": args.day} 194 194 else: 195 - search_func = search_summaries 195 + search_func = search_insights 196 196 query_kwargs = {} 197 197 if args.query: 198 198 # Single query mode - run query and exit
+3 -3
think/indexer/core.py
··· 16 16 17 17 # Mapping of index types to their SQLite filenames 18 18 DB_NAMES = { 19 - "summaries": "summaries.sqlite", 19 + "insights": "insights.sqlite", 20 20 "events": "events.sqlite", 21 21 "transcripts": "transcripts.sqlite", 22 22 "entities": "entities.sqlite", ··· 25 25 26 26 # SQL statements to create required tables per index 27 27 SCHEMAS = { 28 - "summaries": [ 28 + "insights": [ 29 29 "CREATE TABLE IF NOT EXISTS files(path TEXT PRIMARY KEY, mtime INTEGER)", 30 30 """ 31 - CREATE VIRTUAL TABLE IF NOT EXISTS summaries_text USING fts5( 31 + CREATE VIRTUAL TABLE IF NOT EXISTS insights_text USING fts5( 32 32 sentence, path UNINDEXED, day UNINDEXED, topic UNINDEXED, position UNINDEXED 33 33 ) 34 34 """,
+2 -2
think/indexer/events.py
··· 7 7 from typing import Any, Dict, List 8 8 9 9 from .core import _scan_files, get_index 10 - from .summaries import find_summary_files 10 + from .insights import find_insight_files 11 11 12 12 13 13 def _index_events(conn: sqlite3.Connection, rel: str, path: str, verbose: bool) -> None: ··· 50 50 """Index event JSON files.""" 51 51 logger = logging.getLogger(__name__) 52 52 conn, _ = get_index(index="events", journal=journal) 53 - files = find_summary_files(journal, (".json",)) 53 + files = find_insight_files(journal, (".json",)) 54 54 if files: 55 55 logger.info("\nIndexing %s event files...", len(files)) 56 56 changed = _scan_files(
+27 -27
think/indexer/summaries.py think/indexer/insights.py
··· 1 - """Summary indexing and search functionality.""" 1 + """Insight indexing and search functionality.""" 2 2 3 3 import logging 4 4 import os ··· 8 8 import sqlite_utils 9 9 from syntok import segmenter 10 10 11 - from think.utils import day_dirs, get_topics 11 + from think.utils import day_dirs, get_insights 12 12 13 13 from .core import _scan_files, get_index 14 14 15 15 # Sentence indexing helpers 16 - TOPIC_DIR = os.path.join(os.path.dirname(__file__), "..", "topics") 17 - TOPIC_BASENAMES = sorted(get_topics().keys()) 16 + INSIGHTS_DIR = os.path.join(os.path.dirname(__file__), "..", "insights") 17 + INSIGHT_TYPES = sorted(get_insights().keys()) 18 18 19 19 20 20 def split_sentences(text: str) -> List[str]: ··· 31 31 return sentences 32 32 33 33 34 - def find_summary_files( 34 + def find_insight_files( 35 35 journal: str, exts: Tuple[str, ...] | None = None 36 36 ) -> Dict[str, str]: 37 - """Map relative summary file path to full path filtered by ``exts``.""" 37 + """Map relative insight file path to full path filtered by ``exts``.""" 38 38 files: Dict[str, str] = {} 39 39 exts = exts or (".md", ".json") 40 40 for day, day_path in day_dirs().items(): 41 - topics_dir = os.path.join(day_path, "topics") 42 - if not os.path.isdir(topics_dir): 41 + insights_dir = os.path.join(day_path, "insights") 42 + if not os.path.isdir(insights_dir): 43 43 continue 44 - for name in os.listdir(topics_dir): 44 + for name in os.listdir(insights_dir): 45 45 base, ext = os.path.splitext(name) 46 - if ext in exts and base in TOPIC_BASENAMES: 47 - rel = os.path.join(day, "topics", name) 48 - files[rel] = os.path.join(topics_dir, name) 46 + if ext in exts and base in INSIGHT_TYPES: 47 + rel = os.path.join(day, "insights", name) 48 + files[rel] = os.path.join(insights_dir, name) 49 49 return files 50 50 51 51 52 52 def _index_sentences( 53 53 conn: sqlite3.Connection, rel: str, path: str, verbose: bool 54 54 ) -> None: 55 - """Index sentences from a summary markdown file.""" 55 + """Index sentences from an insight markdown file.""" 56 56 logger = logging.getLogger(__name__) 57 57 with open(path, "r", encoding="utf-8") as f: 58 58 text = f.read() ··· 63 63 for pos, sentence in enumerate(sentences): 64 64 conn.execute( 65 65 ( 66 - "INSERT INTO summaries_text(sentence, path, day, topic, position) VALUES (?, ?, ?, ?, ?)" 66 + "INSERT INTO insights_text(sentence, path, day, topic, position) VALUES (?, ?, ?, ?, ?)" 67 67 ), 68 68 (sentence, rel, day, topic, pos), 69 69 ) 70 70 71 71 72 - def scan_summaries(journal: str, verbose: bool = False) -> bool: 73 - """Index sentences from summary markdown files.""" 72 + def scan_insights(journal: str, verbose: bool = False) -> bool: 73 + """Index sentences from insight markdown files.""" 74 74 logger = logging.getLogger(__name__) 75 - conn, _ = get_index(index="summaries", journal=journal) 76 - files = find_summary_files(journal, (".md",)) 75 + conn, _ = get_index(index="insights", journal=journal) 76 + files = find_insight_files(journal, (".md",)) 77 77 if files: 78 - logger.info("\nIndexing %s summary files...", len(files)) 78 + logger.info("\nIndexing %s insight files...", len(files)) 79 79 changed = _scan_files( 80 80 conn, 81 81 files, 82 - "DELETE FROM summaries_text WHERE path=?", 82 + "DELETE FROM insights_text WHERE path=?", 83 83 _index_sentences, 84 84 verbose, 85 85 ) ··· 89 89 return changed 90 90 91 91 92 - def search_summaries( 92 + def search_insights( 93 93 query: str, 94 94 limit: int = 5, 95 95 offset: int = 0, ··· 97 97 day: str | None = None, 98 98 topic: str | None = None, 99 99 ) -> tuple[int, List[Dict[str, str]]]: 100 - """Search the summary sentence index and return total count and results.""" 100 + """Search the insight sentence index and return total count and results.""" 101 101 102 - conn, _ = get_index(index="summaries") 102 + conn, _ = get_index(index="insights") 103 103 db = sqlite_utils.Database(conn) 104 104 quoted = db.quote(query) 105 105 106 - where_clause = f"summaries_text MATCH {quoted}" 106 + where_clause = f"insights_text MATCH {quoted}" 107 107 params: List[str] = [] 108 108 109 109 if day: ··· 114 114 params.append(f"%{topic}%") 115 115 116 116 total = conn.execute( 117 - f"SELECT count(*) FROM summaries_text WHERE {where_clause}", params 117 + f"SELECT count(*) FROM insights_text WHERE {where_clause}", params 118 118 ).fetchone()[0] 119 119 120 120 cursor = conn.execute( 121 121 f""" 122 - SELECT sentence, path, day, topic, position, bm25(summaries_text) as rank 123 - FROM summaries_text WHERE {where_clause} ORDER BY rank LIMIT ? OFFSET ? 122 + SELECT sentence, path, day, topic, position, bm25(insights_text) as rank 123 + FROM insights_text WHERE {where_clause} ORDER BY rank LIMIT ? OFFSET ? 124 124 """, 125 125 params + [limit, offset], 126 126 )
+19 -19
think/journal_stats.py
··· 9 9 from typing import Dict 10 10 11 11 from observe.utils import load_analysis_frames 12 - from think.summarize import scan_day as summarize_scan_day 12 + from think.insight import scan_day as insight_scan_day 13 13 from think.utils import day_dirs, setup_cli 14 14 15 15 logger = logging.getLogger(__name__) ··· 49 49 files.extend(day_dir.glob("*_screen.mp4")) 50 50 files.extend(day_dir.glob("*_screen.mov")) 51 51 52 - topics = day_dir / "topics" 53 - if topics.is_dir(): 54 - files.extend(topics.glob("*.json")) 55 - files.extend(topics.glob("*.md")) 52 + insights = day_dir / "insights" 53 + if insights.is_dir(): 54 + files.extend(insights.glob("*.json")) 55 + files.extend(insights.glob("*.md")) 56 56 57 57 if not files: 58 58 return 0.0 ··· 235 235 unprocessed.extend(day_dir.glob("*_screen.mov")) 236 236 stats["unprocessed_files"] = len(unprocessed) 237 237 238 - # --- Topic summaries --- 239 - summary_info = summarize_scan_day(day) 240 - stats["topics_processed"] = len(summary_info["processed"]) 241 - stats["topics_pending"] = len(summary_info["repairable"]) 238 + # --- Insight summaries --- 239 + insight_info = insight_scan_day(day) 240 + stats["insights_processed"] = len(insight_info["processed"]) 241 + stats["insights_pending"] = len(insight_info["repairable"]) 242 242 243 - # --- Topic occurrences and heatmap --- 244 - topics_dir = day_dir / "topics" 243 + # --- Insight occurrences and heatmap --- 244 + insights_dir = day_dir / "insights" 245 245 weekday = datetime.strptime(day, "%Y%m%d").weekday() 246 246 247 - if topics_dir.is_dir(): 248 - for fname in os.listdir(topics_dir): 247 + if insights_dir.is_dir(): 248 + for fname in os.listdir(insights_dir): 249 249 base, ext = os.path.splitext(fname) 250 250 if ext != ".json": 251 251 continue 252 - topic_file = topics_dir / fname 252 + insight_file = insights_dir / fname 253 253 try: 254 - with open(topic_file, "r", encoding="utf-8") as f: 254 + with open(insight_file, "r", encoding="utf-8") as f: 255 255 data = json.load(f) 256 256 except json.JSONDecodeError as e: 257 - logger.warning(f"Invalid JSON in {topic_file}: {e}") 257 + logger.warning(f"Invalid JSON in {insight_file}: {e}") 258 258 continue 259 259 except (OSError, IOError) as e: 260 - logger.warning(f"Error reading {topic_file}: {e}") 260 + logger.warning(f"Error reading {insight_file}: {e}") 261 261 continue 262 262 263 263 items = data.get("occurrences", []) if isinstance(data, dict) else data 264 264 if not isinstance(items, list): 265 - logger.debug(f"No occurrences list in {topic_file}") 265 + logger.debug(f"No occurrences list in {insight_file}") 266 266 continue 267 267 268 268 # Initialize topic data ··· 278 278 eh, em, es = map(int, end.split(":")) 279 279 except (ValueError, AttributeError, TypeError): 280 280 logger.debug( 281 - f"Invalid timestamp in {topic_file}: {start} - {end}" 281 + f"Invalid timestamp in {insight_file}: {start} - {end}" 282 282 ) 283 283 continue 284 284
+28 -28
think/summarize.py think/insight.py
··· 14 14 PromptNotFoundError, 15 15 day_log, 16 16 day_path, 17 - get_topics, 17 + get_insights, 18 18 load_prompt, 19 19 setup_cli, 20 20 ) ··· 22 22 COMMON_SYSTEM_INSTRUCTION = "You are an expert productivity analyst tasked with analyzing a full workday transcript containing both audio conversations and screen activity data, segmented into 5-minute chunks. You will be given the transcripts and then following that you will have a detailed user request for how to process them. Please follow those instructions carefully. Take time to consider all of the nuance of the interactions from the day, deeply think through how best to prioritize the most important aspects and understandings, formulate the best approach for each step of the analysis." 23 23 24 24 25 - def _topic_basenames() -> list[str]: 26 - """Return available topic basenames under :data:`TOPICS`.""" 27 - return sorted(get_topics().keys()) 25 + def _insight_basenames() -> list[str]: 26 + """Return available insight basenames.""" 27 + return sorted(get_insights().keys()) 28 28 29 29 30 30 def _output_paths( ··· 34 34 35 35 Args: 36 36 day_dir: Day directory path (YYYYMMDD) 37 - basename: Topic basename 37 + basename: Insight basename 38 38 segment: Optional segment key (HHMMSS_LEN) 39 39 40 40 Returns: 41 41 (md_path, json_path) tuple 42 - - Daily: YYYYMMDD/topics/{basename}.md 42 + - Daily: YYYYMMDD/insights/{basename}.md 43 43 - Segment: YYYYMMDD/{segment}/{basename}.md 44 44 """ 45 45 day = Path(day_dir) 46 46 47 47 if segment: 48 - # Segment topics go directly in segment directory 48 + # Segment insights go directly in segment directory 49 49 segment_dir = day / segment 50 50 return segment_dir / f"{basename}.md", segment_dir / f"{basename}.json" 51 51 else: 52 - # Daily topics go in topics/ subdirectory 53 - topic_dir = day / "topics" 54 - return topic_dir / f"{basename}.md", topic_dir / f"{basename}.json" 52 + # Daily insights go in insights/ subdirectory 53 + insights_dir = day / "insights" 54 + return insights_dir / f"{basename}.md", insights_dir / f"{basename}.json" 55 55 56 56 57 57 def scan_day(day: str) -> dict[str, list[str]]: 58 - """Return lists of processed and pending summary markdown files.""" 58 + """Return lists of processed and pending insight markdown files.""" 59 59 day_dir = day_path(day) 60 - summarized: list[str] = [] 61 - unsummarized: list[str] = [] 62 - for base in _topic_basenames(): 60 + processed: list[str] = [] 61 + pending: list[str] = [] 62 + for base in _insight_basenames(): 63 63 md_path, _ = _output_paths(day_dir, base) 64 64 if md_path.exists(): 65 - summarized.append(os.path.join("topics", md_path.name)) 65 + processed.append(os.path.join("insights", md_path.name)) 66 66 else: 67 - unsummarized.append(os.path.join("topics", md_path.name)) 68 - return {"processed": sorted(summarized), "repairable": sorted(unsummarized)} 67 + pending.append(os.path.join("insights", md_path.name)) 68 + return {"processed": sorted(processed), "repairable": sorted(pending)} 69 69 70 70 71 71 def count_tokens(markdown: str, prompt: str, api_key: str, model: str) -> None: ··· 251 251 else: 252 252 markdown, file_count = cluster(args.day) 253 253 day_dir = str(day_path(args.day)) 254 - topic_basename = Path(args.topic).stem 255 - topic_meta = get_topics().get(topic_basename, {}) 256 - extra_occ = topic_meta.get("occurrences") 254 + insight_basename = Path(args.topic).stem 255 + insight_meta = get_insights().get(insight_basename, {}) 256 + extra_occ = insight_meta.get("occurrences") 257 257 skip_occ = extra_occ is False 258 258 success = False 259 259 ··· 266 266 if not api_key: 267 267 parser.error("GOOGLE_API_KEY not found in environment") 268 268 269 - topic_path = Path(args.topic) 269 + insight_path = Path(args.topic) 270 270 try: 271 - topic_prompt = load_prompt( 272 - topic_path.stem, base_dir=topic_path.parent, include_journal=True 271 + insight_prompt = load_prompt( 272 + insight_path.stem, base_dir=insight_path.parent, include_journal=True 273 273 ) 274 274 except PromptNotFoundError: 275 - parser.error(f"Topic file not found: {topic_path}") 275 + parser.error(f"Insight file not found: {insight_path}") 276 276 277 - prompt = topic_prompt.text 277 + prompt = insight_prompt.text 278 278 279 279 model = GEMINI_PRO if args.pro else GEMINI_FLASH 280 280 day = args.day ··· 288 288 count_tokens(markdown, prompt, api_key, model) 289 289 return 290 290 291 - md_path, json_path = _output_paths(day_dir, topic_basename, segment=args.segment) 291 + md_path, json_path = _output_paths(day_dir, insight_basename, segment=args.segment) 292 292 # Use cache key scoped to day or segment 293 293 if args.segment: 294 294 cache_display_name = f"{day}_{args.segment}" ··· 334 334 335 335 crumb_builder = ( 336 336 CrumbBuilder() 337 - .add_file(str(topic_prompt.path)) 337 + .add_file(str(insight_prompt.path)) 338 338 .add_glob(os.path.join(day_dir, "*/audio.jsonl")) 339 339 .add_glob(os.path.join(day_dir, "*/*_audio.jsonl")) # Split audio 340 340 .add_glob(os.path.join(day_dir, "*/screen.md")) ··· 422 422 success = True 423 423 424 424 finally: 425 - msg = f"summarize {topic_basename} {'ok' if success else 'failed'}" 425 + msg = f"insight {insight_basename} {'ok' if success else 'failed'}" 426 426 if args.force: 427 427 msg += " --force" 428 428 day_log(args.day, msg)
think/topics/activity.json think/insights/activity.json
think/topics/activity.txt think/insights/activity.txt
think/topics/decisions.json think/insights/decisions.json
think/topics/decisions.txt think/insights/decisions.txt
think/topics/documentation.json think/insights/documentation.json
think/topics/documentation.txt think/insights/documentation.txt
think/topics/files.json think/insights/files.json
think/topics/files.txt think/insights/files.txt
think/topics/flow.json think/insights/flow.json
think/topics/flow.txt think/insights/flow.txt
think/topics/followups.json think/insights/followups.json
think/topics/followups.txt think/insights/followups.txt
think/topics/knowledge_graph.json think/insights/knowledge_graph.json
think/topics/knowledge_graph.txt think/insights/knowledge_graph.txt
think/topics/media.json think/insights/media.json
think/topics/media.txt think/insights/media.txt
think/topics/meetings.json think/insights/meetings.json
think/topics/meetings.txt think/insights/meetings.txt
think/topics/messages.json think/insights/messages.json
think/topics/messages.txt think/insights/messages.txt
think/topics/opportunities.json think/insights/opportunities.json
think/topics/opportunities.txt think/insights/opportunities.txt
think/topics/research.json think/insights/research.json
think/topics/research.txt think/insights/research.txt
think/topics/schedule.json think/insights/schedule.json
think/topics/schedule.txt think/insights/schedule.txt
think/topics/timeline.json think/insights/timeline.json
think/topics/timeline.txt think/insights/timeline.txt
think/topics/tools.json think/insights/tools.json
think/topics/tools.txt think/insights/tools.txt
+17 -17
think/utils.py
··· 16 16 17 17 DATE_RE = re.compile(r"\d{8}") 18 18 19 - # Topic colors are now stored in each topic's JSON metadata file 19 + # Insight colors are now stored in each insight's JSON metadata file 20 20 21 21 AGENT_DIR = Path(__file__).parent.parent / "muse" / "agents" 22 22 ··· 476 476 return (args, extra) if parse_known else args 477 477 478 478 479 - def get_topics() -> dict[str, dict[str, object]]: 480 - """Return available topics with metadata. 479 + def get_insights() -> dict[str, dict[str, object]]: 480 + """Return available insights with metadata. 481 481 482 - Each key is the topic name. The value contains the ``path`` to the 482 + Each key is the insight name. The value contains the ``path`` to the 483 483 ``.txt`` file, the ``color`` from the metadata JSON, the file 484 484 ``mtime`` and any keys loaded from a matching ``.json`` metadata file. 485 485 """ 486 486 487 - topics_dir = Path(__file__).parent / "topics" 488 - topics: dict[str, dict[str, object]] = {} 489 - for txt_path in sorted(topics_dir.glob("*.txt")): 487 + insights_dir = Path(__file__).parent / "insights" 488 + insights: dict[str, dict[str, object]] = {} 489 + for txt_path in sorted(insights_dir.glob("*.txt")): 490 490 name = txt_path.stem 491 491 mtime = int(txt_path.stat().st_mtime) 492 492 info: dict[str, object] = { ··· 508 508 info["color"] = "#6c757d" # Default gray color 509 509 else: 510 510 info["color"] = "#6c757d" # Default gray color 511 - topics[name] = info 512 - return topics 511 + insights[name] = info 512 + return insights 513 513 514 514 515 515 def get_agent(persona: str = "default") -> dict: ··· 557 557 except Exception: 558 558 pass # Ignore if facets can't be loaded 559 559 560 - # Add topics to agent instructions 561 - topics = get_topics() 562 - if topics: 563 - topics_list = [] 564 - for topic_name, info in sorted(topics.items()): 560 + # Add insights to agent instructions 561 + insights = get_insights() 562 + if insights: 563 + insights_list = [] 564 + for insight_name, info in sorted(insights.items()): 565 565 desc = str(info.get("contains", "")).replace("\n", " ").strip() 566 566 if desc: 567 - topics_list.append(f"- `{topic_name}`: {desc}") 567 + insights_list.append(f"- `{insight_name}`: {desc}") 568 568 else: 569 - topics_list.append(f"- `{topic_name}`") 570 - extra_parts.append("## Available Topics\n" + "\n".join(topics_list)) 569 + insights_list.append(f"- `{insight_name}`") 570 + extra_parts.append("## Available Insights\n" + "\n".join(insights_list)) 571 571 572 572 # Add current date/time 573 573 now = datetime.now()