From 2b9207fdcab0c695f24a3b97b4aeccd6b1344d7e Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Mon, 16 Feb 2026 12:02:04 -0700 Subject: [PATCH] Rename "topic" to "agent" across the codebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "topic" field originally named the agent/generator that produced output. Rename to "agent" consistently: Python identifiers, SQLite FTS5 schema column, CLI flags (--topic/-t → --agent/-a), CLI commands (topics → agents), JSON event fields, stats cache keys, API response keys, UI constants and templates, documentation, and muse prompts. Key renames: - get_output_topic() → get_output_name() (returns filename stem) - meta["indexer"]["topic"] → meta["indexer"]["agent"] - write_events_jsonl(topic=) → write_events_jsonl(agent=) - TOPIC_ICONS/LABELS → AGENT_ICONS/LABELS - topic_counts/minutes/counts_by_day → agent_counts/minutes/counts_by_day Adds migration script (apps/search/maint/004_migrate_topic_to_agent.py) for existing event JSONL and stats cache files. Index requires rebuild via sol indexer --rebuild after migration. Audio enrichment "topics" field (subject-matter keywords) is unchanged. Natural-language uses of "topic" in muse prompts are preserved. Also fixes a flaky test in test_cortex_client.py where callosum_server fixture didn't match platform default path. --- apps/calendar/_day.html | 54 +++--- apps/calendar/routes.py | 8 +- apps/chat/workspace.html | 6 +- apps/entities/muse/entities.md | 6 +- apps/entities/muse/entity_assist.md | 10 +- apps/entities/muse/entity_describe.md | 2 +- apps/entities/muse/entity_observer.md | 6 +- apps/home/routes.py | 20 +-- apps/home/workspace.html | 22 +-- apps/search/maint/003_migrate_index_stream.py | 2 +- .../maint/004_migrate_topic_to_agent.py | 163 ++++++++++++++++++ apps/search/routes.py | 58 +++---- apps/search/tests/test_maint_migrate_index.py | 8 +- apps/search/workspace.html | 64 +++---- apps/stats/dashboard.js | 2 +- apps/todos/muse/daily.md | 6 +- apps/todos/muse/weekly.md | 10 +- apps/todos/todo.py | 4 +- docs/APPS.md | 10 +- docs/JOURNAL.md | 16 +- docs/PROMPT_TEMPLATES.md | 2 +- muse/anticipation.py | 8 +- muse/decisionalizer.md | 18 +- muse/default.md | 30 ++-- muse/facet_newsletter.md | 2 +- muse/help.md | 8 +- muse/joke_bot.md | 20 +-- muse/journal/SKILL.md | 18 +- muse/occurrence.py | 14 +- observe/hear.py | 4 +- observe/screen.py | 4 +- .../facets/personal/events/20240101.jsonl | 2 +- .../journal/facets/work/events/20240101.jsonl | 4 +- .../journal/facets/work/events/20240105.jsonl | 2 +- tests/test_call.py | 26 +-- tests/test_cluster.py | 2 +- tests/test_cortex_client.py | 8 +- tests/test_formatters.py | 62 +++---- tests/test_generators.py | 12 +- tests/test_journal_index.py | 44 ++--- tests/test_journal_stats.py | 4 +- tests/test_output_path.py | 12 +- tests/test_screen_formatter.py | 4 +- tests/test_template_substitution.py | 6 +- think/activities.py | 8 +- think/cluster.py | 4 +- think/dream.py | 2 +- think/entities/formatting.py | 6 +- think/events.py | 4 +- think/facets.py | 4 +- think/formatters.py | 28 +-- think/hooks.py | 12 +- think/indexer/cli.py | 21 +-- think/indexer/journal.py | 58 +++---- think/journal_stats.py | 48 +++--- think/markdown.py | 4 +- think/muse.py | 30 ++-- think/planner.md | 16 +- think/tools/call.py | 26 +-- think/tools/search.py | 22 +-- 60 files changed, 625 insertions(+), 461 deletions(-) create mode 100644 apps/search/maint/004_migrate_topic_to_agent.py diff --git a/apps/calendar/_day.html b/apps/calendar/_day.html index 1741244ea..459a8e4b0 100644 --- a/apps/calendar/_day.html +++ b/apps/calendar/_day.html @@ -500,7 +500,7 @@ const isPlanned = o.occurred === false; const cardClass = isPlanned ? 'all-day-card planned' : 'all-day-card'; const badge = isPlanned ? 'Planned' : ''; - const title = escapeHtml(o.title || o.summary || o.topic || 'Event'); + const title = escapeHtml(o.title || o.summary || o.agent || 'Event'); html += `
`; html += `${title}${badge}
`; }); @@ -556,7 +556,7 @@ // Process event blocks for column layout const processedEvents = []; - const topics = []; + const agents = []; timedEvents.forEach(o => { const idx = list.indexOf(o); @@ -568,54 +568,54 @@ e = Math.min(e, endHour * 60); if (e <= s) return; - const topic = o.topic || 'other'; - if (!topics.includes(topic)) topics.push(topic); + const agent = o.agent || 'other'; + if (!agents.includes(agent)) agents.push(agent); processedEvents.push({ - startMinutes: s, endMinutes: e, topic: topic, - title: escapeHtml(o.title || o.summary || topic), + startMinutes: s, endMinutes: e, agent: agent, + title: escapeHtml(o.title || o.summary || agent), color: o.color || '#6c757d', index: idx, isPlanned: o.occurred === false }); }); - const eventsByTopic = {}; - topics.forEach(topic => { - eventsByTopic[topic] = processedEvents.filter(e => e.topic === topic); - eventsByTopic[topic].sort((a, b) => a.startMinutes - b.startMinutes); + const eventsByAgent = {}; + agents.forEach(agent => { + eventsByAgent[agent] = processedEvents.filter(e => e.agent === agent); + eventsByAgent[agent].sort((a, b) => a.startMinutes - b.startMinutes); }); - const topicColumnCounts = {}; - topics.forEach(topic => { - const topicEvents = eventsByTopic[topic]; - const topicColumns = []; - topicEvents.forEach(event => { + const agentColumnCounts = {}; + agents.forEach(agent => { + const agentEvents = eventsByAgent[agent]; + const agentColumns = []; + agentEvents.forEach(event => { let columnIndex = 0; - while (columnIndex < topicColumns.length && - topicColumns[columnIndex].some(existing => + while (columnIndex < agentColumns.length && + agentColumns[columnIndex].some(existing => event.startMinutes < existing.endMinutes && event.endMinutes > existing.startMinutes)) { columnIndex++; } - if (columnIndex === topicColumns.length) topicColumns.push([]); - topicColumns[columnIndex].push(event); + if (columnIndex === agentColumns.length) agentColumns.push([]); + agentColumns[columnIndex].push(event); event.slugColumn = columnIndex; }); - topicColumnCounts[topic] = topicColumns.length; + agentColumnCounts[agent] = agentColumns.length; }); let globalColumnOffset = 0; - const topicColumnOffsets = {}; - topics.forEach(topic => { - topicColumnOffsets[topic] = globalColumnOffset; - globalColumnOffset += topicColumnCounts[topic]; + const agentColumnOffsets = {}; + agents.forEach(agent => { + agentColumnOffsets[agent] = globalColumnOffset; + globalColumnOffset += agentColumnCounts[agent]; }); const totalColumns = globalColumnOffset; const columnWidth = totalColumns > 0 ? 100 / totalColumns : 100; processedEvents.forEach(event => { - event.column = topicColumnOffsets[event.topic] + event.slugColumn; + event.column = agentColumnOffsets[event.agent] + event.slugColumn; }); processedEvents.forEach(event => { @@ -671,8 +671,8 @@ if (!occ) return; const body = document.getElementById('modalBody'); const isPlanned = occ.occurred === false; - const topic = occ.topic ? occ.topic.charAt(0).toUpperCase() + occ.topic.slice(1) : ''; - const titleText = `${topic ? topic + ': ' : ''}${occ.title || ''}`.trim(); + const agent = occ.agent ? occ.agent.charAt(0).toUpperCase() + occ.agent.slice(1) : ''; + const titleText = `${agent ? agent + ': ' : ''}${occ.title || ''}`.trim(); const subtitle = occ.subject || occ.summary || ''; let html = ''; diff --git a/apps/calendar/routes.py b/apps/calendar/routes.py index 7866e57ea..351addc76 100644 --- a/apps/calendar/routes.py +++ b/apps/calendar/routes.py @@ -66,8 +66,8 @@ def calendar_day_events(day: str) -> Any: # Transform events into timeline format result = [] for event in raw_events: - topic = event.get("topic", "other") - topic_color = generators.get(topic, {}).get("color", "#6c757d") + agent = event.get("agent", "other") + agent_color = generators.get(agent, {}).get("color", "#6c757d") formatted = { "title": event.get("title", ""), @@ -75,8 +75,8 @@ def calendar_day_events(day: str) -> Any: "subject": event.get("subject", ""), "details": event.get("details", event.get("description", "")), "participants": event.get("participants", []), - "topic": topic, - "color": topic_color, + "agent": agent, + "color": agent_color, "facet": event.get("facet", ""), "occurred": event.get("occurred", True), "source": event.get("source", ""), diff --git a/apps/chat/workspace.html b/apps/chat/workspace.html index ddc3c6526..91747ceaf 100644 --- a/apps/chat/workspace.html +++ b/apps/chat/workspace.html @@ -1525,7 +1525,7 @@ function cleanupOldPlacards() { } } -// Format tool args into display-friendly params (query, facet, day, topic, etc.) +// Format tool args into display-friendly params (query, facet, day, agent, etc.) function formatToolParams(args) { if (!args) return []; const params = []; @@ -1545,8 +1545,8 @@ function formatToolParams(args) { if (args.day_to) { params.push({label: 'to', value: formatDay(args.day_to)}); } - if (args.topic) { - params.push({label: 'topic', value: args.topic}); + if (args.agent) { + params.push({label: 'agent', value: args.agent}); } return params; diff --git a/apps/entities/muse/entities.md b/apps/entities/muse/entities.md index 40c20ea5e..2d1ef7dc2 100644 --- a/apps/entities/muse/entities.md +++ b/apps/entities/muse/entities.md @@ -54,8 +54,8 @@ SOL_DAY and SOL_FACET are set in your environment. Commands default to the curre - The `entity` parameter can be entity_id, full name, or alias - if it matches an attached entity, uses that entity's canonical name Discovery tools (note facet scoping): -- `sol call journal read TOPIC` - read full agent output (e.g., knowledge_graph, followups) - GLOBAL -- `sol call journal search QUERY -d DAY -t TOPIC -f FACET -n LIMIT` - unified search across all journal content - facet-scopable +- `sol call journal read AGENT` - read full agent output (e.g., knowledge_graph, followups) - GLOBAL +- `sol call journal search QUERY -d DAY -a AGENT -f FACET -n LIMIT` - unified search across all journal content - facet-scopable - `sol call journal events [-f FACET]` - get structured events - **FACET-SCOPED when facet parameter provided** **IMPORTANT**: When using GLOBAL search tools, you must actively filter results to find ONLY entities that participated in THIS facet's activities. Seeing an entity in a global search result does NOT automatically mean it belongs to this facet. @@ -91,7 +91,7 @@ Seeing an entity in a global search does NOT mean it belongs to this facet. **Priority 3: Insights and Transcripts** (use sparingly with extreme filtering) - `sol call journal search "people OR companies OR organizations OR projects OR entities" -d $day_YYYYMMDD -n 10` - GLOBAL, may include other facets -- `sol call journal search "[entity names]" -d $day_YYYYMMDD -t audio` - GLOBAL, must validate facet relevance +- `sol call journal search "[entity names]" -d $day_YYYYMMDD -a audio` - GLOBAL, must validate facet relevance - For each result: verify the entity was actively involved in THIS facet's context, not just mentioned **Red flag check**: If you're finding many entities but facet events were empty, you're likely detecting entities from other facets. Stop and reassess. diff --git a/apps/entities/muse/entity_assist.md b/apps/entities/muse/entity_assist.md index 66ee690d8..233b2287d 100644 --- a/apps/entities/muse/entity_assist.md +++ b/apps/entities/muse/entity_assist.md @@ -39,10 +39,10 @@ Entity operations: Research tools (use sparingly, be quick): - `sol call journal search QUERY -n 3` - find entity mentions in all journal content -- `sol call journal search QUERY -t audio -n 3` - find entity in transcripts -- `sol call journal search QUERY -t news -n 3` - find entity in facet news +- `sol call journal search QUERY -a audio -n 3` - find entity in transcripts +- `sol call journal search QUERY -a news -n 3` - find entity in facet news - `sol call journal events -d DAY` - find entity in events for a specific day -- `sol call journal read TOPIC` - read full agent output when snippet search is insufficient +- `sol call journal read AGENT` - read full agent output when snippet search is insufficient ## Quick Addition Process @@ -72,8 +72,8 @@ If entity already exists (check by name or entity_id), consider if the request i ### Step 3: Quick Research Execute a few targeted searches based on type: -- **Person**: `sol call journal search "{name}" -n 3` or `sol call journal search "{name}" -t event -n 3` -- **Company**: `sol call journal search "{name}" -t news -n 3` or `sol call journal search "{name}" -n 3` +- **Person**: `sol call journal search "{name}" -n 3` or `sol call journal search "{name}" -a event -n 3` +- **Company**: `sol call journal search "{name}" -a news -n 3` or `sol call journal search "{name}" -n 3` - **Project**: `sol call journal search "{name}" -n 3` - **Tool**: `sol call journal search "{name}" -n 3` diff --git a/apps/entities/muse/entity_describe.md b/apps/entities/muse/entity_describe.md index 6b74c70d5..f7b9b2cbc 100644 --- a/apps/entities/muse/entity_describe.md +++ b/apps/entities/muse/entity_describe.md @@ -25,7 +25,7 @@ You receive: Use these `sol call` commands for quick research (be efficient, 2-3 calls max): - `sol call journal search QUERY -f FACET -n LIMIT` - find mentions in journal content, scoped to facet -- `sol call journal search QUERY -t audio -n LIMIT` - find mentions in transcripts +- `sol call journal search QUERY -a audio -n LIMIT` - find mentions in transcripts ## Process diff --git a/apps/entities/muse/entity_observer.md b/apps/entities/muse/entity_observer.md index 324796036..10de64332 100644 --- a/apps/entities/muse/entity_observer.md +++ b/apps/entities/muse/entity_observer.md @@ -34,8 +34,8 @@ SOL_DAY and SOL_FACET are set in your environment. Commands default to the curre - Use entity_id from `sol call entities observations` response for consistency Discovery tools: -- `sol call journal read TOPIC` - read full agent output (e.g., knowledge_graph, followups) -- `sol call journal search QUERY -d DAY -t TOPIC -f FACET -n LIMIT` - unified search across journal content +- `sol call journal read AGENT` - read full agent output (e.g., knowledge_graph, followups) +- `sol call journal search QUERY -d DAY -a AGENT -f FACET -n LIMIT` - unified search across journal content - `sol call journal events [-f FACET]` - get structured events ## What Makes a Good Observation @@ -75,7 +75,7 @@ For each attached entity in this facet: The response includes the resolved entity with its `id` field. 2. **Mine recent content** for factoids about this entity: - - Search transcripts: `sol call journal search "{name}" -t audio -n 5` + - Search transcripts: `sol call journal search "{name}" -a audio -n 5` - Check knowledge graph: `sol call journal read knowledge_graph` - Search insights: `sol call journal search "{name}" -n 5` diff --git a/apps/home/routes.py b/apps/home/routes.py index 88ed2bcf7..58a59c8c5 100644 --- a/apps/home/routes.py +++ b/apps/home/routes.py @@ -95,14 +95,14 @@ def _get_day_summary(day: str) -> dict[str, Any]: }, } - # Events by topic (aggregated across facets for totals) - events_by_topic: dict[str, int] = defaultdict(int) + # Events by agent (aggregated across facets for totals) + events_by_agent: dict[str, int] = defaultdict(int) for facet_name in facet_names: facet_config = facet_map.get(facet_name, {}) facet_data: dict[str, Any] = { "todos": [], - "events_by_topic": {}, + "events_by_agent": {}, "entities": [], "news_content": None, } @@ -127,11 +127,11 @@ def _get_day_summary(day: str) -> dict[str, Any]: events = get_events(day, facet=facet_name) facet_events: dict[str, int] = defaultdict(int) for event in events: - topic = event.get("topic", "other") - facet_events[topic] += 1 - events_by_topic[topic] += 1 + agent = event.get("agent", "other") + facet_events[agent] += 1 + events_by_agent[agent] += 1 result["totals"]["events"] += 1 - facet_data["events_by_topic"] = dict(facet_events) + facet_data["events_by_agent"] = dict(facet_events) except Exception: pass @@ -160,7 +160,7 @@ def _get_day_summary(day: str) -> dict[str, Any]: # Determine if facet has any activity for this day has_activity = bool( facet_data["todos"] - or facet_data["events_by_topic"] + or facet_data["events_by_agent"] or facet_data["entities"] ) @@ -172,8 +172,8 @@ def _get_day_summary(day: str) -> dict[str, Any]: "has_activity": has_activity, } - # Add aggregated events by topic to totals - result["totals"]["events_by_topic"] = dict(events_by_topic) + # Add aggregated events by agent to totals + result["totals"]["events_by_agent"] = dict(events_by_agent) # Load recent entities (attached entities with last_seen in lookback window) recent_entities = _get_recent_entities(day, facet_names) diff --git a/apps/home/workspace.html b/apps/home/workspace.html index 9267dedd5..1bef34b0c 100644 --- a/apps/home/workspace.html +++ b/apps/home/workspace.html @@ -399,36 +399,36 @@ function renderEvents(data, facet) { const container = document.getElementById('events-content'); - let eventsByTopic; + let eventsByAgent; if (facet) { const facetData = data.facets[facet]; - eventsByTopic = facetData ? facetData.events_by_topic : {}; + eventsByAgent = facetData ? facetData.events_by_agent : {}; } else { - eventsByTopic = data.totals.events_by_topic || {}; + eventsByAgent = data.totals.events_by_agent || {}; } - const topics = Object.keys(eventsByTopic); - if (topics.length === 0) { + const agents = Object.keys(eventsByAgent); + if (agents.length === 0) { container.innerHTML = '
No events
'; return; } // Sort by count descending - topics.sort((a, b) => eventsByTopic[b] - eventsByTopic[a]); + agents.sort((a, b) => eventsByAgent[b] - eventsByAgent[a]); // Find max count for scaling - const maxCount = Math.max(...topics.map(t => eventsByTopic[t])); + const maxCount = Math.max(...agents.map(t => eventsByAgent[t])); container.innerHTML = `
- ${topics.map(topic => { - const count = eventsByTopic[topic]; + ${agents.map(agent => { + const count = eventsByAgent[agent]; const percent = maxCount > 0 ? (count / maxCount) * 100 : 0; return `
- ${topic} + ${agent}
`; }).join('')} @@ -543,7 +543,7 @@ const facetData = data.facets[name] || {}; const todos = facetData.todos || []; const pendingTodos = todos.filter(t => !t.completed).length; - const eventCount = Object.values(facetData.events_by_topic || {}).reduce((a, b) => a + b, 0); + const eventCount = Object.values(facetData.events_by_agent || {}).reduce((a, b) => a + b, 0); // For recent days, use recent entities count if no detected entities const detectedCount = (facetData.entities || []).length; const recentCount = recentDay ? (data.recent_entities?.[name] || []).length : 0; diff --git a/apps/search/maint/003_migrate_index_stream.py b/apps/search/maint/003_migrate_index_stream.py index 6c94b3439..63a95bf8b 100644 --- a/apps/search/maint/003_migrate_index_stream.py +++ b/apps/search/maint/003_migrate_index_stream.py @@ -20,7 +20,7 @@ from think.utils import get_journal, setup_cli logger = logging.getLogger(__name__) -EXPECTED_COLUMNS = {"content", "path", "day", "facet", "topic", "stream", "idx"} +EXPECTED_COLUMNS = {"content", "path", "day", "facet", "agent", "stream", "idx"} def _get_db_path(journal: str) -> str: diff --git a/apps/search/maint/004_migrate_topic_to_agent.py b/apps/search/maint/004_migrate_topic_to_agent.py new file mode 100644 index 000000000..4a108beed --- /dev/null +++ b/apps/search/maint/004_migrate_topic_to_agent.py @@ -0,0 +1,163 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +"""Migrate journal event/stats keys from topic naming to agent naming.""" + +from __future__ import annotations + +import argparse +import json +from dataclasses import dataclass +from pathlib import Path + +from think.utils import get_journal, setup_cli + +STATS_KEY_RENAMES = { + "topic_data": "agent_data", + "topic_counts": "agent_counts", + "topic_minutes": "agent_minutes", + "topic_counts_by_day": "agent_counts_by_day", +} + + +@dataclass +class MigrationCounters: + """Mutable counters for migration operations.""" + + events_modified: int = 0 + events_skipped: int = 0 + events_errors: int = 0 + stats_modified: int = 0 + stats_skipped: int = 0 + stats_errors: int = 0 + event_records_renamed: int = 0 + stats_keys_renamed: int = 0 + + +def _migrate_event_file( + file_path: Path, *, dry_run: bool, counters: MigrationCounters +) -> None: + """Migrate one events JSONL file from topic key to agent key.""" + try: + lines = file_path.read_text(encoding="utf-8").splitlines() + out_lines: list[str] = [] + modified = False + + for line in lines: + stripped = line.strip() + if not stripped: + continue + + row = json.loads(stripped) + if isinstance(row, dict) and "topic" in row: + if "agent" not in row: + row["agent"] = row["topic"] + del row["topic"] + modified = True + counters.event_records_renamed += 1 + + out_lines.append(json.dumps(row, ensure_ascii=False)) + + if not modified: + counters.events_skipped += 1 + return + + counters.events_modified += 1 + if dry_run: + print(f"[DRY-RUN] update {file_path}") + return + + payload = "\n".join(out_lines) + if payload: + payload += "\n" + file_path.write_text(payload, encoding="utf-8") + except Exception as exc: + counters.events_errors += 1 + print(f"[ERROR] events migration failed for {file_path}: {exc}") + + +def _migrate_stats_file( + file_path: Path, *, dry_run: bool, counters: MigrationCounters +) -> None: + """Migrate one stats.json file from topic keys to agent keys.""" + try: + data = json.loads(file_path.read_text(encoding="utf-8")) + if not isinstance(data, dict): + counters.stats_skipped += 1 + return + + modified = False + for old_key, new_key in STATS_KEY_RENAMES.items(): + if old_key not in data: + continue + + if new_key not in data: + data[new_key] = data[old_key] + del data[old_key] + modified = True + counters.stats_keys_renamed += 1 + + if not modified: + counters.stats_skipped += 1 + return + + counters.stats_modified += 1 + if dry_run: + print(f"[DRY-RUN] update {file_path}") + return + + file_path.write_text( + json.dumps(data, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + except Exception as exc: + counters.stats_errors += 1 + print(f"[ERROR] stats migration failed for {file_path}: {exc}") + + +def migrate_topic_to_agent(*, journal: str, dry_run: bool) -> MigrationCounters: + """Run topic->agent key migration for events JSONL and stats JSON files.""" + counters = MigrationCounters() + journal_path = Path(journal) + + event_files = sorted(journal_path.glob("facets/*/events/*.jsonl")) + stats_files = sorted(journal_path.rglob("stats.json")) + + for file_path in event_files: + _migrate_event_file(file_path, dry_run=dry_run, counters=counters) + + for file_path in stats_files: + _migrate_stats_file(file_path, dry_run=dry_run, counters=counters) + + print("Migration complete") + print(f" events modified: {counters.events_modified}") + print(f" events skipped: {counters.events_skipped}") + print(f" events errors: {counters.events_errors}") + print(f" stats modified: {counters.stats_modified}") + print(f" stats skipped: {counters.stats_skipped}") + print(f" stats errors: {counters.stats_errors}") + print(f" event rows renamed: {counters.event_records_renamed}") + print(f" stats keys renamed: {counters.stats_keys_renamed}") + print("After migration, run: sol indexer --rebuild") + + return counters + + +def main() -> None: + """CLI entry point for topic->agent migration.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--dry-run", + action="store_true", + help="Preview file changes without writing them.", + ) + args = setup_cli(parser) + + if args.dry_run: + print("[DRY-RUN] No files will be modified.") + + migrate_topic_to_agent(journal=get_journal(), dry_run=args.dry_run) + + +if __name__ == "__main__": + main() diff --git a/apps/search/routes.py b/apps/search/routes.py index 539a1fdf6..9e592221e 100644 --- a/apps/search/routes.py +++ b/apps/search/routes.py @@ -18,8 +18,8 @@ search_bp = Blueprint( url_prefix="/app/search", ) -# Topic icons for display -TOPIC_ICONS = { +# Agent icons for display +AGENT_ICONS = { "flow": "📝", "knowledge_graph": "🗺️", "meetings": "📅", @@ -34,8 +34,8 @@ TOPIC_ICONS = { "import": "📥", } -# Topic display names -TOPIC_LABELS = { +# Agent display names +AGENT_LABELS = { "flow": "Flow", "knowledge_graph": "Knowledge Graph", "meetings": "Meetings", @@ -56,9 +56,9 @@ def _parse_facet_filter() -> str | None: return request.args.get("facet", "").strip() or None -def _parse_topic_filter() -> str | None: - """Parse single topic filter from request args.""" - return request.args.get("topic", "").strip() or None +def _parse_agent_filter() -> str | None: + """Parse single agent filter from request args.""" + return request.args.get("agent", "").strip() or None def _parse_stream_filter() -> str | None: @@ -85,7 +85,7 @@ def _highlight_query_terms(text: str, query: str) -> str: def _format_result(result: dict, query: str, facets_map: dict) -> dict: """Format a search result for API response.""" meta = result.get("metadata", {}) - topic = meta.get("topic", "") + agent = meta.get("agent", "") text = result.get("text", "") facet_name = meta.get("facet", "") @@ -105,9 +105,9 @@ def _format_result(result: dict, query: str, facets_map: dict) -> dict: return { "id": result.get("id", ""), "day": meta.get("day", ""), - "topic": topic, - "topic_icon": TOPIC_ICONS.get(topic, "📄"), - "topic_label": TOPIC_LABELS.get(topic, topic.title()), + "agent": agent, + "agent_icon": AGENT_ICONS.get(agent, "📄"), + "agent_label": AGENT_LABELS.get(agent, agent.title()), "facet": facet_name, "facet_title": facet_info.get("title", facet_name), "facet_color": facet_info.get("color", ""), @@ -129,14 +129,14 @@ def search_journal_api() -> Any: limit: Max results per day (default 5) offset: Day offset for pagination (default 0) facet: Filter by facet name (optional, empty string for no-facet items) - topic: Filter by single topic (optional) + agent: Filter by single agent (optional) Returns: JSON with: - total: Total match count - days: List of day groups, each with date info and results - facets: List of facets with counts for filter sidebar - - topics: List of topics with counts for filter sidebar + - agents: List of agents with counts for filter sidebar """ query = request.args.get("q", "").strip() @@ -144,7 +144,7 @@ def search_journal_api() -> Any: results_per_day = int(request.args.get("limit", 5)) day_offset = int(request.args.get("offset", 0)) facet_filter = _parse_facet_filter() - topic_filter = _parse_topic_filter() + agent_filter = _parse_agent_filter() stream_filter = _parse_stream_filter() # Load facet metadata for enriching results @@ -154,11 +154,11 @@ def search_journal_api() -> Any: # First get unfiltered counts for sidebar display base_counts = search_counts(query, stream=stream_filter) facet_counts = dict(base_counts["facets"]) - topic_counts = dict(base_counts["topics"]) + agent_counts = dict(base_counts["agents"]) # Get filtered counts for results filtered_counts = search_counts( - query, facet=facet_filter, topic=topic_filter, stream=stream_filter + query, facet=facet_filter, agent=agent_filter, stream=stream_filter ) day_counts = dict(filtered_counts["days"]) @@ -177,7 +177,7 @@ def search_journal_api() -> Any: offset=0, day=day, facet=facet_filter, - topic=topic_filter, + agent=agent_filter, stream=stream_filter, ) total_in_day = day_counts.get(day, 0) @@ -212,14 +212,14 @@ def search_journal_api() -> Any: # Sort by count descending facets_list.sort(key=lambda x: x["count"], reverse=True) - # Build topic list for sidebar (unfiltered counts for discovery) - topics_list = [] - for topic, count in sorted(topic_counts.items(), key=lambda x: -x[1]): - topics_list.append( + # Build agent list for sidebar (unfiltered counts for discovery) + agents_list = [] + for agent, count in sorted(agent_counts.items(), key=lambda x: -x[1]): + agents_list.append( { - "name": topic, - "label": TOPIC_LABELS.get(topic, topic.title()), - "icon": TOPIC_ICONS.get(topic, "📄"), + "name": agent, + "label": AGENT_LABELS.get(agent, agent.title()), + "icon": AGENT_ICONS.get(agent, "📄"), "count": count, } ) @@ -231,7 +231,7 @@ def search_journal_api() -> Any: "showing_days": len(days_response), "days": days_response, "facets": facets_list, - "topics": topics_list, + "agents": agents_list, } ) @@ -246,7 +246,7 @@ def day_results_api() -> Any: offset: Result offset within the day (default 0) limit: Max results (default 20) facet: Facet filter (optional) - topic: Single topic filter (optional) + agent: Single agent filter (optional) """ query = request.args.get("q", "").strip() day = request.args.get("day", "").strip() @@ -256,14 +256,14 @@ def day_results_api() -> Any: offset = int(request.args.get("offset", 0)) limit = int(request.args.get("limit", 20)) facet_filter = _parse_facet_filter() - topic_filter = _parse_topic_filter() + agent_filter = _parse_agent_filter() stream_filter = _parse_stream_filter() facets_map = get_facets() # Get total count for this day with filters counts = search_counts( - query, day=day, facet=facet_filter, topic=topic_filter, stream=stream_filter + query, day=day, facet=facet_filter, agent=agent_filter, stream=stream_filter ) total_in_day = counts["total"] @@ -274,7 +274,7 @@ def day_results_api() -> Any: offset=offset, day=day, facet=facet_filter, - topic=topic_filter, + agent=agent_filter, stream=stream_filter, ) diff --git a/apps/search/tests/test_maint_migrate_index.py b/apps/search/tests/test_maint_migrate_index.py index 456b4b2ed..d879f0604 100644 --- a/apps/search/tests/test_maint_migrate_index.py +++ b/apps/search/tests/test_maint_migrate_index.py @@ -30,12 +30,12 @@ def _create_old_schema(tmp_path): path UNINDEXED, day UNINDEXED, facet UNINDEXED, - topic UNINDEXED, + agent UNINDEXED, idx UNINDEXED ) """) conn.execute( - "INSERT INTO chunks(content, path, day, facet, topic, idx) " + "INSERT INTO chunks(content, path, day, facet, agent, idx) " "VALUES ('test', 'test.md', '20240101', 'work', 'flow', 0)" ) conn.commit() @@ -56,7 +56,7 @@ def _create_current_schema(tmp_path): path UNINDEXED, day UNINDEXED, facet UNINDEXED, - topic UNINDEXED, + agent UNINDEXED, stream UNINDEXED, idx UNINDEXED ) @@ -80,7 +80,7 @@ def test_migrate_old_schema_rebuilds(tmp_path): # Verify new schema accepts stream column conn = sqlite3.connect(db_path) conn.execute( - "INSERT INTO chunks(content, path, day, facet, topic, stream, idx) " + "INSERT INTO chunks(content, path, day, facet, agent, stream, idx) " "VALUES ('test', 'test.md', '20240101', 'work', 'flow', 'archon', 0)" ) row = conn.execute("SELECT stream FROM chunks").fetchone() diff --git a/apps/search/workspace.html b/apps/search/workspace.html index d4b3cf1d7..7a4cdbda1 100644 --- a/apps/search/workspace.html +++ b/apps/search/workspace.html @@ -142,7 +142,7 @@ flex-wrap: wrap; } -.result-topic { +.result-agent { display: flex; align-items: center; gap: 0.25rem; @@ -151,7 +151,7 @@ color: #374151; } -.result-topic-icon { +.result-agent-icon { font-size: 0.9rem; } @@ -289,9 +289,9 @@
-
-

Topics

-
+
+

Agents

+
@@ -324,7 +324,7 @@ // State let currentQuery = ''; let currentFacet = ''; - let currentTopic = ''; + let currentAgent = ''; let dayOffset = 0; // DOM elements @@ -333,7 +333,7 @@ const searchEmpty = document.getElementById('search-empty'); const loadMoreDaysBtn = document.getElementById('load-more-days'); const facetList = document.getElementById('facet-list'); - const topicList = document.getElementById('topic-list'); + const agentList = document.getElementById('agent-list'); // Initialize loadFromHash(); @@ -353,12 +353,12 @@ const hashParams = new URLSearchParams(window.location.hash.slice(1)); const q = hashParams.get('q') || ''; const f = hashParams.get('facet') || ''; - const t = hashParams.get('topic') || ''; + const t = hashParams.get('agent') || ''; - if (q !== currentQuery || f !== currentFacet || t !== currentTopic) { + if (q !== currentQuery || f !== currentFacet || t !== currentAgent) { currentQuery = q; currentFacet = f; - currentTopic = t; + currentAgent = t; // Notify app bar of query change window.dispatchEvent(new CustomEvent('search.queryUpdate', { @@ -377,7 +377,7 @@ const params = new URLSearchParams(); if (currentQuery) params.set('q', currentQuery); if (currentFacet) params.set('facet', currentFacet); - if (currentTopic) params.set('topic', currentTopic); + if (currentAgent) params.set('agent', currentAgent); window.location.hash = params.toString(); } @@ -403,14 +403,14 @@ url.searchParams.set('q', currentQuery); url.searchParams.set('offset', dayOffset); if (currentFacet) url.searchParams.set('facet', currentFacet); - if (currentTopic) url.searchParams.set('topic', currentTopic); + if (currentAgent) url.searchParams.set('agent', currentAgent); fetch(url) .then(r => r.json()) .then(data => { if (reset) { resultsContainer.innerHTML = ''; - renderFilters(data.facets, data.topics); + renderFilters(data.facets, data.agents); } renderResults(data, reset); @@ -430,7 +430,7 @@ loadMoreDaysBtn.style.display = 'none'; } - function renderFilters(facets, topics) { + function renderFilters(facets, agents) { // Render facet filters let facetHtml = `