Skip to content

Commit 208df1e

Browse files
phernandezclaude
andcommitted
refactor: convert recent_activity.py to context manager pattern
Converts recent_activity tool to use async with get_client() context manager: - Changed import from 'client' to 'get_client' - Wrapped function body at line 101 in async with block - Indented lines 102-257 inside context manager - Helper function _get_project_activity() already accepts client parameter - Tests: 4/4 passing - Coverage: 82% for recent_activity.py Tool provides comprehensive activity tracking: - Discovery mode for cross-project activity overview - Project-specific mode for detailed activity - Type filtering (entity, relation, observation) - Natural language timeframe support - Formatted output with guidance for AI assistants Total tools converted: 12/16 (Phase 0.3) Updated SPEC-16 Phase 0.3 checklist to mark recent_activity.py as completed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 567cd99 commit 208df1e

2 files changed

Lines changed: 139 additions & 138 deletions

File tree

specs/SPEC-16 MCP Cloud Service Consolidation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ Convert from `from async_client import client` to `async with get_client() as cl
456456
- [x] `tools/list_directory.py` (11/11 tests passing)
457457
- [x] `tools/move_note.py` (34/34 tests passing, 90% coverage)
458458
- [x] `tools/search.py` (16/16 tests passing, 96% coverage)
459-
- [ ] `tools/recent_activity.py`
459+
- [x] `tools/recent_activity.py` (4/4 tests passing, 82% coverage)
460460
- [ ] `tools/project_management.py`
461461
- [x] `tools/edit_note.py` (17/17 tests passing)
462462
- [x] `tools/canvas.py` (5/5 tests passing)

src/basic_memory/mcp/tools/recent_activity.py

Lines changed: 138 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from loguru import logger
66
from fastmcp import Context
77

8-
from basic_memory.mcp.async_client import client
8+
from basic_memory.mcp.async_client import get_client
99
from basic_memory.mcp.project_context import get_active_project, resolve_project_parameter
1010
from basic_memory.mcp.server import mcp
1111
from basic_memory.mcp.tools.utils import call_get
@@ -98,162 +98,163 @@ async def recent_activity(
9898
- For focused queries, consider using build_context with a specific URI
9999
- Max timeframe is 1 year in the past
100100
"""
101-
# Build common parameters for API calls
102-
params = {
103-
"page": 1,
104-
"page_size": 10,
105-
"max_related": 10,
106-
}
107-
if depth:
108-
params["depth"] = depth
109-
if timeframe:
110-
params["timeframe"] = timeframe # pyright: ignore
111-
112-
# Validate and convert type parameter
113-
if type:
114-
# Convert single string to list
115-
if isinstance(type, str):
116-
type_list = [type]
117-
else:
118-
type_list = type
119-
120-
# Validate each type against SearchItemType enum
121-
validated_types = []
122-
for t in type_list:
123-
try:
124-
# Try to convert string to enum
125-
if isinstance(t, str):
126-
validated_types.append(SearchItemType(t.lower()))
127-
except ValueError:
128-
valid_types = [t.value for t in SearchItemType]
129-
raise ValueError(f"Invalid type: {t}. Valid types are: {valid_types}")
130-
131-
# Add validated types to params
132-
params["type"] = [t.value for t in validated_types] # pyright: ignore
133-
134-
# Resolve project parameter using the three-tier hierarchy
135-
resolved_project = await resolve_project_parameter(project)
136-
137-
if resolved_project is None:
138-
# Discovery Mode: Get activity across all projects
139-
logger.info(
140-
f"Getting recent activity across all projects: type={type}, depth={depth}, timeframe={timeframe}"
141-
)
101+
async with get_client() as client:
102+
# Build common parameters for API calls
103+
params = {
104+
"page": 1,
105+
"page_size": 10,
106+
"max_related": 10,
107+
}
108+
if depth:
109+
params["depth"] = depth
110+
if timeframe:
111+
params["timeframe"] = timeframe # pyright: ignore
112+
113+
# Validate and convert type parameter
114+
if type:
115+
# Convert single string to list
116+
if isinstance(type, str):
117+
type_list = [type]
118+
else:
119+
type_list = type
120+
121+
# Validate each type against SearchItemType enum
122+
validated_types = []
123+
for t in type_list:
124+
try:
125+
# Try to convert string to enum
126+
if isinstance(t, str):
127+
validated_types.append(SearchItemType(t.lower()))
128+
except ValueError:
129+
valid_types = [t.value for t in SearchItemType]
130+
raise ValueError(f"Invalid type: {t}. Valid types are: {valid_types}")
131+
132+
# Add validated types to params
133+
params["type"] = [t.value for t in validated_types] # pyright: ignore
134+
135+
# Resolve project parameter using the three-tier hierarchy
136+
resolved_project = await resolve_project_parameter(project)
137+
138+
if resolved_project is None:
139+
# Discovery Mode: Get activity across all projects
140+
logger.info(
141+
f"Getting recent activity across all projects: type={type}, depth={depth}, timeframe={timeframe}"
142+
)
142143

143-
# Get list of all projects
144-
response = await call_get(client, "/projects/projects")
145-
project_list = ProjectList.model_validate(response.json())
146-
147-
projects_activity = {}
148-
total_items = 0
149-
total_entities = 0
150-
total_relations = 0
151-
total_observations = 0
152-
most_active_project = None
153-
most_active_count = 0
154-
active_projects = 0
155-
156-
# Query each project's activity
157-
for project_info in project_list.projects:
158-
project_activity = await _get_project_activity(client, project_info, params, depth)
159-
projects_activity[project_info.name] = project_activity
160-
161-
# Aggregate stats
162-
item_count = project_activity.item_count
163-
if item_count > 0:
164-
active_projects += 1
165-
total_items += item_count
166-
167-
# Count by type
168-
for result in project_activity.activity.results:
169-
if result.primary_result.type == "entity":
170-
total_entities += 1
171-
elif result.primary_result.type == "relation":
172-
total_relations += 1
173-
elif result.primary_result.type == "observation":
174-
total_observations += 1
175-
176-
# Track most active project
177-
if item_count > most_active_count:
178-
most_active_count = item_count
179-
most_active_project = project_info.name
180-
181-
# Build summary stats
182-
summary = ActivityStats(
183-
total_projects=len(project_list.projects),
184-
active_projects=active_projects,
185-
most_active_project=most_active_project,
186-
total_items=total_items,
187-
total_entities=total_entities,
188-
total_relations=total_relations,
189-
total_observations=total_observations,
190-
)
144+
# Get list of all projects
145+
response = await call_get(client, "/projects/projects")
146+
project_list = ProjectList.model_validate(response.json())
147+
148+
projects_activity = {}
149+
total_items = 0
150+
total_entities = 0
151+
total_relations = 0
152+
total_observations = 0
153+
most_active_project = None
154+
most_active_count = 0
155+
active_projects = 0
156+
157+
# Query each project's activity
158+
for project_info in project_list.projects:
159+
project_activity = await _get_project_activity(client, project_info, params, depth)
160+
projects_activity[project_info.name] = project_activity
161+
162+
# Aggregate stats
163+
item_count = project_activity.item_count
164+
if item_count > 0:
165+
active_projects += 1
166+
total_items += item_count
167+
168+
# Count by type
169+
for result in project_activity.activity.results:
170+
if result.primary_result.type == "entity":
171+
total_entities += 1
172+
elif result.primary_result.type == "relation":
173+
total_relations += 1
174+
elif result.primary_result.type == "observation":
175+
total_observations += 1
176+
177+
# Track most active project
178+
if item_count > most_active_count:
179+
most_active_count = item_count
180+
most_active_project = project_info.name
181+
182+
# Build summary stats
183+
summary = ActivityStats(
184+
total_projects=len(project_list.projects),
185+
active_projects=active_projects,
186+
most_active_project=most_active_project,
187+
total_items=total_items,
188+
total_entities=total_entities,
189+
total_relations=total_relations,
190+
total_observations=total_observations,
191+
)
191192

192-
# Generate guidance for the assistant
193-
guidance_lines = ["\n" + "─" * 40]
193+
# Generate guidance for the assistant
194+
guidance_lines = ["\n" + "─" * 40]
194195

195-
if most_active_project and most_active_count > 0:
196-
guidance_lines.extend(
197-
[
198-
f"Suggested project: '{most_active_project}' (most active with {most_active_count} items)",
199-
f"Ask user: 'Should I use {most_active_project} for this task, or would you prefer a different project?'",
200-
]
201-
)
202-
elif active_projects > 0:
203-
# Has activity but no clear most active project
204-
active_project_names = [
205-
name for name, activity in projects_activity.items() if activity.item_count > 0
206-
]
207-
if len(active_project_names) == 1:
196+
if most_active_project and most_active_count > 0:
208197
guidance_lines.extend(
209198
[
210-
f"Suggested project: '{active_project_names[0]}' (only active project)",
211-
f"Ask user: 'Should I use {active_project_names[0]} for this task?'",
199+
f"Suggested project: '{most_active_project}' (most active with {most_active_count} items)",
200+
f"Ask user: 'Should I use {most_active_project} for this task, or would you prefer a different project?'",
212201
]
213202
)
203+
elif active_projects > 0:
204+
# Has activity but no clear most active project
205+
active_project_names = [
206+
name for name, activity in projects_activity.items() if activity.item_count > 0
207+
]
208+
if len(active_project_names) == 1:
209+
guidance_lines.extend(
210+
[
211+
f"Suggested project: '{active_project_names[0]}' (only active project)",
212+
f"Ask user: 'Should I use {active_project_names[0]} for this task?'",
213+
]
214+
)
215+
else:
216+
guidance_lines.extend(
217+
[
218+
f"Multiple active projects found: {', '.join(active_project_names)}",
219+
"Ask user: 'Which project should I use for this task?'",
220+
]
221+
)
214222
else:
223+
# No recent activity
215224
guidance_lines.extend(
216225
[
217-
f"Multiple active projects found: {', '.join(active_project_names)}",
218-
"Ask user: 'Which project should I use for this task?'",
226+
"No recent activity found in any project.",
227+
"Consider: Ask which project to use or if they want to create a new one.",
219228
]
220229
)
221-
else:
222-
# No recent activity
230+
223231
guidance_lines.extend(
224-
[
225-
"No recent activity found in any project.",
226-
"Consider: Ask which project to use or if they want to create a new one.",
227-
]
232+
["", "Session reminder: Remember their project choice throughout this conversation."]
228233
)
229234

230-
guidance_lines.extend(
231-
["", "Session reminder: Remember their project choice throughout this conversation."]
232-
)
233-
234-
guidance = "\n".join(guidance_lines)
235+
guidance = "\n".join(guidance_lines)
235236

236-
# Format discovery mode output
237-
return _format_discovery_output(projects_activity, summary, timeframe, guidance)
237+
# Format discovery mode output
238+
return _format_discovery_output(projects_activity, summary, timeframe, guidance)
238239

239-
else:
240-
# Project-Specific Mode: Get activity for specific project
241-
logger.info(
242-
f"Getting recent activity from project {resolved_project}: type={type}, depth={depth}, timeframe={timeframe}"
243-
)
240+
else:
241+
# Project-Specific Mode: Get activity for specific project
242+
logger.info(
243+
f"Getting recent activity from project {resolved_project}: type={type}, depth={depth}, timeframe={timeframe}"
244+
)
244245

245-
active_project = await get_active_project(client, resolved_project, context)
246-
project_url = active_project.project_url
246+
active_project = await get_active_project(client, resolved_project, context)
247+
project_url = active_project.project_url
247248

248-
response = await call_get(
249-
client,
250-
f"{project_url}/memory/recent",
251-
params=params,
252-
)
253-
activity_data = GraphContext.model_validate(response.json())
249+
response = await call_get(
250+
client,
251+
f"{project_url}/memory/recent",
252+
params=params,
253+
)
254+
activity_data = GraphContext.model_validate(response.json())
254255

255-
# Format project-specific mode output
256-
return _format_project_output(resolved_project, activity_data, timeframe, type)
256+
# Format project-specific mode output
257+
return _format_project_output(resolved_project, activity_data, timeframe, type)
257258

258259

259260
async def _get_project_activity(

0 commit comments

Comments
 (0)