Skip to content

Commit 5efecc4

Browse files
authored
feat: event tag search (#131)
* support event tag search * update docs
1 parent dc9657e commit 5efecc4

File tree

9 files changed

+294
-4
lines changed

9 files changed

+294
-4
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
title: 'Search Events By Tags'
3+
openapi: get /api/v1/users/event_tags/search/{user_id}
4+
---
5+
Search events by tags.
6+

docs/site/docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
"api-reference/events/get_events",
151151
"api-reference/events/search_events",
152152
"api-reference/events/search_event_gists",
153+
"api-reference/events/search_events_by_tags",
153154
"api-reference/events/update_event",
154155
"api-reference/events/delete_event"
155156
]

docs/site/features/event/event_tag.mdx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,18 @@ for event in events:
3939

4040
## Searching Events by Tag
4141

42-
You can also search for events that have specific tags applied.
42+
You can search for events that have specific tags applied using the `search_event_by_tags` method.
4343

4444
```python
4545
from memobase import MemoBaseClient
4646
4747
client = MemoBaseClient(project_url='YOUR_PROJECT_URL', api_key='YOUR_API_KEY')
4848
user = client.get_user('some_user_id')
4949
50-
# Find all events tagged with 'emotion'
51-
events = user.search_event(tags=["emotion"])
50+
# Find events with specific tags (AND condition)
51+
events = user.search_event_by_tags(tags=["emotion", "romance"])
5252
print(events)
5353
```
5454

55-
For more details, see the [API Reference](/api-reference/events/search_events).
55+
56+
For more details, see the [API Reference](/api-reference/events/search_events_by_tags).

