letta-nightly 0.7.0.dev20250423003112__py3-none-any.whl → 0.7.2.dev20250423222439__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.
Files changed (55) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +113 -81
  3. letta/agents/letta_agent.py +2 -2
  4. letta/agents/letta_agent_batch.py +38 -34
  5. letta/client/client.py +10 -2
  6. letta/constants.py +4 -3
  7. letta/functions/function_sets/multi_agent.py +1 -3
  8. letta/functions/helpers.py +3 -3
  9. letta/groups/dynamic_multi_agent.py +58 -59
  10. letta/groups/round_robin_multi_agent.py +43 -49
  11. letta/groups/sleeptime_multi_agent.py +28 -18
  12. letta/groups/supervisor_multi_agent.py +21 -20
  13. letta/helpers/composio_helpers.py +1 -1
  14. letta/helpers/converters.py +29 -0
  15. letta/helpers/datetime_helpers.py +9 -0
  16. letta/helpers/message_helper.py +1 -0
  17. letta/helpers/tool_execution_helper.py +3 -3
  18. letta/jobs/llm_batch_job_polling.py +2 -1
  19. letta/llm_api/anthropic.py +10 -6
  20. letta/llm_api/anthropic_client.py +2 -2
  21. letta/llm_api/cohere.py +2 -2
  22. letta/llm_api/google_ai_client.py +2 -2
  23. letta/llm_api/google_vertex_client.py +2 -2
  24. letta/llm_api/openai.py +11 -4
  25. letta/llm_api/openai_client.py +34 -2
  26. letta/local_llm/chat_completion_proxy.py +2 -2
  27. letta/orm/agent.py +8 -1
  28. letta/orm/custom_columns.py +15 -0
  29. letta/schemas/agent.py +6 -0
  30. letta/schemas/letta_message_content.py +2 -1
  31. letta/schemas/llm_config.py +12 -2
  32. letta/schemas/message.py +18 -0
  33. letta/schemas/openai/chat_completion_response.py +52 -3
  34. letta/schemas/response_format.py +78 -0
  35. letta/schemas/tool_execution_result.py +14 -0
  36. letta/server/rest_api/chat_completions_interface.py +2 -2
  37. letta/server/rest_api/interface.py +3 -2
  38. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +1 -1
  39. letta/server/rest_api/routers/v1/agents.py +4 -4
  40. letta/server/rest_api/routers/v1/groups.py +2 -2
  41. letta/server/rest_api/routers/v1/messages.py +41 -19
  42. letta/server/server.py +24 -57
  43. letta/services/agent_manager.py +6 -1
  44. letta/services/llm_batch_manager.py +28 -26
  45. letta/services/tool_executor/tool_execution_manager.py +37 -28
  46. letta/services/tool_executor/tool_execution_sandbox.py +35 -16
  47. letta/services/tool_executor/tool_executor.py +299 -68
  48. letta/services/tool_sandbox/base.py +3 -2
  49. letta/services/tool_sandbox/e2b_sandbox.py +5 -4
  50. letta/services/tool_sandbox/local_sandbox.py +11 -6
  51. {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/METADATA +1 -1
  52. {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/RECORD +55 -53
  53. {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/LICENSE +0 -0
  54. {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/WHEEL +0 -0
  55. {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/entry_points.txt +0 -0
@@ -39,9 +39,10 @@ class Message(BaseModel):
39
39
  tool_calls: Optional[List[ToolCall]] = None
40
40
  role: str
41
41
  function_call: Optional[FunctionCall] = None # Deprecated
42
- reasoning_content: Optional[str] = None # Used in newer reasoning APIs
42
+ reasoning_content: Optional[str] = None # Used in newer reasoning APIs, e.g. DeepSeek
43
43
  reasoning_content_signature: Optional[str] = None # NOTE: for Anthropic
44
44
  redacted_reasoning_content: Optional[str] = None # NOTE: for Anthropic
45
+ ommitted_reasoning_content: bool = False # NOTE: for OpenAI o1/o3
45
46
 
46
47
 
47
48
  class Choice(BaseModel):
@@ -52,16 +53,64 @@ class Choice(BaseModel):
52
53
  seed: Optional[int] = None # found in TogetherAI
53
54
 
54
55
 
56
+ class UsageStatisticsPromptTokenDetails(BaseModel):
57
+ cached_tokens: int = 0
58
+ # NOTE: OAI specific
59
+ # audio_tokens: int = 0
60
+
61
+ def __add__(self, other: "UsageStatisticsPromptTokenDetails") -> "UsageStatisticsPromptTokenDetails":
62
+ return UsageStatisticsPromptTokenDetails(
63
+ cached_tokens=self.cached_tokens + other.cached_tokens,
64
+ )
65
+
66
+
67
+ class UsageStatisticsCompletionTokenDetails(BaseModel):
68
+ reasoning_tokens: int = 0
69
+ # NOTE: OAI specific
70
+ # audio_tokens: int = 0
71
+ # accepted_prediction_tokens: int = 0
72
+ # rejected_prediction_tokens: int = 0
73
+
74
+ def __add__(self, other: "UsageStatisticsCompletionTokenDetails") -> "UsageStatisticsCompletionTokenDetails":
75
+ return UsageStatisticsCompletionTokenDetails(
76
+ reasoning_tokens=self.reasoning_tokens + other.reasoning_tokens,
77
+ )
78
+
79
+
55
80
  class UsageStatistics(BaseModel):
56
81
  completion_tokens: int = 0
57
82
  prompt_tokens: int = 0
58
83
  total_tokens: int = 0
59
84
 
85
+ prompt_tokens_details: Optional[UsageStatisticsPromptTokenDetails] = None
86
+ completion_tokens_details: Optional[UsageStatisticsCompletionTokenDetails] = None
87
+
60
88
  def __add__(self, other: "UsageStatistics") -> "UsageStatistics":
89
+
90
+ if self.prompt_tokens_details is None and other.prompt_tokens_details is None:
91
+ total_prompt_tokens_details = None
92
+ elif self.prompt_tokens_details is None:
93
+ total_prompt_tokens_details = other.prompt_tokens_details
94
+ elif other.prompt_tokens_details is None:
95
+ total_prompt_tokens_details = self.prompt_tokens_details
96
+ else:
97
+ total_prompt_tokens_details = self.prompt_tokens_details + other.prompt_tokens_details
98
+
99
+ if self.completion_tokens_details is None and other.completion_tokens_details is None:
100
+ total_completion_tokens_details = None
101
+ elif self.completion_tokens_details is None:
102
+ total_completion_tokens_details = other.completion_tokens_details
103
+ elif other.completion_tokens_details is None:
104
+ total_completion_tokens_details = self.completion_tokens_details
105
+ else:
106
+ total_completion_tokens_details = self.completion_tokens_details + other.completion_tokens_details
107
+
61
108
  return UsageStatistics(
62
109
  completion_tokens=self.completion_tokens + other.completion_tokens,
63
110
  prompt_tokens=self.prompt_tokens + other.prompt_tokens,
64
111
  total_tokens=self.total_tokens + other.total_tokens,
112
+ prompt_tokens_details=total_prompt_tokens_details,
113
+ completion_tokens_details=total_completion_tokens_details,
65
114
  )
66
115
 
67
116
 
@@ -70,7 +119,7 @@ class ChatCompletionResponse(BaseModel):
70
119
 
71
120
  id: str
72
121
  choices: List[Choice]
73
- created: datetime.datetime
122
+ created: Union[datetime.datetime, int]
74
123
  model: Optional[str] = None # NOTE: this is not consistent with OpenAI API standard, however is necessary to support local LLMs
75
124
  # system_fingerprint: str # docs say this is mandatory, but in reality API returns None
76
125
  system_fingerprint: Optional[str] = None
@@ -138,7 +187,7 @@ class ChatCompletionChunkResponse(BaseModel):
138
187
 
139
188
  id: str
140
189
  choices: List[ChunkChoice]
141
- created: Union[datetime.datetime, str]
190
+ created: Union[datetime.datetime, int]
142
191
  model: str
143
192
  # system_fingerprint: str # docs say this is mandatory, but in reality API returns None
144
193
  system_fingerprint: Optional[str] = None
@@ -0,0 +1,78 @@
1
+ from enum import Enum
2
+ from typing import Annotated, Any, Dict, Literal, Union
3
+
4
+ from pydantic import BaseModel, Field, validator
5
+
6
+
7
+ class ResponseFormatType(str, Enum):
8
+ """Enum defining the possible response format types."""
9
+
10
+ text = "text"
11
+ json_schema = "json_schema"
12
+ json_object = "json_object"
13
+
14
+
15
+ class ResponseFormat(BaseModel):
16
+ """Base class for all response formats."""
17
+
18
+ type: ResponseFormatType = Field(
19
+ ...,
20
+ description="The type of the response format.",
21
+ # why use this?
22
+ example=ResponseFormatType.text,
23
+ )
24
+
25
+
26
+ # ---------------------
27
+ # Response Format Types
28
+ # ---------------------
29
+
30
+ # SQLAlchemy type for database mapping
31
+ ResponseFormatDict = Dict[str, Any]
32
+
33
+
34
+ class TextResponseFormat(ResponseFormat):
35
+ """Response format for plain text responses."""
36
+
37
+ type: Literal[ResponseFormatType.text] = Field(
38
+ ResponseFormatType.text,
39
+ description="The type of the response format.",
40
+ )
41
+
42
+
43
+ class JsonSchemaResponseFormat(ResponseFormat):
44
+ """Response format for JSON schema-based responses."""
45
+
46
+ type: Literal[ResponseFormatType.json_schema] = Field(
47
+ ResponseFormatType.json_schema,
48
+ description="The type of the response format.",
49
+ )
50
+ json_schema: Dict[str, Any] = Field(
51
+ ...,
52
+ description="The JSON schema of the response.",
53
+ )
54
+
55
+ @validator("json_schema")
56
+ def validate_json_schema(cls, v: Dict[str, Any]) -> Dict[str, Any]:
57
+ """Validate that the provided schema is a valid JSON schema."""
58
+ if not isinstance(v, dict):
59
+ raise ValueError("JSON schema must be a dictionary")
60
+ if "schema" not in v:
61
+ raise ValueError("JSON schema should include a $schema property")
62
+ return v
63
+
64
+
65
+ class JsonObjectResponseFormat(ResponseFormat):
66
+ """Response format for JSON object responses."""
67
+
68
+ type: Literal[ResponseFormatType.json_object] = Field(
69
+ ResponseFormatType.json_object,
70
+ description="The type of the response format.",
71
+ )
72
+
73
+
74
+ # Pydantic type for validation
75
+ ResponseFormatUnion = Annotated[
76
+ Union[TextResponseFormat | JsonSchemaResponseFormat | JsonObjectResponseFormat],
77
+ Field(discriminator="type"),
78
+ ]
@@ -0,0 +1,14 @@
1
+ from typing import Any, List, Literal, Optional
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from letta.schemas.agent import AgentState
6
+
7
+
8
+ class ToolExecutionResult(BaseModel):
9
+ status: Literal["success", "error"] = Field(..., description="The status of the tool execution and return object")
10
+ func_return: Optional[Any] = Field(None, description="The function return object")
11
+ agent_state: Optional[AgentState] = Field(None, description="The agent state")
12
+ stdout: Optional[List[str]] = Field(None, description="Captured stdout (prints, logs) from function invocation")
13
+ stderr: Optional[List[str]] = Field(None, description="Captured stderr from the function invocation")
14
+ sandbox_config_fingerprint: Optional[str] = Field(None, description="The fingerprint of the config for the sandbox")
@@ -238,7 +238,7 @@ class ChatCompletionsStreamingInterface(AgentChunkStreamingInterface):
238
238
  return ChatCompletionChunk(
239
239
  id=chunk.id,
240
240
  object=chunk.object,
241
- created=chunk.created.timestamp(),
241
+ created=chunk.created,
242
242
  model=chunk.model,
243
243
  choices=[
244
244
  Choice(
@@ -256,7 +256,7 @@ class ChatCompletionsStreamingInterface(AgentChunkStreamingInterface):
256
256
  return ChatCompletionChunk(
257
257
  id=chunk.id,
258
258
  object=chunk.object,
259
- created=chunk.created.timestamp(),
259
+ created=chunk.created,
260
260
  model=chunk.model,
261
261
  choices=[
262
262
  Choice(
@@ -1001,7 +1001,7 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
1001
1001
  # Example case that would trigger here:
1002
1002
  # id='chatcmpl-AKtUvREgRRvgTW6n8ZafiKuV0mxhQ'
1003
1003
  # choices=[ChunkChoice(finish_reason=None, index=0, delta=MessageDelta(content=None, tool_calls=None, function_call=None), logprobs=None)]
1004
- # created=datetime.datetime(2024, 10, 21, 20, 40, 57, tzinfo=TzInfo(UTC))
1004
+ # created=1713216662
1005
1005
  # model='gpt-4o-mini-2024-07-18'
1006
1006
  # object='chat.completion.chunk'
1007
1007
  warnings.warn(f"Couldn't find delta in chunk: {chunk}")
@@ -1240,10 +1240,11 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
1240
1240
  and function_call.function.name == self.assistant_message_tool_name
1241
1241
  and self.assistant_message_tool_kwarg in func_args
1242
1242
  ):
1243
+ # Coerce content to `str` in cases where it's a JSON due to `response_format` being a JSON
1243
1244
  processed_chunk = AssistantMessage(
1244
1245
  id=msg_obj.id,
1245
1246
  date=msg_obj.created_at,
1246
- content=func_args[self.assistant_message_tool_kwarg],
1247
+ content=str(func_args[self.assistant_message_tool_kwarg]),
1247
1248
  name=msg_obj.name,
1248
1249
  otid=Message.generate_otid_from_id(msg_obj.id, chunk_index) if chunk_index is not None else None,
1249
1250
  )
@@ -111,7 +111,7 @@ async def send_message_to_agent_chat_completions(
111
111
  server.send_messages,
112
112
  actor=actor,
113
113
  agent_id=letta_agent.agent_state.id,
114
- messages=messages,
114
+ input_messages=messages,
115
115
  interface=streaming_interface,
116
116
  put_inner_thoughts_first=False,
117
117
  )
@@ -412,7 +412,7 @@ def list_blocks(
412
412
  """
413
413
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
414
414
  try:
415
- agent = server.agent_manager.get_agent_by_id(agent_id, actor=actor)
415
+ agent = server.agent_manager.get_agent_by_id(agent_id, actor)
416
416
  return agent.memory.blocks
417
417
  except NoResultFound as e:
418
418
  raise HTTPException(status_code=404, detail=str(e))
@@ -640,7 +640,7 @@ async def send_message(
640
640
  result = await server.send_message_to_agent(
641
641
  agent_id=agent_id,
642
642
  actor=actor,
643
- messages=request.messages,
643
+ input_messages=request.messages,
644
644
  stream_steps=False,
645
645
  stream_tokens=False,
646
646
  # Support for AssistantMessage
@@ -703,7 +703,7 @@ async def send_message_streaming(
703
703
  result = await server.send_message_to_agent(
704
704
  agent_id=agent_id,
705
705
  actor=actor,
706
- messages=request.messages,
706
+ input_messages=request.messages,
707
707
  stream_steps=True,
708
708
  stream_tokens=request.stream_tokens,
709
709
  # Support for AssistantMessage
@@ -730,7 +730,7 @@ async def process_message_background(
730
730
  result = await server.send_message_to_agent(
731
731
  agent_id=agent_id,
732
732
  actor=actor,
733
- messages=messages,
733
+ input_messages=messages,
734
734
  stream_steps=False, # NOTE(matt)
735
735
  stream_tokens=False,
736
736
  use_assistant_message=use_assistant_message,
@@ -128,7 +128,7 @@ async def send_group_message(
128
128
  result = await server.send_group_message_to_agent(
129
129
  group_id=group_id,
130
130
  actor=actor,
131
- messages=request.messages,
131
+ input_messages=request.messages,
132
132
  stream_steps=False,
133
133
  stream_tokens=False,
134
134
  # Support for AssistantMessage
@@ -167,7 +167,7 @@ async def send_group_message_streaming(
167
167
  result = await server.send_group_message_to_agent(
168
168
  group_id=group_id,
169
169
  actor=actor,
170
- messages=request.messages,
170
+ input_messages=request.messages,
171
171
  stream_steps=True,
172
172
  stream_tokens=request.stream_tokens,
173
173
  # Support for AssistantMessage
@@ -1,16 +1,17 @@
1
1
  from typing import List, Optional
2
2
 
3
- from fastapi import APIRouter, Body, Depends, Header
3
+ from fastapi import APIRouter, Body, Depends, Header, status
4
4
  from fastapi.exceptions import HTTPException
5
5
  from starlette.requests import Request
6
6
 
7
7
  from letta.agents.letta_agent_batch import LettaAgentBatch
8
8
  from letta.log import get_logger
9
9
  from letta.orm.errors import NoResultFound
10
- from letta.schemas.job import BatchJob, JobStatus, JobType
10
+ from letta.schemas.job import BatchJob, JobStatus, JobType, JobUpdate
11
11
  from letta.schemas.letta_request import CreateBatch
12
12
  from letta.server.rest_api.utils import get_letta_server
13
13
  from letta.server.server import SyncServer
14
+ from letta.settings import settings
14
15
 
15
16
  router = APIRouter(prefix="/messages", tags=["messages"])
16
17
 
@@ -43,19 +44,26 @@ async def create_messages_batch(
43
44
  if length > max_bytes:
44
45
  raise HTTPException(status_code=413, detail=f"Request too large ({length} bytes). Max is {max_bytes} bytes.")
45
46
 
46
- try:
47
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
48
-
49
- # Create a new job
50
- batch_job = BatchJob(
51
- user_id=actor.id,
52
- status=JobStatus.created,
53
- metadata={
54
- "job_type": "batch_messages",
55
- },
56
- callback_url=str(payload.callback_url),
47
+ # Reject request if env var is not set
48
+ if not settings.enable_batch_job_polling:
49
+ raise HTTPException(
50
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
51
+ detail=f"Server misconfiguration: LETTA_ENABLE_BATCH_JOB_POLLING is set to False.",
57
52
  )
58
53
 
54
+ actor = server.user_manager.get_user_or_default(user_id=actor_id)
55
+ batch_job = BatchJob(
56
+ user_id=actor.id,
57
+ status=JobStatus.running,
58
+ metadata={
59
+ "job_type": "batch_messages",
60
+ },
61
+ callback_url=str(payload.callback_url),
62
+ )
63
+
64
+ try:
65
+ batch_job = server.job_manager.create_job(pydantic_job=batch_job, actor=actor)
66
+
59
67
  # create the batch runner
60
68
  batch_runner = LettaAgentBatch(
61
69
  message_manager=server.message_manager,
@@ -67,14 +75,17 @@ async def create_messages_batch(
67
75
  job_manager=server.job_manager,
68
76
  actor=actor,
69
77
  )
70
- llm_batch_job = await batch_runner.step_until_request(batch_requests=payload.requests, letta_batch_job_id=batch_job.id)
78
+ await batch_runner.step_until_request(batch_requests=payload.requests, letta_batch_job_id=batch_job.id)
71
79
 
72
80
  # TODO: update run metadata
73
- batch_job = server.job_manager.create_job(pydantic_job=batch_job, actor=actor)
74
- except Exception:
81
+ except Exception as e:
75
82
  import traceback
76
83
 
84
+ print("Error creating batch job", e)
77
85
  traceback.print_exc()
86
+
87
+ # mark job as failed
88
+ server.job_manager.update_job_by_id(job_id=batch_job.id, job=BatchJob(status=JobStatus.failed), actor=actor)
78
89
  raise
79
90
  return batch_job
80
91
 
@@ -125,8 +136,19 @@ async def cancel_batch_run(
125
136
 
126
137
  try:
127
138
  job = server.job_manager.get_job_by_id(job_id=batch_id, actor=actor)
128
- job.status = JobStatus.cancelled
129
- server.job_manager.update_job_by_id(job_id=job, job=job)
130
- # TODO: actually cancel it
139
+ job = server.job_manager.update_job_by_id(job_id=job.id, job_update=JobUpdate(status=JobStatus.cancelled), actor=actor)
140
+
141
+ # Get related llm batch jobs
142
+ llm_batch_jobs = server.batch_manager.list_llm_batch_jobs(letta_batch_id=job.id, actor=actor)
143
+ for llm_batch_job in llm_batch_jobs:
144
+ if llm_batch_job.status in {JobStatus.running, JobStatus.created}:
145
+ # TODO: Extend to providers beyond anthropic
146
+ # TODO: For now, we only support anthropic
147
+ # Cancel the job
148
+ anthropic_batch_id = llm_batch_job.create_batch_response.id
149
+ await server.anthropic_async_client.messages.batches.cancel(anthropic_batch_id)
150
+
151
+ # Update all the batch_job statuses
152
+ server.batch_manager.update_llm_batch_status(llm_batch_id=llm_batch_job.id, status=JobStatus.cancelled, actor=actor)
131
153
  except NoResultFound:
132
154
  raise HTTPException(status_code=404, detail="Run not found")
letta/server/server.py CHANGED
@@ -28,7 +28,6 @@ from letta.functions.mcp_client.types import MCPServerType, MCPTool, SSEServerCo
28
28
  from letta.groups.helpers import load_multi_agent
29
29
  from letta.helpers.datetime_helpers import get_utc_time
30
30
  from letta.helpers.json_helpers import json_dumps, json_loads
31
- from letta.helpers.message_helper import prepare_input_message_create
32
31
 
33
32
  # TODO use custom interface
34
33
  from letta.interface import AgentInterface # abstract
@@ -148,7 +147,7 @@ class Server(object):
148
147
  raise NotImplementedError
149
148
 
150
149
  @abstractmethod
151
- def send_messages(self, user_id: str, agent_id: str, messages: Union[MessageCreate, List[Message]]) -> None:
150
+ def send_messages(self, user_id: str, agent_id: str, input_messages: List[MessageCreate]) -> None:
152
151
  """Send a list of messages to the agent"""
153
152
  raise NotImplementedError
154
153
 
@@ -372,19 +371,13 @@ class SyncServer(Server):
372
371
  self,
373
372
  actor: User,
374
373
  agent_id: str,
375
- input_messages: Union[Message, List[Message]],
374
+ input_messages: List[MessageCreate],
376
375
  interface: Union[AgentInterface, None] = None, # needed to getting responses
377
376
  put_inner_thoughts_first: bool = True,
378
377
  # timestamp: Optional[datetime],
379
378
  ) -> LettaUsageStatistics:
380
379
  """Send the input message through the agent"""
381
380
  # TODO: Thread actor directly through this function, since the top level caller most likely already retrieved the user
382
- # Input validation
383
- if isinstance(input_messages, Message):
384
- input_messages = [input_messages]
385
- if not all(isinstance(m, Message) for m in input_messages):
386
- raise ValueError(f"messages should be a Message or a list of Message, got {type(input_messages)}")
387
-
388
381
  logger.debug(f"Got input messages: {input_messages}")
389
382
  letta_agent = None
390
383
  try:
@@ -400,8 +393,9 @@ class SyncServer(Server):
400
393
  metadata = interface.metadata if hasattr(interface, "metadata") else None
401
394
  else:
402
395
  metadata = None
396
+
403
397
  usage_stats = letta_agent.step(
404
- messages=input_messages,
398
+ input_messages=input_messages,
405
399
  chaining=self.chaining,
406
400
  max_chaining_steps=self.max_chaining_steps,
407
401
  stream=token_streaming,
@@ -572,23 +566,14 @@ class SyncServer(Server):
572
566
  )
573
567
 
574
568
  # NOTE: eventually deprecate and only allow passing Message types
575
- # Convert to a Message object
576
- if timestamp:
577
- message = Message(
578
- agent_id=agent_id,
579
- role="user",
580
- content=[TextContent(text=packaged_user_message)],
581
- created_at=timestamp,
582
- )
583
- else:
584
- message = Message(
585
- agent_id=agent_id,
586
- role="user",
587
- content=[TextContent(text=packaged_user_message)],
588
- )
569
+ message = MessageCreate(
570
+ agent_id=agent_id,
571
+ role="user",
572
+ content=[TextContent(text=packaged_user_message)],
573
+ )
589
574
 
590
575
  # Run the agent state forward
591
- usage = self._step(actor=actor, agent_id=agent_id, input_messages=message)
576
+ usage = self._step(actor=actor, agent_id=agent_id, input_messages=[message])
592
577
  return usage
593
578
 
594
579
  def system_message(
@@ -660,23 +645,14 @@ class SyncServer(Server):
660
645
  self,
661
646
  actor: User,
662
647
  agent_id: str,
663
- messages: Union[List[MessageCreate], List[Message]],
648
+ input_messages: List[MessageCreate],
664
649
  wrap_user_message: bool = True,
665
650
  wrap_system_message: bool = True,
666
651
  interface: Union[AgentInterface, ChatCompletionsStreamingInterface, None] = None, # needed for responses
667
652
  metadata: Optional[dict] = None, # Pass through metadata to interface
668
653
  put_inner_thoughts_first: bool = True,
669
654
  ) -> LettaUsageStatistics:
670
- """Send a list of messages to the agent.
671
-
672
- If messages are of type MessageCreate, convert them to Message objects before sending.
673
- """
674
- if all(isinstance(m, MessageCreate) for m in messages):
675
- message_objects = [prepare_input_message_create(m, agent_id, wrap_user_message, wrap_system_message) for m in messages]
676
- elif all(isinstance(m, Message) for m in messages):
677
- message_objects = messages
678
- else:
679
- raise ValueError(f"All messages must be of type Message or MessageCreate, got {[type(m) for m in messages]}")
655
+ """Send a list of messages to the agent."""
680
656
 
681
657
  # Store metadata in interface if provided
682
658
  if metadata and hasattr(interface, "metadata"):
@@ -686,7 +662,7 @@ class SyncServer(Server):
686
662
  return self._step(
687
663
  actor=actor,
688
664
  agent_id=agent_id,
689
- input_messages=message_objects,
665
+ input_messages=input_messages,
690
666
  interface=interface,
691
667
  put_inner_thoughts_first=put_inner_thoughts_first,
692
668
  )
@@ -703,8 +679,6 @@ class SyncServer(Server):
703
679
  @trace_method
704
680
  def get_cached_llm_config(self, **kwargs):
705
681
  key = make_key(**kwargs)
706
- print(self._llm_config_cache)
707
- print("KEY", key)
708
682
  if key not in self._llm_config_cache:
709
683
  self._llm_config_cache[key] = self.get_llm_config_from_handle(**kwargs)
710
684
  return self._llm_config_cache[key]
@@ -1019,12 +993,8 @@ class SyncServer(Server):
1019
993
  agent = self.load_agent(agent_id=sleeptime_agent.id, actor=actor)
1020
994
  for passage in self.list_data_source_passages(source_id=source.id, user_id=actor.id):
1021
995
  agent.step(
1022
- messages=[
1023
- Message(
1024
- role="user",
1025
- content=[TextContent(text=passage.text)],
1026
- agent_id=sleeptime_agent.id,
1027
- ),
996
+ input_messages=[
997
+ MessageCreate(role="user", content=passage.text),
1028
998
  ]
1029
999
  )
1030
1000
  self.agent_manager.delete_agent(agent_id=sleeptime_agent.id, actor=actor)
@@ -1182,7 +1152,6 @@ class SyncServer(Server):
1182
1152
  provider = self.get_provider_from_name(provider_name)
1183
1153
 
1184
1154
  llm_configs = [config for config in provider.list_llm_models() if config.handle == handle]
1185
- print("LLM CONFIGS", llm_configs)
1186
1155
  if not llm_configs:
1187
1156
  llm_configs = [config for config in provider.list_llm_models() if config.model == model_name]
1188
1157
  if not llm_configs:
@@ -1195,8 +1164,6 @@ class SyncServer(Server):
1195
1164
  if not llm_configs:
1196
1165
  raise e
1197
1166
 
1198
- print("CONFIGS", llm_configs)
1199
-
1200
1167
  if len(llm_configs) == 1:
1201
1168
  llm_config = llm_configs[0]
1202
1169
  elif len(llm_configs) > 1:
@@ -1343,17 +1310,17 @@ class SyncServer(Server):
1343
1310
 
1344
1311
  # Next, attempt to run the tool with the sandbox
1345
1312
  try:
1346
- sandbox_run_result = ToolExecutionSandbox(tool.name, tool_args, actor, tool_object=tool).run(
1313
+ tool_execution_result = ToolExecutionSandbox(tool.name, tool_args, actor, tool_object=tool).run(
1347
1314
  agent_state=agent_state, additional_env_vars=tool_env_vars
1348
1315
  )
1349
1316
  return ToolReturnMessage(
1350
1317
  id="null",
1351
1318
  tool_call_id="null",
1352
1319
  date=get_utc_time(),
1353
- status=sandbox_run_result.status,
1354
- tool_return=str(sandbox_run_result.func_return),
1355
- stdout=sandbox_run_result.stdout,
1356
- stderr=sandbox_run_result.stderr,
1320
+ status=tool_execution_result.status,
1321
+ tool_return=str(tool_execution_result.func_return),
1322
+ stdout=tool_execution_result.stdout,
1323
+ stderr=tool_execution_result.stderr,
1357
1324
  )
1358
1325
 
1359
1326
  except Exception as e:
@@ -1567,7 +1534,7 @@ class SyncServer(Server):
1567
1534
  agent_id: str,
1568
1535
  actor: User,
1569
1536
  # role: MessageRole,
1570
- messages: Union[List[Message], List[MessageCreate]],
1537
+ input_messages: List[MessageCreate],
1571
1538
  stream_steps: bool,
1572
1539
  stream_tokens: bool,
1573
1540
  # related to whether or not we return `LettaMessage`s or `Message`s
@@ -1647,7 +1614,7 @@ class SyncServer(Server):
1647
1614
  self.send_messages,
1648
1615
  actor=actor,
1649
1616
  agent_id=agent_id,
1650
- messages=messages,
1617
+ input_messages=input_messages,
1651
1618
  interface=streaming_interface,
1652
1619
  metadata=metadata,
1653
1620
  )
@@ -1701,7 +1668,7 @@ class SyncServer(Server):
1701
1668
  self,
1702
1669
  group_id: str,
1703
1670
  actor: User,
1704
- messages: Union[List[Message], List[MessageCreate]],
1671
+ input_messages: Union[List[Message], List[MessageCreate]],
1705
1672
  stream_steps: bool,
1706
1673
  stream_tokens: bool,
1707
1674
  chat_completion_mode: bool = False,
@@ -1751,7 +1718,7 @@ class SyncServer(Server):
1751
1718
  task = asyncio.create_task(
1752
1719
  asyncio.to_thread(
1753
1720
  letta_multi_agent.step,
1754
- messages=messages,
1721
+ input_messages=input_messages,
1755
1722
  chaining=self.chaining,
1756
1723
  max_chaining_steps=self.max_chaining_steps,
1757
1724
  )
@@ -161,7 +161,7 @@ class AgentManager:
161
161
  # Basic CRUD operations
162
162
  # ======================================================================================================================
163
163
  @trace_method
164
- def create_agent(self, agent_create: CreateAgent, actor: PydanticUser) -> PydanticAgentState:
164
+ def create_agent(self, agent_create: CreateAgent, actor: PydanticUser, _test_only_force_id: Optional[str] = None) -> PydanticAgentState:
165
165
  # validate required configs
166
166
  if not agent_create.llm_config or not agent_create.embedding_config:
167
167
  raise ValueError("llm_config and embedding_config are required")
@@ -239,6 +239,10 @@ class AgentManager:
239
239
  created_by_id=actor.id,
240
240
  last_updated_by_id=actor.id,
241
241
  )
242
+
243
+ if _test_only_force_id:
244
+ new_agent.id = _test_only_force_id
245
+
242
246
  session.add(new_agent)
243
247
  session.flush()
244
248
  aid = new_agent.id
@@ -364,6 +368,7 @@ class AgentManager:
364
368
  "base_template_id": agent_update.base_template_id,
365
369
  "message_buffer_autoclear": agent_update.message_buffer_autoclear,
366
370
  "enable_sleeptime": agent_update.enable_sleeptime,
371
+ "response_format": agent_update.response_format,
367
372
  }
368
373
  for col, val in scalar_updates.items():
369
374
  if val is not None: