Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/site/api-reference/events/search_events_by_tags.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: 'Search Events By Tags'
openapi: get /api/v1/users/event_tags/search/{user_id}
---
Search events by tags.

1 change: 1 addition & 0 deletions docs/site/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
"api-reference/events/get_events",
"api-reference/events/search_events",
"api-reference/events/search_event_gists",
"api-reference/events/search_events_by_tags",
"api-reference/events/update_event",
"api-reference/events/delete_event"
]
Expand Down
9 changes: 5 additions & 4 deletions docs/site/features/event/event_tag.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,18 @@ for event in events:

## Searching Events by Tag

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

```python
from memobase import MemoBaseClient

client = MemoBaseClient(project_url='YOUR_PROJECT_URL', api_key='YOUR_API_KEY')
user = client.get_user('some_user_id')

# Find all events tagged with 'emotion'
events = user.search_event(tags=["emotion"])
# Find events with specific tags (AND condition)
events = user.search_event_by_tags(tags=["emotion", "romance"])
print(events)
```

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

For more details, see the [API Reference](/api-reference/events/search_events_by_tags).
94 changes: 94 additions & 0 deletions docs/site/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2221,6 +2221,100 @@
]
}
},
"/api/v1/users/event_tags/search/{user_id}": {
"get": {
"tags": [
"event"
],
"summary": "Search User Events By Tags",
"operationId": "search_user_events_by_tags_api_v1_users_event_tags_search__user_id__get",
"parameters": [
{
"name": "user_id",
"in": "path",
"required": true,
"schema": {
"anyOf": [
{
"type": "string",
"format": "uuid4"
},
{
"type": "string",
"format": "uuid5"
}
],
"description": "The ID of the user",
"title": "User Id"
},
"description": "The ID of the user"
},
{
"name": "tags",
"in": "query",
"required": false,
"schema": {
"type": "string",
"description": "Comma-separated list of tag names that events must have (e.g.'emotion,romance')",
"title": "Tags"
},
"description": "Comma-separated list of tag names that events must have (e.g.'emotion,romance')"
},
{
"name": "tag_values",
"in": "query",
"required": false,
"schema": {
"type": "string",
"description": "Comma-separated tag=value pairs for exact matches (e.g., 'emotion=happy,topic=work')",
"title": "Tag Values"
},
"description": "Comma-separated tag=value pairs for exact matches (e.g., 'emotion=happy,topic=work')"
},
{
"name": "topk",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"description": "Number of events to retrieve, default is 10",
"default": 10,
"title": "Topk"
},
"description": "Number of events to retrieve, default is 10"
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserEventsDataResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
},
"x-code-samples": [
{
"lang": "python",
"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",
"label": "Python"
}
]
}
},
"/api/v1/users/context/{user_id}": {
"get": {
"tags": [
Expand Down
41 changes: 41 additions & 0 deletions src/client/memobase/core/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,47 @@ def search_event_gist(
)
return [UserEventGistData.model_validate(e) for e in r.data["gists"]]

def search_event_by_tags(
self,
tags: Optional[list[str]] = None,
tag_values: Optional[dict[str, str]] = None,
topk: int = 10,
) -> list[UserEventData]:
"""
Search user events by tags.

Args:
tags: List of tag names that events must have (AND condition)
tag_values: Dict of tag=value pairs for exact matches (AND condition)
topk: Number of events to retrieve, default is 10

Examples:
- search_event_by_tags(tags=["emotion", "romance"])
Returns events that have both 'emotion' AND 'romance' tags (with any value)

- search_event_by_tags(tag_values={"emotion": "happy", "topic": "work"})
Returns events where emotion tag equals 'happy' AND topic tag equals 'work'

- search_event_by_tags(tags=["emotion"], tag_values={"topic": "work"})
Returns events that have 'emotion' tag (any value) AND topic tag equals 'work'
"""
params = f"?topk={topk}"

if tags:
tags_str = ",".join(tags)
params += f"&tags={tags_str}"

if tag_values:
tag_values_str = ",".join([f"{k}={v}" for k, v in tag_values.items()])
params += f"&tag_values={tag_values_str}"

r = unpack_response(
self.project_client.client.get(
f"/users/event_tags/search/{self.user_id}{params}"
)
)
return [UserEventData.model_validate(e) for e in r.data["events"]]

def context(
self,
max_token_size: int = 1000,
Expand Down
6 changes: 6 additions & 0 deletions src/server/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,12 @@ def custom_openapi():
openapi_extra=API_X_CODE_DOCS["GET /users/event_gist/search/{user_id}"],
)(api_layer.event.search_user_event_gists)

router.get(
"/users/event_tags/search/{user_id}",
tags=["event"],
openapi_extra=API_X_CODE_DOCS["GET /users/event_tags/search/{user_id}"],
)(api_layer.event.search_user_events_by_tags)

router.get(
"/users/context/{user_id}",
tags=["context"],
Expand Down
23 changes: 23 additions & 0 deletions src/server/api/memobase_server/api_layer/docs/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,29 @@
)


# Search user events by tags
add_api_code_docs(
"GET",
"/users/event_tags/search/{user_id}",
py_code(
"""
from memobase import MemoBaseClient

client = MemoBaseClient(project_url='PROJECT_URL', api_key='PROJECT_TOKEN')
u = client.get_user(uid)

# Search for events with specific tags
events = u.search_event_by_tags(tags=["emotion", "romance"])

# Search for events with specific tag values
events = u.search_event_by_tags(tag_values={"emotion": "happy", "topic": "work"})

# Combine both filters
events = u.search_event_by_tags(tags=["emotion"], tag_values={"topic": "work"})
"""
),
)

add_api_code_docs(
"GET",
"/users/event_gist/search/{user_id}",
Expand Down
28 changes: 28 additions & 0 deletions src/server/api/memobase_server/api_layer/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,31 @@ async def search_user_event_gists(
user_id, project_id, query, topk, similarity_threshold, time_range_in_days
)
return p.to_response(res.UserEventGistsDataResponse)


async def search_user_events_by_tags(
request: Request,
user_id: UUID = Path(..., description="The ID of the user"),
tags: str = Query(None, description="Comma-separated list of tag names that events must have (e.g.'emotion,romance')"),
tag_values: str = Query(None, description="Comma-separated tag=value pairs for exact matches (e.g., 'emotion=happy,topic=work')"),
topk: int = Query(10, description="Number of events to retrieve, default is 10"),
) -> res.UserEventsDataResponse:
project_id = request.state.memobase_project_id

has_event_tag = None
if tags:
has_event_tag = [tag.strip() for tag in tags.split(",") if tag.strip()]

event_tag_equal = None
if tag_values:
event_tag_equal = {}
for pair in tag_values.split(","):
if "=" in pair:
tag_name, tag_value = pair.split("=", 1)
event_tag_equal[tag_name.strip()] = tag_value.strip()

p = await controllers.event.filter_user_events(
user_id, project_id, has_event_tag, event_tag_equal, topk
)

return p.to_response(res.UserEventsDataResponse)
90 changes: 90 additions & 0 deletions src/server/api/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,96 @@ async def test_api_event_search(
assert d["errno"] == 0


@pytest.mark.asyncio
async def test_api_event_search_by_tags(
client,
db_env,
mock_llm_complete,
mock_llm_validate_complete,
mock_event_summary_llm_complete,
mock_entry_summary_llm_complete,
mock_event_get_embedding,
):
# Create a user
response = client.post(f"{PREFIX}/users", json={})
d = response.json()
assert response.status_code == 200
assert d["errno"] == 0
u_id = d["data"]["id"]

# Insert a chat blob that will create an event with tags
response = client.post(
f"{PREFIX}/blobs/insert/{u_id}",
json={
"blob_type": "chat",
"blob_data": {
"messages": [
{"role": "user", "content": "I'm feeling happy today"},
{"role": "assistant", "content": "That's wonderful!"},
]
},
},
)
d = response.json()
assert response.status_code == 200
assert d["errno"] == 0

# Process the buffer to create the event
response = client.post(f"{PREFIX}/users/buffer/{u_id}/chat")
assert response.status_code == 200
assert response.json()["errno"] == 0

# Test 1: Search by single tag name
response = client.get(f"{PREFIX}/users/event_tags/search/{u_id}?tags=emotion")
d = response.json()
assert response.status_code == 200
assert d["errno"] == 0
assert len(d["data"]["events"]) == 1
assert d["data"]["events"][0]["event_data"]["event_tags"][0]["tag"] == "emotion"
assert d["data"]["events"][0]["event_data"]["event_tags"][0]["value"] == "happy"

# Test 2: Search by multiple tag names (comma-separated)
response = client.get(f"{PREFIX}/users/event_tags/search/{u_id}?tags=emotion,nonexistent")
d = response.json()
assert response.status_code == 200
assert d["errno"] == 0
assert len(d["data"]["events"]) == 0 # Should be empty since "nonexistent" tag doesn't exist

# Test 3: Search by tag value
response = client.get(f"{PREFIX}/users/event_tags/search/{u_id}?tag_values=emotion=happy")
d = response.json()
assert response.status_code == 200
assert d["errno"] == 0
assert len(d["data"]["events"]) == 1

# Test 4: Search by wrong tag value
response = client.get(f"{PREFIX}/users/event_tags/search/{u_id}?tag_values=emotion=sad")
d = response.json()
assert response.status_code == 200
assert d["errno"] == 0
assert len(d["data"]["events"]) == 0

# Test 5: Search with topk parameter
response = client.get(f"{PREFIX}/users/event_tags/search/{u_id}?tags=emotion&topk=5")
d = response.json()
assert response.status_code == 200
assert d["errno"] == 0
assert len(d["data"]["events"]) <= 5

# Test 6: Search with both tags and tag_values
response = client.get(f"{PREFIX}/users/event_tags/search/{u_id}?tags=emotion&tag_values=emotion=happy")
d = response.json()
assert response.status_code == 200
assert d["errno"] == 0
assert len(d["data"]["events"]) == 1

# Clean up
response = client.delete(f"{PREFIX}/users/{u_id}")
d = response.json()
assert response.status_code == 200
assert d["errno"] == 0


@pytest.mark.asyncio
async def test_api_non_uuid_access(client, db_env):

Expand Down