letta-nightly 0.6.28.dev20250220163833__py3-none-any.whl → 0.6.29.dev20250221033538__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of letta-nightly might be problematic. Click here for more details.

@@ -1,21 +1,30 @@
1
1
  import asyncio
2
- from typing import TYPE_CHECKING, Iterable, List, Optional, Union, cast
2
+ from typing import TYPE_CHECKING, List, Optional, Union
3
3
 
4
+ import httpx
5
+ import openai
4
6
  from fastapi import APIRouter, Body, Depends, Header, HTTPException
5
7
  from fastapi.responses import StreamingResponse
6
- from openai.types.chat import ChatCompletionMessageParam
7
8
  from openai.types.chat.completion_create_params import CompletionCreateParams
9
+ from starlette.concurrency import run_in_threadpool
8
10
 
9
11
  from letta.agent import Agent
10
12
  from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
11
13
  from letta.log import get_logger
12
- from letta.schemas.message import MessageCreate
13
- from letta.schemas.openai.chat_completion_response import Message
14
+ from letta.schemas.message import Message, MessageCreate
14
15
  from letta.schemas.user import User
15
16
  from letta.server.rest_api.chat_completions_interface import ChatCompletionsStreamingInterface
16
17
 
17
18
  # TODO this belongs in a controller!
18
- from letta.server.rest_api.utils import get_letta_server, sse_async_generator
19
+ from letta.server.rest_api.utils import (
20
+ convert_letta_messages_to_openai,
21
+ create_assistant_message_from_openai_response,
22
+ create_user_message,
23
+ get_letta_server,
24
+ get_messages_from_completion_request,
25
+ sse_async_generator,
26
+ )
27
+ from letta.settings import model_settings
19
28
 
20
29
  if TYPE_CHECKING:
21
30
  from letta.server.server import SyncServer
@@ -25,6 +34,88 @@ router = APIRouter(prefix="/v1", tags=["chat_completions"])
25
34
  logger = get_logger(__name__)
26
35
 
27
36
 
