|
5 | 5 | from loguru import logger |
6 | 6 | from fastmcp import Context |
7 | 7 |
|
8 | | -from basic_memory.mcp.async_client import client |
| 8 | +from basic_memory.mcp.async_client import get_client |
9 | 9 | from basic_memory.mcp.project_context import get_active_project, resolve_project_parameter |
10 | 10 | from basic_memory.mcp.server import mcp |
11 | 11 | from basic_memory.mcp.tools.utils import call_get |
@@ -98,162 +98,163 @@ async def recent_activity( |
98 | 98 | - For focused queries, consider using build_context with a specific URI |
99 | 99 | - Max timeframe is 1 year in the past |
100 | 100 | """ |
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 | + ) |
142 | 143 |
|
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 | + ) |
191 | 192 |
|
192 | | - # Generate guidance for the assistant |
193 | | - guidance_lines = ["\n" + "─" * 40] |
| 193 | + # Generate guidance for the assistant |
| 194 | + guidance_lines = ["\n" + "─" * 40] |
194 | 195 |
|
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: |
208 | 197 | guidance_lines.extend( |
209 | 198 | [ |
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?'", |
212 | 201 | ] |
213 | 202 | ) |
| 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 | + ) |
214 | 222 | else: |
| 223 | + # No recent activity |
215 | 224 | guidance_lines.extend( |
216 | 225 | [ |
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.", |
219 | 228 | ] |
220 | 229 | ) |
221 | | - else: |
222 | | - # No recent activity |
| 230 | + |
223 | 231 | 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."] |
228 | 233 | ) |
229 | 234 |
|
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) |
235 | 236 |
|
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) |
238 | 239 |
|
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 | + ) |
244 | 245 |
|
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 |
247 | 248 |
|
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()) |
254 | 255 |
|
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) |
257 | 258 |
|
258 | 259 |
|
259 | 260 | async def _get_project_activity( |
|
0 commit comments