diff --git a/letta/server/rest_api/routers/v1/identities.py b/letta/server/rest_api/routers/v1/identities.py index 04d38c63..d3d7bfd9 100644 --- a/letta/server/rest_api/routers/v1/identities.py +++ b/letta/server/rest_api/routers/v1/identities.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, List, Optional from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query from letta.orm.errors import NoResultFound, UniqueConstraintViolationError -from letta.schemas.identity import Identity, IdentityCreate, IdentityType, IdentityUpdate +from letta.schemas.identity import Identity, IdentityCreate, IdentityProperty, IdentityType, IdentityUpdate from letta.server.rest_api.utils import get_letta_server if TYPE_CHECKING: @@ -125,6 +125,24 @@ def modify_identity( raise HTTPException(status_code=500, detail=f"{e}") +@router.put("/{identity_id}/properties", tags=["identities"], operation_id="upsert_identity_properties") +def upsert_identity_properties( + identity_id: str, + properties: List[IdentityProperty] = Body(...), + server: "SyncServer" = Depends(get_letta_server), + actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present +): + try: + actor = server.user_manager.get_user_or_default(user_id=actor_id) + return server.identity_manager.upsert_identity_properties(identity_id=identity_id, properties=properties, actor=actor) + except HTTPException: + raise + except NoResultFound as e: + raise HTTPException(status_code=404, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=f"{e}") + + @router.delete("/{identity_id}", tags=["identities"], operation_id="delete_identity") def delete_identity( identity_id: str, diff --git a/letta/services/identity_manager.py b/letta/services/identity_manager.py index c1e19b41..9bb629e8 100644 --- a/letta/services/identity_manager.py +++ b/letta/services/identity_manager.py @@ -8,7 +8,7 @@ from letta.orm.agent import Agent as AgentModel from letta.orm.block import Block as BlockModel from letta.orm.identity import Identity as IdentityModel from letta.schemas.identity import Identity as PydanticIdentity -from letta.schemas.identity import IdentityCreate, IdentityType, IdentityUpdate +from letta.schemas.identity import IdentityCreate, IdentityProperty, IdentityType, IdentityUpdate from letta.schemas.user import User as PydanticUser from letta.utils import enforce_types @@ -165,6 +165,20 @@ class IdentityManager: existing_identity.update(session, actor=actor) return existing_identity.to_pydantic() + @enforce_types + def upsert_identity_properties(self, identity_id: str, properties: List[IdentityProperty], actor: PydanticUser) -> PydanticIdentity: + with self.session_maker() as session: + existing_identity = IdentityModel.read(db_session=session, identifier=identity_id, actor=actor) + if existing_identity is None: + raise HTTPException(status_code=404, detail="Identity not found") + return self._update_identity( + session=session, + existing_identity=existing_identity, + identity=IdentityUpdate(properties=properties), + actor=actor, + replace=True, + ) + @enforce_types def delete_identity(self, identity_id: str, actor: PydanticUser) -> None: with self.session_maker() as session: diff --git a/tests/test_managers.py b/tests/test_managers.py index ab797b04..c939f31f 100644 --- a/tests/test_managers.py +++ b/tests/test_managers.py @@ -3488,6 +3488,34 @@ def test_get_set_blocks_for_identities(server: SyncServer, default_block, defaul server.identity_manager.delete_identity(identity.id, actor=default_user) +def test_upsert_properties(server: SyncServer, default_user): + identity_create = IdentityCreate( + identifier_key="1234", + name="caren", + identity_type=IdentityType.user, + properties=[ + IdentityProperty(key="email", value="caren@letta.com", type=IdentityPropertyType.string), + IdentityProperty(key="age", value=28, type=IdentityPropertyType.number), + ], + ) + + identity = server.identity_manager.create_identity(identity_create, actor=default_user) + properties = [ + IdentityProperty(key="email", value="caren@gmail.com", type=IdentityPropertyType.string), + IdentityProperty(key="age", value="28", type=IdentityPropertyType.string), + IdentityProperty(key="test", value=123, type=IdentityPropertyType.number), + ] + + updated_identity = server.identity_manager.upsert_identity_properties( + identity_id=identity.id, + properties=properties, + actor=default_user, + ) + assert updated_identity.properties == properties + + server.identity_manager.delete_identity(identity.id, actor=default_user) + + # ====================================================================================================================== # SourceManager Tests - Sources # ======================================================================================================================