37
+ @router.post(
38
+ "/fast/chat/completions",
39
+ response_model=None,
40
+ operation_id="create_fast_chat_completions",
41
+ responses={
42
+ 200: {
43
+ "description": "Successful response",
44
+ "content": {
45
+ "text/event-stream": {"description": "Server-Sent Events stream"},
46
+ },
47
+ }
48
+ },
49
+ )
50
+ async def create_fast_chat_completions(
51
+ completion_request: CompletionCreateParams = Body(...),
52
+ server: "SyncServer" = Depends(get_letta_server),
53
+ user_id: Optional[str] = Header(None, alias="user_id"),
54
+ ):
55
+ # TODO: This is necessary, we need to factor out CompletionCreateParams due to weird behavior
56
+ agent_id = str(completion_request.get("user", None))
57
+ if agent_id is None:
58
+ error_msg = "Must pass agent_id in the 'user' field"
59
+ logger.error(error_msg)
60
+ raise HTTPException(status_code=400, detail=error_msg)
61
+ model = completion_request.get("model")
62
+
63
+ actor = server.user_manager.get_user_or_default(user_id=user_id)
64
+ client = openai.AsyncClient(
65
+ api_key=model_settings.openai_api_key,
66
+ max_retries=0,
67
+ http_client=httpx.AsyncClient(
68
+ timeout=httpx.Timeout(connect=15.0, read=5.0, write=5.0, pool=5.0),
69
+ follow_redirects=True,
70
+ limits=httpx.Limits(
71
+ max_connections=50,
72
+ max_keepalive_connections=50,
73
+ keepalive_expiry=120,
74
+ ),
75
+ ),
76
+ )
77
+
78
+ # Magic message manipulating
79
+ input_message = get_messages_from_completion_request(completion_request)[-1]
80
+ completion_request.pop("messages")
81
+
82
+ # Get in context messages
83
+ in_context_messages = server.agent_manager.get_in_context_messages(agent_id=agent_id, actor=actor)
84
+ openai_dict_in_context_messages = convert_letta_messages_to_openai(in_context_messages)
85
+ openai_dict_in_context_messages.append(input_message)
86
+
87
+ async def event_stream():
88
+ # TODO: Factor this out into separate interface
89
+ response_accumulator = []
90
+
91
+ stream = await client.chat.completions.create(**completion_request, messages=openai_dict_in_context_messages)
92
+
93
+ async with stream:
94
+ async for chunk in stream:
95
+ if chunk.choices and chunk.choices[0].delta.content:
96
+ # TODO: This does not support tool calling right now
97
+ response_accumulator.append(chunk.choices[0].delta.content)
98
+ yield f"data: {chunk.model_dump_json()}\n\n"
99
+
100
+ # Construct messages
101
+ user_message = create_user_message(input_message=input_message, agent_id=agent_id, actor=actor)
102
+ assistant_message = create_assistant_message_from_openai_response(
103
+ response_text="".join(response_accumulator), agent_id=agent_id, model=str(model), actor=actor
104
+ )
105
+
106
+ # Persist both in one synchronous DB call, done in a threadpool
107
+ await run_in_threadpool(
108
+ server.agent_manager.append_to_in_context_messages,
109
+ [user_message, assistant_message],
110
+ agent_id=agent_id,
111
+ actor=actor,
112
+ )
113
+
114
+ yield "data: [DONE]\n\n"
115
+
116
+ return StreamingResponse(event_stream(), media_type="text/event-stream")
117
+
118
+
28
119
  @router.post(
29
120
  "/chat/completions",
30
121
  response_model=None,
@@ -44,26 +135,8 @@ async def create_chat_completions(
44
135
  user_id: Optional[str] = Header(None, alias="user_id"),
45
136
  ):
46
137
  # Validate and process fields
47
- try:
48
- messages = list(cast(Iterable[ChatCompletionMessageParam], completion_request["messages"]))
49
- except KeyError:
50
- # Handle the case where "messages" is not present in the request
51
- raise HTTPException(status_code=400, detail="The 'messages' field is missing in the request.")
52
- except TypeError:
53
- # Handle the case where "messages" is not iterable
54
- raise HTTPException(status_code=400, detail="The 'messages' field must be an iterable.")
55
- except Exception as e:
56
- # Catch any other unexpected errors and include the exception message
57
- raise HTTPException(status_code=400, detail=f"An error occurred while processing 'messages': {str(e)}")
58
-
59
- if messages[-1]["role"] != "user":
60
- logger.error(f"The last message does not have a `user` role: {messages}")
61
- raise HTTPException(status_code=400, detail="'messages[-1].role' must be a 'user'")
62
-
138
+ messages = get_messages_from_completion_request(completion_request)
63
139
  input_message = messages[-1]
64
- if not isinstance(input_message["content"], str):
65
- logger.error(f"The input message does not have valid content: {input_message}")
66
- raise HTTPException(status_code=400, detail="'messages[-1].content' must be a 'string'")
67
140
 
68
141
  # Process remaining fields
69
142
  if not completion_request["stream"]:
@@ -138,6 +211,7 @@ async def send_message_to_agent_chat_completions(
138
211
  agent_id=letta_agent.agent_state.id,
139
212
  messages=messages,
140
213
  interface=streaming_interface,
214
+ put_inner_thoughts_first=False,
141
215
  )
142
216
  )
143
217
 
@@ -1,3 +1,4 @@
1
+ import traceback
1
2
  from datetime import datetime
2
3
  from typing import Annotated, List, Optional
3
4
 
@@ -51,7 +52,7 @@ def list_agents(
51
52
  project_id: Optional[str] = Query(None, description="Search agents by project id"),
52
53
  template_id: Optional[str] = Query(None, description="Search agents by template id"),
53
54
  base_template_id: Optional[str] = Query(None, description="Search agents by base template id"),
54
- identifier_key: Optional[str] = Query(None, description="Search agents by identifier key"),
55
+ identifier_keys: Optional[List[str]] = Query(None, description="Search agents by identifier keys"),
55
56
  ):
56
57
  """
57
58
  List all agents associated with a given user.
@@ -67,7 +68,6 @@ def list_agents(
67
68
  "project_id": project_id,
68
69
  "template_id": template_id,
69
70
  "base_template_id": base_template_id,
70
- "identifier_key": identifier_key,
71
71
  }.items()
72
72
  if value is not None
73
73
  }
@@ -81,6 +81,7 @@ def list_agents(
81
81
  query_text=query_text,
82
82
  tags=tags,
83
83
  match_all_tags=match_all_tags,
84
+ identifier_keys=identifier_keys,
84
85
  **kwargs,
85
86
  )
86
87
  return agents
@@ -119,8 +120,12 @@ def create_agent(
119
120
  """