docs/site/openapi.json

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2221,6 +2221,100 @@
22212221
]
22222222
}
22232223
},
2224+
"/api/v1/users/event_tags/search/{user_id}": {
2225+
"get": {
2226+
"tags": [
2227+
"event"
2228+
],
2229+
"summary": "Search User Events By Tags",
2230+
"operationId": "search_user_events_by_tags_api_v1_users_event_tags_search__user_id__get",
2231+
"parameters": [
2232+
{
2233+
"name": "user_id",
2234+
"in": "path",
2235+
"required": true,
2236+
"schema": {
2237+
"anyOf": [
2238+
{
2239+
"type": "string",
2240+
"format": "uuid4"
2241+
},
2242+
{
2243+
"type": "string",
2244+
"format": "uuid5"
2245+
}
2246+
],
2247+
"description": "The ID of the user",
2248+
"title": "User Id"
2249+
},
2250+
"description": "The ID of the user"
2251+
},
2252+
{
2253+
"name": "tags",
2254+
"in": "query",
2255+
"required": false,
2256+
"schema": {
2257+
"type": "string",
2258+
"description": "Comma-separated list of tag names that events must have (e.g.'emotion,romance')",
2259+
"title": "Tags"
2260+
},
2261+
"description": "Comma-separated list of tag names that events must have (e.g.'emotion,romance')"
2262+
},
2263+
{
2264+
"name": "tag_values",
2265+
"in": "query",
2266+
"required": false,
2267+
"schema": {
2268+
"type": "string",
2269+
"description": "Comma-separated tag=value pairs for exact matches (e.g., 'emotion=happy,topic=work')",
2270+
"title": "Tag Values"
2271+
},
2272+
"description": "Comma-separated tag=value pairs for exact matches (e.g., 'emotion=happy,topic=work')"
2273+
},
2274+
{
2275+
"name": "topk",
2276+
"in": "query",
2277+
"required": false,
2278+
"schema": {
2279+
"type": "integer",
2280+
"description": "Number of events to retrieve, default is 10",
2281+
"default": 10,
2282+
"title": "Topk"
2283+
},
2284+
"description": "Number of events to retrieve, default is 10"
2285+
}
2286+
],
2287+
"responses": {
2288+
"200": {
2289+
"description": "Successful Response",
2290+
"content": {
2291+
"application/json": {
2292+
"schema": {
2293+
"$ref": "#/components/schemas/UserEventsDataResponse"
2294+
}
2295+
}
2296+
}
2297+
},
2298+
"422": {
2299+
"description": "Validation Error",
2300+
"content": {
2301+
"application/json": {
2302+
"schema": {
2303+
"$ref": "#/components/schemas/HTTPValidationError"
2304+
}
2305+
}
2306+
}
2307+
}
2308+
},
2309+
"x-code-samples": [
2310+
{
2311+
"lang": "python",
2312+
"source": "# To use the Python SDK, install the package:\n# pip install memobase\n\nfrom memobase import MemoBaseClient\n\nclient = MemoBaseClient(project_url='PROJECT_URL', api_key='PROJECT_TOKEN')\nu = client.get_user(uid)\n\n# Search for events with specific tags\nevents = u.search_event_by_tags(tags=[\"emotion\", \"romance\"])\n\n# Search for events with specific tag values\nevents = u.search_event_by_tags(tag_values={\"emotion\": \"happy\", \"topic\": \"work\"})\n\n# Combine both filters\nevents = u.search_event_by_tags(tags=[\"emotion\"], tag_values={\"topic\": \"work\"})\n\n",
2313+
"label": "Python"
2314+
}
2315+
]
2316+
}
2317+
},
22242318
"/api/v1/users/context/{user_id}": {
22252319
"get": {
22262320
"tags": [

src/client/memobase/core/entry.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,47 @@ def search_event_gist(
311311
)
312312
return [UserEventGistData.model_validate(e) for e in r.data["gists"]]
313313

314+
def search_event_by_tags(
315+
self,
316+
tags: Optional[list[str]] = None,
317+
tag_values: Optional[dict[str, str]] = None,
318+
topk: int = 10,
319+
) -> list[UserEventData]:
320+
"""
321+
Search user events by tags.
322+
323+
Args:
324+
tags: List of tag names that events must have (AND condition)
325+
tag_values: Dict of tag=value pairs for exact matches (AND condition)
326+
topk: Number of events to retrieve, default is 10
327+
328+
Examples:
329+
- search_event_by_tags(tags=["emotion", "romance"])
330+
Returns events that have both 'emotion' AND 'romance' tags (with any value)
331+
332+
- search_event_by_tags(tag_values={"emotion": "happy", "topic": "work"})
333+
Returns events where emotion tag equals 'happy' AND topic tag equals 'work'
334+
335+
- search_event_by_tags(tags=["emotion"], tag_values={"topic": "work"})
336+
Returns events that have 'emotion' tag (any value) AND topic tag equals 'work'
337+
"""
338+
params = f"?topk={topk}"
339+
340+
if tags:
341+
tags_str = ",".join(tags)
342+
params += f"&tags={tags_str}"
343+
344+
if tag_values:
345+
tag_values_str = ",".join([f"{k}={v}" for k, v in tag_values.items()])
346+
params += f"&tag_values={tag_values_str}"
347+
348+
r = unpack_response(
349+
self.project_client.client.get(
350+
f"/users/event_tags/search/{self.user_id}{params}"
351+
)
352+
)
353+
return [UserEventData.model_validate(e) for e in r.data["events"]]
354+
314355
def context(
315356
self,
316357
max_token_size: int = 1000,

src/server/api/api.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,12 @@ def custom_openapi():
258258
openapi_extra=API_X_CODE_DOCS["GET /users/event_gist/search/{user_id}"],
259259
)(api_layer.event.search_user_event_gists)
260260

261+
router.get(
262+
"/users/event_tags/search/{user_id}",
263+
tags=["event"],
264+
openapi_extra=API_X_CODE_DOCS["GET /users/event_tags/search/{user_id}"],
265+
)(api_layer.event.search_user_events_by_tags)
266+
261267
router.get(
262268
"/users/context/{user_id}",
263269
tags=["context"],

src/server/api/memobase_server/api_layer/docs/event.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,29 @@
258258
)
259259

260260

261+
# Search user events by tags
262+
add_api_code_docs(
263+
"GET",
264+
"/users/event_tags/search/{user_id}",
265+
py_code(
266+
"""
267+
from memobase import MemoBaseClient
268+
269+
client = MemoBaseClient(project_url='PROJECT_URL', api_key='PROJECT_TOKEN')
270+
u = client.get_user(uid)
271+
272+
# Search for events with specific tags
273+
events = u.search_event_by_tags(tags=["emotion", "romance"])
274+
275+
# Search for events with specific tag values
276+
events = u.search_event_by_tags(tag_values={"emotion": "happy", "topic": "work"})
277+
278+
# Combine both filters
279+
events = u.search_event_by_tags(tags=["emotion"], tag_values={"topic": "work"})
280+
"""
281+
),
282+
)
283+
261284
add_api_code_docs(
262285
"GET",
263286
"/users/event_gist/search/{user_id}",

src/server/api/memobase_server/api_layer/event.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,31 @@ async def search_user_event_gists(
9898
user_id, project_id, query, topk, similarity_threshold, time_range_in_days
9999
)
100100
return p.to_response(res.UserEventGistsDataResponse)
101+
102+
103+
async def search_user_events_by_tags(
104+
request: Request,
105+
user_id: UUID = Path(..., description="The ID of the user"),
106+
tags: str = Query(None, description="Comma-separated list of tag names that events must have (e.g.'emotion,romance')"),
107+
tag_values: str = Query(None, description="Comma-separated tag=value pairs for exact matches (e.g., 'emotion=happy,topic=work')"),
108+
topk: int = Query(10, description="Number of events to retrieve, default is 10"),
109+
) -> res.UserEventsDataResponse:
110+
project_id = request.state.memobase_project_id
111+
112+
has_event_tag = None
113+
if tags:
114+
has_event_tag = [tag.strip() for tag in tags.split(",") if tag.strip()]
115+
116+
event_tag_equal = None
117+
if tag_values:
118+
event_tag_equal = {}
119+
for pair in tag_values.split(","):
120+
if "=" in pair:
121+
tag_name, tag_value = pair.split("=", 1)
122+
event_tag_equal[tag_name.strip()] = tag_value.strip()
123+
124+
p = await controllers.event.filter_user_events(
125+
user_id, project_id, has_event_tag, event_tag_equal, topk
126+
)
127+
128+
return p.to_response(res.UserEventsDataResponse)

src/server/api/tests/test_api.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,96 @@ async def test_api_event_search(
606606
assert d["errno"] == 0
607607

608608

609+
@pytest.mark.asyncio
610+
async def test_api_event_search_by_tags(
611+
client,
612+
db_env,
613+
mock_llm_complete,
614+
mock_llm_validate_complete,
615+
mock_event_summary_llm_complete,
616+
mock_entry_summary_llm_complete,
617+
mock_event_get_embedding,
618+
):
619+
# Create a user
620+
response = client.post(f"{PREFIX}/users", json={})
621+
d = response.json()
622+
assert response.status_code == 200
623+
assert d["errno"] == 0
624+
u_id = d["data"]["id"]
625+
626+
# Insert a chat blob that will create an event with tags
627+
response = client.post(
628+
f"{PREFIX}/blobs/insert/{u_id}",
629+
json={
630+
"blob_type": "chat",
631+
"blob_data": {
632+
"messages": [
633+
{"role": "user", "content": "I'm feeling happy today"},
634+
{"role": "assistant", "content": "That's wonderful!"},
635+
]
636+
},
637+
},
638+
)
639+
d = response.json()
640+
assert response.status_code == 200
641+
assert d["errno"] == 0
642+
643+
# Process the buffer to create the event
644+
response = client.post(f"{PREFIX}/users/buffer/{u_id}/chat")
645+
assert response.status_code == 200
646+
assert response.json()["errno"] == 0
647+
648+
# Test 1: Search by single tag name
649+
response = client.get(f"{PREFIX}/users/event_tags/search/{u_id}?tags=emotion")
650+
d = response.json()
651+
assert response.status_code == 200
652+
assert d["errno"] == 0
653+
assert len(d["data"]["events"]) == 1
654+
assert d["data"]["events"][0]["event_data"]["event_tags"][0]["tag"] == "emotion"
655+
assert d["data"]["events"][0]["event_data"]["event_tags"][0]["value"] == "happy"
656+
657+
# Test 2: Search by multiple tag names (comma-separated)
658+
response = client.get(f"{PREFIX}/users/event_tags/search/{u_id}?tags=emotion,nonexistent")
659+
d = response.json()
660+
assert response.status_code == 200
661+
assert d["errno"] == 0
662+
assert len(d["data"]["events"]) == 0 # Should be empty since "nonexistent" tag doesn't exist
663+
664+
# Test 3: Search by tag value
665+
response = client.get(f"{PREFIX}/users/event_tags/search/{u_id}?tag_values=emotion=happy")
666+
d = response.json()
667+
assert response.status_code == 200
668+
assert d["errno"] == 0
669+
assert len(d["data"]["events"]) == 1
670+
671+
# Test 4: Search by wrong tag value
672+
response = client.get(f"{PREFIX}/users/event_tags/search/{u_id}?tag_values=emotion=sad")
673+
d = response.json()
674+
assert response.status_code == 200
675+
assert d["errno"] == 0
676+
assert len(d["data"]["events"]) == 0
677+
678+
# Test 5: Search with topk parameter
679+
response = client.get(f"{PREFIX}/users/event_tags/search/{u_id}?tags=emotion&topk=5")
680+
d = response.json()
681+
assert response.status_code == 200
682+
assert d["errno"] == 0
683+
assert len(d["data"]["events"]) <= 5
684+
685+
# Test 6: Search with both tags and tag_values
686+
response = client.get(f"{PREFIX}/users/event_tags/search/{u_id}?tags=emotion&tag_values=emotion=happy")
687+
d = response.json()
688+
assert response.status_code == 200
689+
assert d["errno"] == 0
690+
assert len(d["data"]["events"]) == 1
691+
692+
# Clean up
693+
response = client.delete(f"{PREFIX}/users/{u_id}")
694+
d = response.json()
695+
assert response.status_code == 200
696+
assert d["errno"] == 0
697+
698+
609699
@pytest.mark.asyncio
610700
async def test_api_non_uuid_access(client, db_env):
611701

0 commit comments

Comments
 (0)