From 272f055b4a191b01cb03a1695304f2c462991676 Mon Sep 17 00:00:00 2001 From: Ari Webb Date: Thu, 23 Oct 2025 16:40:40 -0700 Subject: [PATCH] feat: attach/detach identities route on blocks and agents, move archives attach/detach routes to agents [LET-4428] (#5708) * deprecate ids for identity endpoints in favor of attach * move archive attach/detach to agent * new identities routes * overrides for path --------- Co-authored-by: Ari Webb --- fern/openapi-overrides.yml | 102 ++++ fern/openapi.json | 478 ++++++++++++++----- letta/schemas/identity.py | 12 +- letta/server/rest_api/routers/v1/agents.py | 76 +++ letta/server/rest_api/routers/v1/archives.py | 44 -- letta/server/rest_api/routers/v1/blocks.py | 38 ++ letta/services/identity_manager.py | 72 +++ 7 files changed, 642 insertions(+), 180 deletions(-) diff --git a/fern/openapi-overrides.yml b/fern/openapi-overrides.yml index e5797209..be730bef 100644 --- a/fern/openapi-overrides.yml +++ b/fern/openapi-overrides.yml @@ -687,6 +687,74 @@ paths: required: true schema: type: string + /v1/agents/{agent_id}/archives/attach/{archive_id}: + patch: + x-fern-sdk-group-name: + - agents + - archives + x-fern-sdk-method-name: attach + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: archive_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/archives/detach/{archive_id}: + patch: + x-fern-sdk-group-name: + - agents + - archives + x-fern-sdk-method-name: detach + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: archive_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/identities/attach/{identity_id}: + patch: + x-fern-sdk-group-name: + - agents + - identities + x-fern-sdk-method-name: attach + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: identity_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/identities/detach/{identity_id}: + patch: + x-fern-sdk-group-name: + - agents + - identities + x-fern-sdk-method-name: detach + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: identity_id + in: path + required: true + schema: + type: string /v1/agents/import: post: x-fern-sdk-group-name: @@ -767,6 +835,40 @@ paths: - blocks - agents x-fern-sdk-method-name: list + /v1/blocks/{block_id}/identities/attach/{identity_id}: + patch: + x-fern-sdk-group-name: + - blocks + - identities + x-fern-sdk-method-name: attach + parameters: + - name: block_id + in: path + required: true + schema: + type: string + - name: identity_id + in: path + required: true + schema: + type: string + /v1/blocks/{block_id}/identities/detach/{identity_id}: + patch: + x-fern-sdk-group-name: + - blocks + - identities + x-fern-sdk-method-name: detach + parameters: + - name: block_id + in: path + required: true + schema: + type: string + - name: identity_id + in: path + required: true + schema: + type: string /v1/jobs/: get: x-fern-sdk-group-name: diff --git a/fern/openapi.json b/fern/openapi.json index 882617d0..d5703fd4 100644 --- a/fern/openapi.json +++ b/fern/openapi.json @@ -346,130 +346,6 @@ } } }, - "/v1/archives/{archive_id}/attach/{agent_id}": { - "patch": { - "tags": ["archives"], - "summary": "Attach Agent To Archive", - "description": "Attach an agent to an archive.", - "operationId": "attach_agent_to_archive", - "parameters": [ - { - "name": "archive_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "minLength": 44, - "maxLength": 44, - "pattern": "^archive-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", - "description": "The ID of the archive in the format 'archive-'", - "examples": ["archive-123e4567-e89b-42d3-8456-426614174000"], - "title": "Archive Id" - }, - "description": "The ID of the archive in the format 'archive-'" - }, - { - "name": "agent_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "minLength": 42, - "maxLength": 42, - "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", - "description": "The ID of the agent in the format 'agent-'", - "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], - "title": "Agent Id" - }, - "description": "The ID of the agent in the format 'agent-'" - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Archive" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/v1/archives/{archive_id}/detach/{agent_id}": { - "patch": { - "tags": ["archives"], - "summary": "Detach Agent From Archive", - "description": "Detach an agent from an archive.", - "operationId": "detach_agent_from_archive", - "parameters": [ - { - "name": "archive_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "minLength": 44, - "maxLength": 44, - "pattern": "^archive-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", - "description": "The ID of the archive in the format 'archive-'", - "examples": ["archive-123e4567-e89b-42d3-8456-426614174000"], - "title": "Archive Id" - }, - "description": "The ID of the archive in the format 'archive-'" - }, - { - "name": "agent_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "minLength": 42, - "maxLength": 42, - "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", - "description": "The ID of the agent in the format 'agent-'", - "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], - "title": "Agent Id" - }, - "description": "The ID of the agent in the format 'agent-'" - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Archive" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, "/v1/archives/{archive_id}/agents": { "get": { "tags": ["archives"], @@ -6325,6 +6201,230 @@ } } }, + "/v1/agents/{agent_id}/archives/attach/{archive_id}": { + "patch": { + "tags": ["agents"], + "summary": "Attach Archive To Agent", + "description": "Attach an archive to an agent.", + "operationId": "attach_archive_to_agent", + "parameters": [ + { + "name": "archive_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Archive Id" + } + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentState" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/archives/detach/{archive_id}": { + "patch": { + "tags": ["agents"], + "summary": "Detach Archive From Agent", + "description": "Detach an archive from an agent.", + "operationId": "detach_archive_from_agent", + "parameters": [ + { + "name": "archive_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Archive Id" + } + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentState" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/identities/attach/{identity_id}": { + "patch": { + "tags": ["agents"], + "summary": "Attach Identity To Agent", + "description": "Attach an identity to an agent.", + "operationId": "attach_identity_to_agent", + "parameters": [ + { + "name": "identity_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Identity Id" + } + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentState" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/identities/detach/{identity_id}": { + "patch": { + "tags": ["agents"], + "summary": "Detach Identity From Agent", + "description": "Detach an identity from an agent.", + "operationId": "detach_identity_from_agent", + "parameters": [ + { + "name": "identity_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Identity Id" + } + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentState" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/v1/agents/{agent_id}/archival-memory": { "get": { "tags": ["agents"], @@ -10810,6 +10910,118 @@ } } }, + "/v1/blocks/{block_id}/identities/attach/{identity_id}": { + "patch": { + "tags": ["blocks"], + "summary": "Attach Identity To Block", + "description": "Attach an identity to a block.", + "operationId": "attach_identity_to_block", + "parameters": [ + { + "name": "identity_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Identity Id" + } + }, + { + "name": "block_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"], + "title": "Block Id" + }, + "description": "The ID of the block in the format 'block-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Block" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/blocks/{block_id}/identities/detach/{identity_id}": { + "patch": { + "tags": ["blocks"], + "summary": "Detach Identity From Block", + "description": "Detach an identity from a block.", + "operationId": "detach_identity_from_block", + "parameters": [ + { + "name": "identity_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Identity Id" + } + }, + { + "name": "block_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"], + "title": "Block Id" + }, + "description": "The ID of the block in the format 'block-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Block" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/v1/jobs/": { "get": { "tags": ["jobs"], @@ -23342,7 +23554,8 @@ } ], "title": "Agent Ids", - "description": "The agent ids that are associated with the identity." + "description": "The agent ids that are associated with the identity.", + "deprecated": true }, "block_ids": { "anyOf": [ @@ -23357,7 +23570,8 @@ } ], "title": "Block Ids", - "description": "The IDs of the blocks associated with the identity." + "description": "The IDs of the blocks associated with the identity.", + "deprecated": true }, "properties": { "anyOf": [ @@ -23482,7 +23696,8 @@ } ], "title": "Agent Ids", - "description": "The agent ids that are associated with the identity." + "description": "The agent ids that are associated with the identity.", + "deprecated": true }, "block_ids": { "anyOf": [ @@ -23497,7 +23712,8 @@ } ], "title": "Block Ids", - "description": "The IDs of the blocks associated with the identity." + "description": "The IDs of the blocks associated with the identity.", + "deprecated": true }, "properties": { "anyOf": [ @@ -23560,7 +23776,8 @@ } ], "title": "Agent Ids", - "description": "The agent ids that are associated with the identity." + "description": "The agent ids that are associated with the identity.", + "deprecated": true }, "block_ids": { "anyOf": [ @@ -23575,7 +23792,8 @@ } ], "title": "Block Ids", - "description": "The IDs of the blocks associated with the identity." + "description": "The IDs of the blocks associated with the identity.", + "deprecated": true }, "properties": { "anyOf": [ diff --git a/letta/schemas/identity.py b/letta/schemas/identity.py index 9318d00c..b3a106ed 100644 --- a/letta/schemas/identity.py +++ b/letta/schemas/identity.py @@ -57,8 +57,8 @@ class IdentityCreate(LettaBase): name: str = Field(..., description="The name of the identity.") identity_type: IdentityType = Field(..., description="The type of the identity.") project_id: Optional[str] = Field(None, description="The project id of the identity, if applicable.") - agent_ids: Optional[List[str]] = Field(None, description="The agent ids that are associated with the identity.") - block_ids: Optional[List[str]] = Field(None, description="The IDs of the blocks associated with the identity.") + agent_ids: Optional[List[str]] = Field(None, description="The agent ids that are associated with the identity.", deprecated=True) + block_ids: Optional[List[str]] = Field(None, description="The IDs of the blocks associated with the identity.", deprecated=True) properties: Optional[List[IdentityProperty]] = Field(None, description="List of properties associated with the identity.") @@ -67,8 +67,8 @@ class IdentityUpsert(LettaBase): name: str = Field(..., description="The name of the identity.") identity_type: IdentityType = Field(..., description="The type of the identity.") project_id: Optional[str] = Field(None, description="The project id of the identity, if applicable.") - agent_ids: Optional[List[str]] = Field(None, description="The agent ids that are associated with the identity.") - block_ids: Optional[List[str]] = Field(None, description="The IDs of the blocks associated with the identity.") + agent_ids: Optional[List[str]] = Field(None, description="The agent ids that are associated with the identity.", deprecated=True) + block_ids: Optional[List[str]] = Field(None, description="The IDs of the blocks associated with the identity.", deprecated=True) properties: Optional[List[IdentityProperty]] = Field(None, description="List of properties associated with the identity.") @@ -76,6 +76,6 @@ class IdentityUpdate(LettaBase): identifier_key: Optional[str] = Field(None, description="External, user-generated identifier key of the identity.") name: Optional[str] = Field(None, description="The name of the identity.") identity_type: Optional[IdentityType] = Field(None, description="The type of the identity.") - agent_ids: Optional[List[str]] = Field(None, description="The agent ids that are associated with the identity.") - block_ids: Optional[List[str]] = Field(None, description="The IDs of the blocks associated with the identity.") + agent_ids: Optional[List[str]] = Field(None, description="The agent ids that are associated with the identity.", deprecated=True) + block_ids: Optional[List[str]] = Field(None, description="The IDs of the blocks associated with the identity.", deprecated=True) properties: Optional[List[IdentityProperty]] = Field(None, description="List of properties associated with the identity.") diff --git a/letta/server/rest_api/routers/v1/agents.py b/letta/server/rest_api/routers/v1/agents.py index c7dcb79e..0f6c1267 100644 --- a/letta/server/rest_api/routers/v1/agents.py +++ b/letta/server/rest_api/routers/v1/agents.py @@ -1005,6 +1005,82 @@ async def detach_block_from_agent( return await server.agent_manager.detach_block_async(agent_id=agent_id, block_id=block_id, actor=actor) +@router.patch("/{agent_id}/archives/attach/{archive_id}", response_model=AgentState, operation_id="attach_archive_to_agent") +async def attach_archive_to_agent( + archive_id: str, + agent_id: AgentId, + server: "SyncServer" = Depends(get_letta_server), + headers: HeaderParams = Depends(get_headers), +): + """ + Attach an archive to an agent. + """ + actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) + await server.archive_manager.attach_agent_to_archive_async( + agent_id=agent_id, + archive_id=archive_id, + actor=actor, + ) + return await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=actor) + + +@router.patch("/{agent_id}/archives/detach/{archive_id}", response_model=AgentState, operation_id="detach_archive_from_agent") +async def detach_archive_from_agent( + archive_id: str, + agent_id: AgentId, + server: "SyncServer" = Depends(get_letta_server), + headers: HeaderParams = Depends(get_headers), +): + """ + Detach an archive from an agent. + """ + actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) + await server.archive_manager.detach_agent_from_archive_async( + agent_id=agent_id, + archive_id=archive_id, + actor=actor, + ) + return await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=actor) + + +@router.patch("/{agent_id}/identities/attach/{identity_id}", response_model=AgentState, operation_id="attach_identity_to_agent") +async def attach_identity_to_agent( + identity_id: str, + agent_id: AgentId, + server: "SyncServer" = Depends(get_letta_server), + headers: HeaderParams = Depends(get_headers), +): + """ + Attach an identity to an agent. + """ + actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) + await server.identity_manager.attach_agent_async( + identity_id=identity_id, + agent_id=agent_id, + actor=actor, + ) + return await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=actor) + + +@router.patch("/{agent_id}/identities/detach/{identity_id}", response_model=AgentState, operation_id="detach_identity_from_agent") +async def detach_identity_from_agent( + identity_id: str, + agent_id: AgentId, + server: "SyncServer" = Depends(get_letta_server), + headers: HeaderParams = Depends(get_headers), +): + """ + Detach an identity from an agent. + """ + actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) + await server.identity_manager.detach_agent_async( + identity_id=identity_id, + agent_id=agent_id, + actor=actor, + ) + return await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=actor) + + @router.get("/{agent_id}/archival-memory", response_model=list[Passage], operation_id="list_passages", deprecated=True) async def list_passages( agent_id: AgentId, diff --git a/letta/server/rest_api/routers/v1/archives.py b/letta/server/rest_api/routers/v1/archives.py index 1296a64d..3f3554c8 100644 --- a/letta/server/rest_api/routers/v1/archives.py +++ b/letta/server/rest_api/routers/v1/archives.py @@ -136,50 +136,6 @@ async def delete_archive( ) -@router.patch("/{archive_id}/attach/{agent_id}", response_model=PydanticArchive, operation_id="attach_agent_to_archive") -async def attach_agent_to_archive( - archive_id: ArchiveId, - agent_id: AgentId, - server: "SyncServer" = Depends(get_letta_server), - headers: HeaderParams = Depends(get_headers), -): - """ - Attach an agent to an archive. - """ - actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) - await server.archive_manager.attach_agent_to_archive_async( - agent_id=agent_id, - archive_id=archive_id, - actor=actor, - ) - return await server.archive_manager.get_archive_by_id_async( - archive_id=archive_id, - actor=actor, - ) - - -@router.patch("/{archive_id}/detach/{agent_id}", response_model=PydanticArchive, operation_id="detach_agent_from_archive") -async def detach_agent_from_archive( - archive_id: ArchiveId, - agent_id: AgentId, - server: "SyncServer" = Depends(get_letta_server), - headers: HeaderParams = Depends(get_headers), -): - """ - Detach an agent from an archive. - """ - actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) - await server.archive_manager.detach_agent_from_archive_async( - agent_id=agent_id, - archive_id=archive_id, - actor=actor, - ) - return await server.archive_manager.get_archive_by_id_async( - archive_id=archive_id, - actor=actor, - ) - - @router.get("/{archive_id}/agents", response_model=List[AgentState], operation_id="list_agents_for_archive") async def list_agents_for_archive( archive_id: ArchiveId, diff --git a/letta/server/rest_api/routers/v1/blocks.py b/letta/server/rest_api/routers/v1/blocks.py index f34fd4d8..14f00984 100644 --- a/letta/server/rest_api/routers/v1/blocks.py +++ b/letta/server/rest_api/routers/v1/blocks.py @@ -212,3 +212,41 @@ async def list_agents_for_block( actor=actor, ) return agents + + +@router.patch("/{block_id}/identities/attach/{identity_id}", response_model=Block, operation_id="attach_identity_to_block") +async def attach_identity_to_block( + identity_id: str, + block_id: BlockId, + server: SyncServer = Depends(get_letta_server), + headers: HeaderParams = Depends(get_headers), +): + """ + Attach an identity to a block. + """ + actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) + await server.identity_manager.attach_block_async( + identity_id=identity_id, + block_id=block_id, + actor=actor, + ) + return await server.block_manager.get_block_by_id_async(block_id=block_id, actor=actor) + + +@router.patch("/{block_id}/identities/detach/{identity_id}", response_model=Block, operation_id="detach_identity_from_block") +async def detach_identity_from_block( + identity_id: str, + block_id: BlockId, + server: SyncServer = Depends(get_letta_server), + headers: HeaderParams = Depends(get_headers), +): + """ + Detach an identity from a block. + """ + actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) + await server.identity_manager.detach_block_async( + identity_id=identity_id, + block_id=block_id, + actor=actor, + ) + return await server.block_manager.get_block_by_id_async(block_id=block_id, actor=actor) diff --git a/letta/services/identity_manager.py b/letta/services/identity_manager.py index 6d3ff76b..7adeeed0 100644 --- a/letta/services/identity_manager.py +++ b/letta/services/identity_manager.py @@ -348,3 +348,75 @@ class IdentityManager: identity_id=identity.id, ) return [block.to_pydantic() for block in blocks] + + @enforce_types + @trace_method + @raise_on_invalid_id(param_name="identity_id", expected_prefix=PrimitiveType.IDENTITY) + @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT) + async def attach_agent_async(self, identity_id: str, agent_id: str, actor: PydanticUser) -> None: + """ + Attach an agent to an identity. + """ + async with db_registry.async_session() as session: + identity = await IdentityModel.read_async(db_session=session, identifier=identity_id, actor=actor) + + agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor) + + # Add agent to identity if not already attached + if agent not in identity.agents: + identity.agents.append(agent) + await identity.update_async(db_session=session, actor=actor) + + @enforce_types + @trace_method + @raise_on_invalid_id(param_name="identity_id", expected_prefix=PrimitiveType.IDENTITY) + @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT) + async def detach_agent_async(self, identity_id: str, agent_id: str, actor: PydanticUser) -> None: + """ + Detach an agent from an identity. + """ + async with db_registry.async_session() as session: + identity = await IdentityModel.read_async(db_session=session, identifier=identity_id, actor=actor) + + agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor) + + # Remove agent from identity if attached + if agent in identity.agents: + identity.agents.remove(agent) + await identity.update_async(db_session=session, actor=actor) + + @enforce_types + @trace_method + @raise_on_invalid_id(param_name="identity_id", expected_prefix=PrimitiveType.IDENTITY) + @raise_on_invalid_id(param_name="block_id", expected_prefix=PrimitiveType.BLOCK) + async def attach_block_async(self, identity_id: str, block_id: str, actor: PydanticUser) -> None: + """ + Attach a block to an identity. + """ + async with db_registry.async_session() as session: + identity = await IdentityModel.read_async(db_session=session, identifier=identity_id, actor=actor) + + block = await BlockModel.read_async(db_session=session, identifier=block_id, actor=actor) + + # Add block to identity if not already attached + if block not in identity.blocks: + identity.blocks.append(block) + await identity.update_async(db_session=session, actor=actor) + + @enforce_types + @trace_method + @raise_on_invalid_id(param_name="identity_id", expected_prefix=PrimitiveType.IDENTITY) + @raise_on_invalid_id(param_name="block_id", expected_prefix=PrimitiveType.BLOCK) + async def detach_block_async(self, identity_id: str, block_id: str, actor: PydanticUser) -> None: + """ + Detach a block from an identity. + """ + async with db_registry.async_session() as session: + identity = await IdentityModel.read_async(db_session=session, identifier=identity_id, actor=actor) + + block = await BlockModel.read_async(db_session=session, identifier=block_id, actor=actor) + + # Remove block from identity if attached + if block in identity.blocks: + identity.blocks.remove(block) + await identity.update_async(db_session=session, actor=actor)