120
121
  Create a new agent with the specified configuration.
121
122
  """
122
- actor = server.user_manager.get_user_or_default(user_id=user_id)
123
- return server.create_agent(agent, actor=actor)
123
+ try:
124
+ actor = server.user_manager.get_user_or_default(user_id=user_id)
125
+ return server.create_agent(agent, actor=actor)
126
+ except Exception as e:
127
+ traceback.print_exc()
128
+ raise HTTPException(status_code=500, detail=str(e))
124
129
 
125
130
 
126
131
  @router.patch("/{agent_id}", response_model=AgentState, operation_id="modify_agent")
@@ -16,6 +16,7 @@ router = APIRouter(prefix="/identities", tags=["identities"])
16
16
  def list_identities(
17
17
  name: Optional[str] = Query(None),
18
18
  project_id: Optional[str] = Query(None),
19
+ identifier_key: Optional[str] = Query(None),
19
20
  identity_type: Optional[IdentityType] = Query(None),
20
21
  before: Optional[str] = Query(None),
21
22
  after: Optional[str] = Query(None),
@@ -30,7 +31,14 @@ def list_identities(
30
31
  actor = server.user_manager.get_user_or_default(user_id=user_id)
31
32
 
32
33
  identities = server.identity_manager.list_identities(
33
- name=name, project_id=project_id, identity_type=identity_type, before=before, after=after, limit=limit, actor=actor
34
+ name=name,
35
+ project_id=project_id,
36
+ identifier_key=identifier_key,
37
+ identity_type=identity_type,
38
+ before=before,
39
+ after=after,
40
+ limit=limit,
41
+ actor=actor,
34
42
  )
35
43
  except HTTPException:
36
44
  raise
@@ -39,13 +47,15 @@ def list_identities(
39
47
  return identities
40
48
 
41
49
 
42
- @router.get("/{identifier_key}", tags=["identities"], response_model=Identity, operation_id="get_identity_from_identifier_key")
50
+ @router.get("/{identity_id}", tags=["identities"], response_model=Identity, operation_id="retrieve_identity")
43
51
  def retrieve_identity(
44
- identifier_key: str,
52
+ identity_id: str,
45
53
  server: "SyncServer" = Depends(get_letta_server),
54
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
46
55
  ):
47
56
  try:
48
- return server.identity_manager.get_identity_from_identifier_key(identifier_key=identifier_key)
57
+ actor = server.user_manager.get_user_or_default(user_id=user_id)
58
+ return server.identity_manager.get_identity(identity_id=identity_id, actor=actor)
49
59
  except NoResultFound as e:
50
60
  raise HTTPException(status_code=404, detail=str(e))
51
61
 
@@ -82,25 +92,25 @@ def upsert_identity(
82
92
  raise HTTPException(status_code=500, detail=f"{e}")
83
93
 
84
94
 
85
- @router.patch("/{identifier_key}", tags=["identities"], response_model=Identity, operation_id="update_identity")
95
+ @router.patch("/{identity_id}", tags=["identities"], response_model=Identity, operation_id="update_identity")
86
96
  def modify_identity(
87
- identifier_key: str,
97
+ identity_id: str,
88
98
  identity: IdentityUpdate = Body(...),
89
99
  server: "SyncServer" = Depends(get_letta_server),
90
100
  user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
91
101
  ):
92
102
  try:
93
103
  actor = server.user_manager.get_user_or_default(user_id=user_id)
94
- return server.identity_manager.update_identity_by_key(identifier_key=identifier_key, identity=identity, actor=actor)
104
+ return server.identity_manager.update_identity(identity_id=identity_id, identity=identity, actor=actor)
95
105
  except HTTPException:
96
106
  raise
97
107
  except Exception as e:
98
108
  raise HTTPException(status_code=500, detail=f"{e}")
99
109
 
100
110
 
101
- @router.delete("/{identifier_key}", tags=["identities"], operation_id="delete_identity")
111
+ @router.delete("/{identity_id}", tags=["identities"], operation_id="delete_identity")
102
112
  def delete_identity(
103
- identifier_key: str,
113
+ identity_id: str,
104
114
  server: "SyncServer" = Depends(get_letta_server),
105
115
  user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
106
116
  ):
@@ -108,4 +118,4 @@ def delete_identity(
108
118
  Delete an identity by its identifier key
109
119
  """
