Skip to content

Commit 3c1cc34

Browse files
committed
fix: modify recent_activity args to be strings instead of enums
Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 81616ab commit 3c1cc34

File tree

3 files changed

+148
-66
lines changed

3 files changed

+148
-66
lines changed

src/basic_memory/mcp/tools/recent_activity.py

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Recent activity tool for Basic Memory MCP server."""
22

3-
from typing import Optional, List
3+
from typing import List, Union
44

55
from loguru import logger
66

@@ -14,7 +14,7 @@
1414

1515
@mcp.tool(
1616
description="""Get recent activity from across the knowledge base.
17-
17+
1818
Timeframe supports natural language formats like:
1919
- "2 days ago"
2020
- "last week"
@@ -25,21 +25,24 @@
2525
""",
2626
)
2727
async def recent_activity(
28-
type: Optional[List[SearchItemType]] = None,
29-
depth: Optional[int] = 1,
30-
timeframe: Optional[TimeFrame] = "7d",
28+
type: Union[str, List[str]] = "",
29+
depth: int = 1,
30+
timeframe: TimeFrame = "7d",
3131
page: int = 1,
3232
page_size: int = 10,
3333
max_related: int = 10,
3434
) -> GraphContext:
3535
"""Get recent activity across the knowledge base.
3636
3737
Args:
38-
type: Filter by content type(s). Valid options:
39-
- ["entity"] for knowledge entities
40-
- ["relation"] for connections between entities
41-
- ["observation"] for notes and observations
38+
type: Filter by content type(s). Can be a string or list of strings.
39+
Valid options:
40+
- "entity" or ["entity"] for knowledge entities
41+
- "relation" or ["relation"] for connections between entities
42+
- "observation" or ["observation"] for notes and observations
4243
Multiple types can be combined: ["entity", "relation"]
44+
Case-insensitive: "ENTITY" and "entity" are treated the same.
45+
Default is an empty string, which returns all types.
4346
depth: How many relation hops to traverse (1-3 recommended)
4447
timeframe: Time window to search. Supports natural language:
4548
- Relative: "2 days ago", "last week", "yesterday"
@@ -59,14 +62,17 @@ async def recent_activity(
5962
# Get all entities for the last 10 days (default)
6063
recent_activity()
6164
62-
# Get all entities from yesterday
65+
# Get all entities from yesterday (string format)
66+
recent_activity(type="entity", timeframe="yesterday")
67+
68+
# Get all entities from yesterday (list format)
6369
recent_activity(type=["entity"], timeframe="yesterday")
6470
6571
# Get recent relations and observations
6672
recent_activity(type=["relation", "observation"], timeframe="today")
6773
6874
# Look back further with more context
69-
recent_activity(type=["entity"], depth=2, timeframe="2 weeks ago")
75+
recent_activity(type="entity", depth=2, timeframe="2 weeks ago")
7076
7177
Notes:
7278
- Higher depth values (>3) may impact performance with large result sets
@@ -86,11 +92,27 @@ async def recent_activity(
8692
if timeframe:
8793
params["timeframe"] = timeframe # pyright: ignore
8894

89-
# send enum values if we have an enum, else send string value
95+
# Validate and convert type parameter
9096
if type:
91-
params["type"] = [ # pyright: ignore
92-
type.value if isinstance(type, SearchItemType) else type for type in type
93-
]
97+
# Convert single string to list
98+
if isinstance(type, str):
99+
type_list = [type]
100+
else:
101+
type_list = type
102+
103+
# Validate each type against SearchItemType enum
104+
validated_types = []
105+
for t in type_list:
106+
try:
107+
# Try to convert string to enum
108+
if isinstance(t, str):
109+
validated_types.append(SearchItemType(t.lower()))
110+
except ValueError:
111+
valid_types = [t.value for t in SearchItemType]
112+
raise ValueError(f"Invalid type: {t}. Valid types are: {valid_types}")
113+
114+
# Add validated types to params
115+
params["type"] = [t.value for t in validated_types] # pyright: ignore
94116

95117
response = await call_get(
96118
client,
Lines changed: 1 addition & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,9 @@
55

66
from mcp.server.fastmcp.exceptions import ToolError
77

8-
from basic_memory.mcp.tools import build_context, recent_activity
8+
from basic_memory.mcp.tools import build_context
99
from basic_memory.schemas.memory import (
1010
GraphContext,
11-
EntitySummary,
12-
ObservationSummary,
13-
RelationSummary,
1411
)
1512

1613

@@ -83,53 +80,6 @@ async def test_get_discussion_context_not_found(client):
8380
]
8481

8582

86-
@pytest.mark.asyncio
87-
async def test_recent_activity_timeframe_formats(client, test_graph):
88-
"""Test that recent_activity accepts various timeframe formats."""
89-
# Test each valid timeframe
90-
for timeframe in valid_timeframes:
91-
try:
92-
result = await recent_activity(
93-
type=["entity"], timeframe=timeframe, page=1, page_size=10, max_related=10
94-
)
95-
assert result is not None
96-
except Exception as e:
97-
pytest.fail(f"Failed with valid timeframe '{timeframe}': {str(e)}")
98-
99-
# Test invalid timeframes should raise ValidationError
100-
for timeframe in invalid_timeframes:
101-
with pytest.raises(ToolError):
102-
await recent_activity(timeframe=timeframe)
103-
104-
105-
@pytest.mark.asyncio
106-
async def test_recent_activity_type_filters(client, test_graph):
107-
"""Test that recent_activity correctly filters by types."""
108-
# Test single type
109-
result = await recent_activity(type=["entity"])
110-
assert result is not None
111-
assert all(isinstance(r, EntitySummary) for r in result.primary_results)
112-
113-
# Test multiple types
114-
result = await recent_activity(type=["entity", "observation"])
115-
assert result is not None
116-
assert all(
117-
isinstance(r, EntitySummary) or isinstance(r, ObservationSummary)
118-
for r in result.primary_results
119-
)
120-
121-
# Test all types
122-
result = await recent_activity(type=["entity", "observation", "relation"])
123-
assert result is not None
124-
# Results can be any type
125-
assert all(
126-
isinstance(r, EntitySummary)
127-
or isinstance(r, ObservationSummary)
128-
or isinstance(r, RelationSummary)
129-
for r in result.primary_results
130-
)
131-
132-
13383
@pytest.mark.asyncio
13484
async def test_build_context_timeframe_formats(client, test_graph):
13585
"""Test that build_context accepts various timeframe formats."""
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""Tests for discussion context MCP tool."""
2+
3+
import pytest
4+
5+
from mcp.server.fastmcp.exceptions import ToolError
6+
7+
from basic_memory.mcp.tools import recent_activity
8+
from basic_memory.schemas.memory import (
9+
EntitySummary,
10+
ObservationSummary,
11+
RelationSummary,
12+
)
13+
from basic_memory.schemas.search import SearchItemType
14+
15+
# Test data for different timeframe formats
16+
valid_timeframes = [
17+
"7d", # Standard format
18+
"yesterday", # Natural language
19+
"0d", # Zero duration
20+
]
21+
22+
invalid_timeframes = [
23+
"invalid", # Nonsense string
24+
"tomorrow", # Future date
25+
]
26+
27+
28+
@pytest.mark.asyncio
29+
async def test_recent_activity_timeframe_formats(client, test_graph):
30+
"""Test that recent_activity accepts various timeframe formats."""
31+
# Test each valid timeframe
32+
for timeframe in valid_timeframes:
33+
try:
34+
result = await recent_activity(
35+
type=["entity"], timeframe=timeframe, page=1, page_size=10, max_related=10
36+
)
37+
assert result is not None
38+
except Exception as e:
39+
pytest.fail(f"Failed with valid timeframe '{timeframe}': {str(e)}")
40+
41+
# Test invalid timeframes should raise ValidationError
42+
for timeframe in invalid_timeframes:
43+
with pytest.raises(ToolError):
44+
await recent_activity(timeframe=timeframe)
45+
46+
47+
@pytest.mark.asyncio
48+
async def test_recent_activity_type_filters(client, test_graph):
49+
"""Test that recent_activity correctly filters by types."""
50+
51+
# Test single string type
52+
result = await recent_activity(type=SearchItemType.ENTITY)
53+
assert result is not None
54+
assert all(isinstance(r, EntitySummary) for r in result.primary_results)
55+
56+
# Test single string type
57+
result = await recent_activity(type="entity")
58+
assert result is not None
59+
assert all(isinstance(r, EntitySummary) for r in result.primary_results)
60+
61+
# Test single type
62+
result = await recent_activity(type=["entity"])
63+
assert result is not None
64+
assert all(isinstance(r, EntitySummary) for r in result.primary_results)
65+
66+
# Test multiple types
67+
result = await recent_activity(type=["entity", "observation"])
68+
assert result is not None
69+
assert all(
70+
isinstance(r, EntitySummary) or isinstance(r, ObservationSummary)
71+
for r in result.primary_results
72+
)
73+
74+
# Test multiple types
75+
result = await recent_activity(type=[SearchItemType.ENTITY, SearchItemType.OBSERVATION])
76+
assert result is not None
77+
assert all(
78+
isinstance(r, EntitySummary) or isinstance(r, ObservationSummary)
79+
for r in result.primary_results
80+
)
81+
82+
# Test all types
83+
result = await recent_activity(type=["entity", "observation", "relation"])
84+
assert result is not None
85+
# Results can be any type
86+
assert all(
87+
isinstance(r, EntitySummary)
88+
or isinstance(r, ObservationSummary)
89+
or isinstance(r, RelationSummary)
90+
for r in result.primary_results
91+
)
92+
93+
94+
@pytest.mark.asyncio
95+
async def test_recent_activity_type_invalid(client, test_graph):
96+
"""Test that recent_activity correctly filters by types."""
97+
98+
# Test single invalid string type
99+
with pytest.raises(ValueError) as e:
100+
await recent_activity(type="note")
101+
assert (
102+
str(e.value) == "Invalid type: note. Valid types are: ['entity', 'observation', 'relation']"
103+
)
104+
105+
# Test invalid string array type
106+
with pytest.raises(ValueError) as e:
107+
await recent_activity(type=["note"])
108+
assert (
109+
str(e.value) == "Invalid type: note. Valid types are: ['entity', 'observation', 'relation']"
110+
)

0 commit comments

Comments
 (0)