Skip to content
Open
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
12 changes: 6 additions & 6 deletions packages/client/client_tests/test_integration_complete.py
Original file line number Diff line number Diff line change
Expand Up @@ -1218,7 +1218,7 @@ def test_create_semantic_set_type(self, memory, unique_test_ids):
set_type_id = memory.create_semantic_set_type(
metadata_tags=["user_id", "session_id"],
is_org_level=False,
name="User Sessions",
name="user_sessions",
description="Set type for user sessions",
)

Expand All @@ -1240,7 +1240,7 @@ def test_list_semantic_set_types(self, memory, unique_test_ids):
# Create a set type first
memory.create_semantic_set_type(
metadata_tags=["list_test_tag"],
name="List Test Set Type",
name="list_test_set_type",
)

# List set types
Expand All @@ -1255,7 +1255,7 @@ def test_delete_semantic_set_type(self, memory, unique_test_ids):
# Create a set type to delete
set_type_id = memory.create_semantic_set_type(
metadata_tags=["delete_test_tag"],
name="Delete Test Set Type",
name="delete_test_set_type",
)

# Delete it
Expand Down Expand Up @@ -1342,7 +1342,7 @@ def test_semantic_set_type_lifecycle(self, memory, unique_test_ids):
# Step 1: Create set type
set_type_id = memory.create_semantic_set_type(
metadata_tags=["lifecycle_tag"],
name="Lifecycle Test",
name="lifecycle_test",
description="Testing full lifecycle",
)
assert set_type_id is not None
Expand Down Expand Up @@ -1417,7 +1417,7 @@ def test_semantic_category_template_lifecycle(self, memory, unique_test_ids):
# Step 1: Create a set type for templates
set_type_id = memory.create_semantic_set_type(
metadata_tags=["template_test_tag"],
name="Template Test Type",
name="template_test_type",
description="Testing category templates",
)
assert set_type_id is not None
Expand Down Expand Up @@ -1481,7 +1481,7 @@ def test_semantic_category_disable(self, memory, unique_test_ids):
# Step 1: Create a set type with a category template
set_type_id = memory.create_semantic_set_type(
metadata_tags=["disable_test_tag"],
name="Disable Test Type",
name="disable_test_type",
)
assert set_type_id is not None

Expand Down
4 changes: 2 additions & 2 deletions packages/client/client_tests/test_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -1497,7 +1497,7 @@ def test_create_semantic_set_type_success(self, mock_client):
result = memory.create_semantic_set_type(
metadata_tags=["user_id", "session_id"],
is_org_level=False,
name="User Sessions",
name="user_sessions",
description="Set type for user sessions",
)

Expand All @@ -1511,7 +1511,7 @@ def test_create_semantic_set_type_success(self, mock_client):
assert json_data["project_id"] == "test_project"
assert json_data["metadata_tags"] == ["user_id", "session_id"]
assert json_data["is_org_level"] is False
assert json_data["name"] == "User Sessions"
assert json_data["name"] == "user_sessions"
assert json_data["description"] == "Set type for user sessions"

def test_create_semantic_set_type_minimal(self, mock_client):
Expand Down
81 changes: 64 additions & 17 deletions packages/common/src/memmachine_common/api/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,47 @@ def _validate_int_compatible(v: str) -> str:
return v


# Stricter rule for user-supplied field/property names (issue #1383),
# distinct from `_is_valid_name`/`SafeId` which validates *identifiers*
# (org_id, project_id, UIDs) and stays permissive for backward compat.
MAX_FIELD_NAME_LENGTH = 32
RESERVED_FIELD_NAME_PREFIX = "_"
_FIELD_NAME_REGEX = regex.compile(r"^[a-z][a-z0-9_]*$")


def _is_valid_field_name(v: str) -> str:
"""Validate a field/property name against the rule from issue #1383.

- 1-32 characters
- starts with a lowercase letter, then ``[a-z0-9_]``
- leading underscore reserved for system use

Three checks instead of one combined regex so the error message names
the specific rule that was violated.
"""
if not v or len(v) > MAX_FIELD_NAME_LENGTH:
raise InvalidNameError(
f"Field/property name must be 1-{MAX_FIELD_NAME_LENGTH} chars, "
f"got {len(v)}: '{v}'",
)
Comment on lines +211 to +215
if v.startswith(RESERVED_FIELD_NAME_PREFIX):
raise InvalidNameError(
"Field/property names beginning with '_' are reserved for system "
f"use: '{v}'",
)
if not _FIELD_NAME_REGEX.fullmatch(v):
raise InvalidNameError(
"Field/property name must start with a-z and contain only "
f"[a-z0-9_]: '{v}'",
)
Comment on lines +222 to +225
return v


IntCompatibleId = Annotated[str, AfterValidator(_validate_int_compatible), Field(...)]

SafeId = Annotated[str, AfterValidator(_is_valid_name), Field(...)]
SafeIdWithDefault = Annotated[SafeId, Field(default=DEFAULT_ORG_AND_PROJECT_ID)]
SafeFieldName = Annotated[str, AfterValidator(_is_valid_field_name), Field(...)]


class _WithOrgAndProj(BaseModel):
Expand Down Expand Up @@ -473,7 +510,7 @@ class MemoryMessage(BaseModel):
default="",
description=SpecDoc.MEMORY_ROLE,
)
metadata: dict[str, str] = Field(
metadata: dict[SafeFieldName, str] = Field(
default_factory=dict,
description=SpecDoc.MEMORY_METADATA,
)
Expand Down Expand Up @@ -582,7 +619,7 @@ class SearchMemoriesSpec(_WithOrgAndProj):
),
]
set_metadata: Annotated[
dict[str, JsonValue] | None,
dict[SafeFieldName, JsonValue] | None,
Field(
default=None,
description=SpecDoc.SET_METADATA,
Expand Down Expand Up @@ -672,7 +709,7 @@ class ListMemoriesSpec(_WithOrgAndProj):
),
]
set_metadata: Annotated[
dict[str, JsonValue] | None,
dict[SafeFieldName, JsonValue] | None,
Field(
default=None,
description=SpecDoc.SET_METADATA,
Expand Down Expand Up @@ -773,6 +810,10 @@ class AddFeatureSpec(_WithOrgAndProj):
description=SpecDoc.FEATURE_SET_ID,
),
]
# Lookup field referencing an existing category by name (see
# ``SemanticMemory.add_new_feature``, which raises ``CategoryNotFoundError``
# for unknown categories). Kept on permissive ``str`` so features can
# still be added to legacy categories whose names predate the #1383 rule.
category_name: Annotated[
str,
Field(
Expand All @@ -781,14 +822,14 @@ class AddFeatureSpec(_WithOrgAndProj):
),
]
tag: Annotated[
str,
SafeFieldName,
Field(
...,
description=SpecDoc.FEATURE_TAG,
),
]
feature: Annotated[
str,
SafeFieldName,
Field(
...,
description=SpecDoc.FEATURE_NAME,
Expand All @@ -802,7 +843,7 @@ class AddFeatureSpec(_WithOrgAndProj):
),
]
feature_metadata: Annotated[
dict[str, JsonValue] | None,
dict[SafeFieldName, JsonValue] | None,
Field(
default=None,
description=SpecDoc.FEATURE_METADATA,
Expand Down Expand Up @@ -858,6 +899,9 @@ class UpdateFeatureSpec(_WithOrgAndProj):
description=SpecDoc.FEATURE_ID,
),
]
# Lookup of an existing category by name (see ``SemanticMemory.add_new_feature``
# at ``semantic_memory.py:299``). Permissive so legacy categories stay
# addressable, matching ``AddFeatureSpec.category_name``.
category_name: Annotated[
str | None,
Field(
Expand All @@ -866,14 +910,14 @@ class UpdateFeatureSpec(_WithOrgAndProj):
),
]
tag: Annotated[
str | None,
SafeFieldName | None,
Field(
default=None,
description=SpecDoc.FEATURE_TAG,
),
]
feature: Annotated[
str | None,
SafeFieldName | None,
Field(
default=None,
description=SpecDoc.FEATURE_NAME,
Expand All @@ -887,7 +931,7 @@ class UpdateFeatureSpec(_WithOrgAndProj):
),
]
metadata: Annotated[
dict[str, str] | None,
dict[SafeFieldName, str] | None,
Field(
default=None,
description=SpecDoc.FEATURE_METADATA,
Expand Down Expand Up @@ -1055,14 +1099,14 @@ class CreateSemanticSetTypeSpec(_WithOrgAndProj):
),
]
metadata_tags: Annotated[
list[str],
list[SafeFieldName],
Field(
...,
description=SpecDoc.SET_TYPE_METADATA_TAGS,
),
]
name: Annotated[
str | None,
SafeFieldName | None,
Field(
default=None,
description=SpecDoc.SET_TYPE_NAME,
Expand Down Expand Up @@ -1168,14 +1212,14 @@ class GetSemanticSetIdSpec(_WithOrgAndProj):
),
]
metadata_tags: Annotated[
list[str],
list[SafeFieldName],
Field(
...,
description=SpecDoc.SET_TYPE_METADATA_TAGS,
),
]
set_metadata: Annotated[
dict[str, JsonValue] | None,
dict[SafeFieldName, JsonValue] | None,
Field(
default=None,
description=SpecDoc.SET_METADATA,
Expand All @@ -1199,7 +1243,7 @@ class ListSemanticSetIdsSpec(_WithOrgAndProj):
"""Specification model for listing semantic set IDs."""

set_metadata: Annotated[
dict[str, JsonValue] | None,
dict[SafeFieldName, JsonValue] | None,
Field(
default=None,
description=SpecDoc.SET_METADATA,
Expand Down Expand Up @@ -1344,7 +1388,7 @@ class AddSemanticCategorySpec(_WithOrgAndProj):
),
]
category_name: Annotated[
str,
SafeFieldName,
Field(
...,
description=SpecDoc.CATEGORY_NAME,
Expand Down Expand Up @@ -1389,7 +1433,7 @@ class AddSemanticCategoryTemplateSpec(_WithOrgAndProj):
),
]
category_name: Annotated[
str,
SafeFieldName,
Field(
...,
description=SpecDoc.CATEGORY_NAME,
Expand Down Expand Up @@ -1485,6 +1529,9 @@ class DisableSemanticCategorySpec(_WithOrgAndProj):
description=SpecDoc.SEMANTIC_SET_ID,
),
]
# Lookup field referencing an existing category by name; kept on the
# permissive `str` type so legacy categories remain disable-able even
# if their names predate the #1383 rule.
category_name: Annotated[
str,
Field(
Expand Down Expand Up @@ -1544,7 +1591,7 @@ class AddSemanticTagSpec(_WithOrgAndProj):
),
]
tag_name: Annotated[
str,
SafeFieldName,
Field(
...,
description=SpecDoc.TAG_NAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -985,12 +985,14 @@ def test_update_feature_error(client, mock_memmachine):


def test_create_semantic_set_type(client, mock_memmachine):
# `name` and `metadata_tags` items must conform to the #1383 rule
# (lowercase alphanumeric/underscore, ≤32 chars, no leading "_").
payload = {
"org_id": "test_org",
"project_id": "test_proj",
"metadata_tags": ["user_id", "session_id"],
"is_org_level": False,
"name": "User Sessions",
"name": "user_sessions",
"description": "Set type for user sessions",
}

Expand All @@ -1005,7 +1007,7 @@ def test_create_semantic_set_type(client, mock_memmachine):
assert call_args["session_data"].session_key == "test_org/test_proj"
assert call_args["metadata_tags"] == ["user_id", "session_id"]
assert call_args["is_org_level"] is False
assert call_args["name"] == "User Sessions"
assert call_args["name"] == "user_sessions"
assert call_args["description"] == "Set type for user sessions"


Expand Down
Loading
Loading