110
120
  actor = server.user_manager.get_user_or_default(user_id=user_id)
111
- server.identity_manager.delete_identity_by_key(identifier_key=identifier_key, actor=actor)
121
+ server.identity_manager.delete_identity(identity_id=identity_id, actor=actor)
@@ -1,23 +1,33 @@
1
1
  import asyncio
2
2
  import json
3
3
  import os
4
+ import uuid
4
5
  import warnings
6
+ from datetime import datetime, timezone
5
7
  from enum import Enum
6
- from typing import TYPE_CHECKING, AsyncGenerator, Optional, Union
8
+ from typing import TYPE_CHECKING, AsyncGenerator, Dict, Iterable, List, Optional, Union, cast
7
9
 
8
- from fastapi import Header
10
+ import pytz
11
+ from fastapi import Header, HTTPException
12
+ from openai.types.chat import ChatCompletionMessageParam
13
+ from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall
14
+ from openai.types.chat.chat_completion_message_tool_call import Function as OpenAIFunction
15
+ from openai.types.chat.completion_create_params import CompletionCreateParams
9
16
  from pydantic import BaseModel
10
17
 
18
+ from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
11
19
  from letta.errors import ContextWindowExceededError, RateLimitExceededError
12
20
  from letta.log import get_logger
21
+ from letta.schemas.enums import MessageRole
22
+ from letta.schemas.letta_message import TextContent
23
+ from letta.schemas.message import Message
13
24
  from letta.schemas.usage import LettaUsageStatistics
25
+ from letta.schemas.user import User
14
26
  from letta.server.rest_api.interface import StreamingServerInterface
15
27
 
16
28
  if TYPE_CHECKING:
17
29
  from letta.server.server import SyncServer
18
30
 
19
- # from letta.orm.user import User
20
- # from letta.orm.utilities import get_db_session
21
31
 
22
32
  SSE_PREFIX = "data: "
23
33
  SSE_SUFFIX = "\n\n"
@@ -128,3 +138,172 @@ def log_error_to_sentry(e):
128
138
  import sentry_sdk
129
139
 
130
140
  sentry_sdk.capture_exception(e)
141
+
142
+
143
+ def create_user_message(input_message: dict, agent_id: str, actor: User) -> Message:
144
+ """
145
+ Converts a user input message into the internal structured format.
146
+ """
147
+ # Generate timestamp in the correct format
148
+ now = datetime.now(pytz.timezone("US/Pacific")).strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
149
+
150
+ # Format message as structured JSON
151
+ structured_message = {"type": "user_message", "message": input_message["content"], "time": now}
152
+
153
+ # Construct the Message object
154
+ user_message = Message(
155
+ id=f"message-{uuid.uuid4()}",
156
+ role=MessageRole.user,
157
+ content=[TextContent(text=json.dumps(structured_message, indent=2))], # Store structured JSON
158
+ organization_id=actor.organization_id,
159
+ agent_id=agent_id,
160
+ model=None,
161
+ tool_calls=None,
162
+ tool_call_id=None,
163
+ created_at=datetime.now(timezone.utc),
164
+ )
165
+
166
+ return user_message
167
+
168
+
169
+ def create_assistant_message_from_openai_response(
170
+ response_text: str,
171
+ agent_id: str,
172
+ model: str,
173
+ actor: User,
174
+ ) -> Message:
175
+ """
176
+ Converts an OpenAI response into a Message that follows the internal
177
+ paradigm where LLM responses are structured as tool calls instead of content.
178
+ """
179
+ tool_call_id = str(uuid.uuid4())
180
+
181
+ # Construct the tool call with the assistant's message
182
+ tool_call = OpenAIToolCall(
183
+ id=tool_call_id,
184
+ function=OpenAIFunction(
185
+ name=DEFAULT_MESSAGE_TOOL,
186
+ arguments='{\n "' + DEFAULT_MESSAGE_TOOL_KWARG + '": ' + f'"{response_text}",\n "request_heartbeat": true\n' + "}",
187
+ ),
188
+ type="function",
189
+ )
190
+
191
+ # Construct the Message object
192
+ assistant_message = Message(
193
+ id=f"message-{uuid.uuid4()}",
194
+ role=MessageRole.assistant,
195
+ content=[],
196
+ organization_id=actor.organization_id,
197
+ agent_id=agent_id,
198
+ model=model,
199
+ tool_calls=[tool_call],
200
+ tool_call_id=None,
201
+ created_at=datetime.now(timezone.utc),
202
+ )
203
+
204
+ return assistant_message
205
+
206
+
207
+ def convert_letta_messages_to_openai(messages: List[Message]) -> List[dict]:
208
+ """
209
+ Flattens Letta's messages (with system, user, assistant, tool roles, etc.)
210
+ into standard OpenAI chat messages (system, user, assistant).
211
+
212
+ Transformation rules:
213
+ 1. Assistant + send_message tool_call => content = tool_call's "message"
214
+ 2. Tool (role=tool) referencing send_message => skip
215
+ 3. User messages might store actual text inside JSON => parse that into content
216
+ 4. System => pass through as normal
217
+ """
218
+
219
+ openai_messages = []
220
+
221
+ for msg in messages:
222
+ # 1. Assistant + 'send_message' tool_calls => flatten
223
+ if msg.role == MessageRole.assistant and msg.tool_calls:
224
+ # Find any 'send_message' tool_calls
225
+ send_message_calls = [tc for tc in msg.tool_calls if tc.function.name == "send_message"]
226
+ if send_message_calls:
227
+ # If we have multiple calls, just pick the first or merge them
228
+ # Typically there's only one.
229
+ tc = send_message_calls[0]
230
+ arguments = json.loads(tc.function.arguments)
231
+ # Extract the "message" string
232
+ extracted_text = arguments.get("message", "")
233
+
234
+ # Create a new content with the extracted text
235
+ msg = Message(
236
+ id=msg.id,
237
+ role=msg.role,
238
+ content=[TextContent(text=extracted_text)],
239
+ organization_id=msg.organization_id,
240
+ agent_id=msg.agent_id,
241
+ model=msg.model,
242
+ name=msg.name,
243
+ tool_calls=None, # no longer needed
244
+ tool_call_id=None,
245
+ created_at=msg.created_at,
246
+ )
247
+
248
+ # 2. If role=tool and it's referencing send_message => skip
249
+ if msg.role == MessageRole.tool and msg.name == "send_message":
250
+ # Usually 'tool' messages with `send_message` are just status/OK messages
251
+ # that OpenAI doesn't need to see. So skip them.
252
+ continue
253
+
254
+ # 3. User messages might store text in JSON => parse it
255
+ if msg.role == MessageRole.user:
256
+ # Example: content=[TextContent(text='{"type": "user_message","message":"Hello"}')]
257
+ # Attempt to parse JSON and extract "message"
258
+ if msg.content and msg.content[0].text.strip().startswith("{"):
259
+ try:
260
+ parsed = json.loads(msg.content[0].text)
261
+ # If there's a "message" field, use that as the content
262
+ if "message" in parsed:
263
+ actual_user_text = parsed["message"]
264
+ msg = Message(
265
+ id=msg.id,
266
+ role=msg.role,
267
+ content=[TextContent(text=actual_user_text)],
268
+ organization_id=msg.organization_id,
269
+ agent_id=msg.agent_id,
270
+ model=msg.model,
271
+ name=msg.name,
272
+ tool_calls=msg.tool_calls,
273
+ tool_call_id=msg.tool_call_id,
274
+ created_at=msg.created_at,
275
+ )
276
+ except json.JSONDecodeError:
277
+ pass # It's not JSON, leave as-is
278
+
279
+ # 4. System is left as-is (or any other role that doesn't need special handling)
280
+ #
281
+ # Finally, convert to dict using your existing method
282
+ openai_messages.append(msg.to_openai_dict())
283
+
284
+ return openai_messages
285
+
286
+
287
+ def get_messages_from_completion_request(completion_request: CompletionCreateParams) -> List[Dict]:
288
+ try:
289
+ messages = list(cast(Iterable[ChatCompletionMessageParam], completion_request["messages"]))
290
+ except KeyError:
291
+ # Handle the case where "messages" is not present in the request
292
+ raise HTTPException(status_code=400, detail="The 'messages' field is missing in the request.")
293
+ except TypeError:
294
+ # Handle the case where "messages" is not iterable
295
+ raise HTTPException(status_code=400, detail="The 'messages' field must be an iterable.")
296
+ except Exception as e:
297
+ # Catch any other unexpected errors and include the exception message
298
+ raise HTTPException(status_code=400, detail=f"An error occurred while processing 'messages': {str(e)}")
299
+
300
+ if messages[-1]["role"] != "user":
301
+ logger.error(f"The last message does not have a `user` role: {messages}")
302
+ raise HTTPException(status_code=400, detail="'messages[-1].role' must be a 'user'")
303
+
304
+ input_message = messages[-1]
305
+ if not isinstance(input_message["content"], str):
306
+ logger.error(f"The input message does not have valid content: {input_message}")
307
+ raise HTTPException(status_code=400, detail="'messages[-1].content' must be a 'string'")
308
+
309
+ return messages
letta/server/server.py CHANGED
@@ -336,6 +336,7 @@ class SyncServer(Server):
336
336
  agent_id: str,
337
337
  input_messages: Union[Message, List[Message]],
338
338
  interface: Union[AgentInterface, None] = None, # needed to getting responses
339
+ put_inner_thoughts_first: bool = True,
339
340
  # timestamp: Optional[datetime],
340
341
  ) -> LettaUsageStatistics:
341
342
  """Send the input message through the agent"""
@@ -368,6 +369,7 @@ class SyncServer(Server):
368
369
  stream=token_streaming,
369
370
  skip_verify=True,
370
371
  metadata=metadata,
372
+ put_inner_thoughts_first=put_inner_thoughts_first,
371
373
  )
372
374
 
373
375
  except Exception as e:
@@ -625,6 +627,7 @@ class SyncServer(Server):
625
627
  wrap_system_message: bool = True,
626
628
  interface: Union[AgentInterface, ChatCompletionsStreamingInterface, None] = None, # needed to getting responses
627
629
  metadata: Optional[dict] = None, # Pass through metadata to interface
630
+ put_inner_thoughts_first: bool = True,
628
631
  ) -> LettaUsageStatistics:
629
632
  """Send a list of messages to the agent
630
633
 
@@ -675,7 +678,13 @@ class SyncServer(Server):
675
678
  interface.metadata = metadata
676
679
 
677
680
  # Run the agent state forward
678
- return self._step(actor=actor, agent_id=agent_id, input_messages=message_objects, interface=interface)
681
+ return self._step(
682
+ actor=actor,
683
+ agent_id=agent_id,
684
+ input_messages=message_objects,
685
+ interface=interface,
686
+ put_inner_thoughts_first=put_inner_thoughts_first,
687
+ )
679
688
 
680
689
  # @LockingServer.agent_lock_decorator
681
690
  def run_command(self, user_id: str, agent_id: str, command: str) -> LettaUsageStatistics:
@@ -11,6 +11,7 @@ from letta.log import get_logger
11
11
  from letta.orm import Agent as AgentModel
12
12
  from letta.orm import AgentPassage, AgentsTags
13
13
  from letta.orm import Block as BlockModel
14
+ from letta.orm import Identity as IdentityModel
14
15
  from letta.orm import Source as SourceModel
15
16
  from letta.orm import SourcePassage, SourcesAgents
16
17
  from letta.orm import Tool as ToolModel
@@ -34,7 +35,6 @@ from letta.schemas.user import User as PydanticUser
34
35
  from letta.serialize_schemas import SerializedAgentSchema
35
36
  from letta.services.block_manager import BlockManager
36
37
  from letta.services.helpers.agent_manager_helper import (
37
- _process_identity,
38
38
  _process_relationship,
39
39
  _process_tags,
40
40
  check_supports_structured_output,
@@ -138,6 +138,7 @@ class AgentManager:
138
138
  tool_ids=tool_ids,
139
139
  source_ids=agent_create.source_ids or [],
140
140
  tags=agent_create.tags or [],
141
+ identity_ids=agent_create.identity_ids or [],
141
142
  description=agent_create.description,
142
143
  metadata=agent_create.metadata,
143
144
  tool_rules=tool_rules,
@@ -145,7 +146,6 @@ class AgentManager:
145
146
  project_id=agent_create.project_id,
146
147
  template_id=agent_create.template_id,
147
148
  base_template_id=agent_create.base_template_id,
148
- identifier_key=agent_create.identifier_key,
149
149
  message_buffer_autoclear=agent_create.message_buffer_autoclear,
150
150
  )
151
151
 
@@ -203,13 +203,13 @@ class AgentManager:
203
203
  tool_ids: List[str],
204
204
  source_ids: List[str],
205
205
  tags: List[str],
206
+ identity_ids: List[str],
206
207
  description: Optional[str] = None,
207
208
  metadata: Optional[Dict] = None,
208
209
  tool_rules: Optional[List[PydanticToolRule]] = None,
209
210
  project_id: Optional[str] = None,
210
211
  template_id: Optional[str] = None,
211
212
  base_template_id: Optional[str] = None,
212
- identifier_key: Optional[str] = None,
213
213
  message_buffer_autoclear: bool = False,
214
214
  ) -> PydanticAgentState:
215
215
  """Create a new agent."""
@@ -237,9 +237,7 @@ class AgentManager:
237
237
  _process_relationship(session, new_agent, "sources", SourceModel, source_ids, replace=True)
238
238
  _process_relationship(session, new_agent, "core_memory", BlockModel, block_ids, replace=True)
239
239
  _process_tags(new_agent, tags, replace=True)
240
- if identifier_key is not None:
241
- identity = self.identity_manager.get_identity_from_identifier_key(identifier_key)
242
- _process_identity(new_agent, identifier_key, identity)
240
+ _process_relationship(session, new_agent, "identities", IdentityModel, identity_ids, replace=True)
243
241
 
244
242
  new_agent.create(session, actor=actor)
245
243
 
@@ -313,9 +311,8 @@ class AgentManager:
313
311
  _process_relationship(session, agent, "core_memory", BlockModel, agent_update.block_ids, replace=True)
314
312
  if agent_update.tags is not None:
315
313
  _process_tags(agent, agent_update.tags, replace=True)
316
- if agent_update.identifier_key is not None:
317
- identity = self.identity_manager.get_identity_from_identifier_key(agent_update.identifier_key)
318
- _process_identity(agent, agent_update.identifier_key, identity)
314
+ if agent_update.identity_ids is not None:
315
+ _process_relationship(session, agent, "identities", IdentityModel, agent_update.identity_ids, replace=True)
319
316
 
320
317
  # Commit and refresh the agent
321
318
  agent.update(session, actor=actor)
@@ -333,6 +330,7 @@ class AgentManager:
333
330
  tags: Optional[List[str]] = None,
334
331
  match_all_tags: bool = False,
335
332
  query_text: Optional[str] = None,
333
+ identifier_keys: Optional[List[str]] = None,
336
334
  **kwargs,
337
335
  ) -> List[PydanticAgentState]:
338
336
  """
@@ -348,6 +346,7 @@ class AgentManager:
348
346
  match_all_tags=match_all_tags,
349
347
  organization_id=actor.organization_id if actor else None,
350
348
  query_text=query_text,
349
+ identifier_keys=identifier_keys,
351
350
  **kwargs,
352
351
  )
353
352
 
@@ -11,7 +11,6 @@ from letta.orm.errors import NoResultFound
11
11
  from letta.prompts import gpt_system
12
12
  from letta.schemas.agent import AgentState, AgentType
13
13
  from letta.schemas.enums import MessageRole
14
- from letta.schemas.identity import Identity
15
14
  from letta.schemas.memory import Memory
16
15
  from letta.schemas.message import Message, MessageCreate, TextContent
17
16
  from letta.schemas.tool_rule import ToolRule
@@ -85,20 +84,6 @@ def _process_tags(agent: AgentModel, tags: List[str], replace=True):
85
84
  agent.tags.extend([tag for tag in new_tags if tag.tag not in existing_tags])
86
85
 
87
86
 
88
- def _process_identity(agent: AgentModel, identifier_key: str, identity: Identity):
89
- """
90
- Handles identity for an agent.
91
-
92
- Args:
93
- agent: The AgentModel instance.
94
- identifier_key: The identifier key of the identity to set or update.
95
- identity: The Identity object to set or update.
96
- """
97
- agent.identifier_key = identifier_key
98
- agent.identity = identity
99
- agent.identity_id = identity.id
100
-
101
-
102
87
  def derive_system_message(agent_type: AgentType, system: Optional[str] = None):
103
88
  if system is None:
104
89
  # TODO: don't hardcode