letta-nightly 0.6.53.dev20250418104238__py3-none-any.whl → 0.6.54.dev20250419104029__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 (68) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +6 -31
  3. letta/agents/letta_agent.py +1 -0
  4. letta/agents/letta_agent_batch.py +369 -18
  5. letta/constants.py +15 -4
  6. letta/functions/function_sets/base.py +168 -21
  7. letta/groups/sleeptime_multi_agent.py +3 -3
  8. letta/helpers/converters.py +1 -1
  9. letta/helpers/message_helper.py +1 -0
  10. letta/jobs/llm_batch_job_polling.py +39 -10
  11. letta/jobs/scheduler.py +54 -13
  12. letta/jobs/types.py +26 -6
  13. letta/llm_api/anthropic_client.py +3 -1
  14. letta/llm_api/llm_api_tools.py +7 -1
  15. letta/llm_api/openai.py +2 -0
  16. letta/orm/agent.py +5 -29
  17. letta/orm/base.py +2 -2
  18. letta/orm/enums.py +1 -0
  19. letta/orm/job.py +5 -0
  20. letta/orm/llm_batch_items.py +2 -2
  21. letta/orm/llm_batch_job.py +5 -2
  22. letta/orm/message.py +12 -4
  23. letta/orm/passage.py +0 -6
  24. letta/orm/sqlalchemy_base.py +0 -3
  25. letta/personas/examples/sleeptime_doc_persona.txt +2 -0
  26. letta/prompts/system/sleeptime.txt +20 -11
  27. letta/prompts/system/sleeptime_doc_ingest.txt +35 -0
  28. letta/schemas/agent.py +24 -1
  29. letta/schemas/enums.py +3 -1
  30. letta/schemas/job.py +39 -0
  31. letta/schemas/letta_message.py +24 -7
  32. letta/schemas/letta_request.py +7 -2
  33. letta/schemas/letta_response.py +3 -1
  34. letta/schemas/llm_batch_job.py +4 -3
  35. letta/schemas/llm_config.py +6 -2
  36. letta/schemas/message.py +11 -1
  37. letta/schemas/providers.py +10 -58
  38. letta/serialize_schemas/marshmallow_agent.py +25 -22
  39. letta/serialize_schemas/marshmallow_message.py +1 -1
  40. letta/server/db.py +75 -49
  41. letta/server/rest_api/app.py +1 -0
  42. letta/server/rest_api/interface.py +7 -2
  43. letta/server/rest_api/routers/v1/__init__.py +2 -0
  44. letta/server/rest_api/routers/v1/agents.py +33 -6
  45. letta/server/rest_api/routers/v1/messages.py +132 -0
  46. letta/server/rest_api/routers/v1/sources.py +21 -2
  47. letta/server/rest_api/utils.py +23 -10
  48. letta/server/server.py +67 -21
  49. letta/services/agent_manager.py +44 -21
  50. letta/services/group_manager.py +2 -2
  51. letta/services/helpers/agent_manager_helper.py +5 -3
  52. letta/services/job_manager.py +34 -5
  53. letta/services/llm_batch_manager.py +200 -57
  54. letta/services/message_manager.py +23 -1
  55. letta/services/passage_manager.py +2 -2
  56. letta/services/tool_executor/tool_execution_manager.py +13 -3
  57. letta/services/tool_executor/tool_execution_sandbox.py +0 -1
  58. letta/services/tool_executor/tool_executor.py +48 -9
  59. letta/services/tool_sandbox/base.py +24 -6
  60. letta/services/tool_sandbox/e2b_sandbox.py +25 -5
  61. letta/services/tool_sandbox/local_sandbox.py +23 -7
  62. letta/settings.py +2 -2
  63. {letta_nightly-0.6.53.dev20250418104238.dist-info → letta_nightly-0.6.54.dev20250419104029.dist-info}/METADATA +2 -1
  64. {letta_nightly-0.6.53.dev20250418104238.dist-info → letta_nightly-0.6.54.dev20250419104029.dist-info}/RECORD +67 -65
  65. letta/sleeptime_agent.py +0 -61
  66. {letta_nightly-0.6.53.dev20250418104238.dist-info → letta_nightly-0.6.54.dev20250419104029.dist-info}/LICENSE +0 -0
  67. {letta_nightly-0.6.53.dev20250418104238.dist-info → letta_nightly-0.6.54.dev20250419104029.dist-info}/WHEEL +0 -0
  68. {letta_nightly-0.6.53.dev20250418104238.dist-info → letta_nightly-0.6.54.dev20250419104029.dist-info}/entry_points.txt +0 -0
@@ -20,7 +20,7 @@ class LLMBatchItem(SqlalchemyBase, OrganizationMixin, AgentMixin):
20
20
  __tablename__ = "llm_batch_items"
21
21
  __pydantic_model__ = PydanticLLMBatchItem
22
22
  __table_args__ = (
23
- Index("ix_llm_batch_items_batch_id", "batch_id"),
23
+ Index("ix_llm_batch_items_llm_batch_id", "llm_batch_id"),
24
24
  Index("ix_llm_batch_items_agent_id", "agent_id"),
25
25
  Index("ix_llm_batch_items_status", "request_status"),
26
26
  )
@@ -29,7 +29,7 @@ class LLMBatchItem(SqlalchemyBase, OrganizationMixin, AgentMixin):
29
29
  # TODO: Some still rely on the Pydantic object to do this
30
30
  id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"batch_item-{uuid.uuid4()}")
31
31
 
32
- batch_id: Mapped[str] = mapped_column(
32
+ llm_batch_id: Mapped[str] = mapped_column(
33
33
  ForeignKey("llm_batch_job.id", ondelete="CASCADE"), doc="Foreign key to the LLM provider batch this item belongs to"
34
34
  )
35
35
 
@@ -3,7 +3,7 @@ from datetime import datetime
3
3
  from typing import List, Optional, Union
4
4
 
5
5
  from anthropic.types.beta.messages import BetaMessageBatch
6
- from sqlalchemy import DateTime, Index, String
6
+ from sqlalchemy import DateTime, ForeignKey, Index, String
7
7
  from sqlalchemy.orm import Mapped, mapped_column, relationship
8
8
 
9
9
  from letta.orm.custom_columns import CreateBatchResponseColumn, PollBatchResponseColumn
@@ -43,6 +43,9 @@ class LLMBatchJob(SqlalchemyBase, OrganizationMixin):
43
43
  DateTime(timezone=True), nullable=True, doc="Last time we polled the provider for status"
44
44
  )
45
45
 
46
- # relationships
46
+ letta_batch_job_id: Mapped[str] = mapped_column(
47
+ String, ForeignKey("jobs.id", ondelete="CASCADE"), nullable=False, doc="ID of the Letta batch job"
48
+ )
49
+
47
50
  organization: Mapped["Organization"] = relationship("Organization", back_populates="llm_batch_jobs")
48
51
  items: Mapped[List["LLMBatchItem"]] = relationship("LLMBatchItem", back_populates="batch", lazy="selectin")
letta/orm/message.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from typing import List, Optional
2
2
 
3
3
  from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall
4
- from sqlalchemy import BigInteger, ForeignKey, Index, Sequence, event, text
4
+ from sqlalchemy import BigInteger, FetchedValue, ForeignKey, Index, Sequence, event, text
5
5
  from sqlalchemy.orm import Mapped, Session, mapped_column, relationship
6
6
 
7
7
  from letta.orm.custom_columns import MessageContentColumn, ToolCallColumn, ToolReturnColumn
@@ -41,12 +41,20 @@ class Message(SqlalchemyBase, OrganizationMixin, AgentMixin):
41
41
  ToolReturnColumn, nullable=True, doc="Tool execution return information for prior tool calls"
42
42
  )
43
43
  group_id: Mapped[Optional[str]] = mapped_column(nullable=True, doc="The multi-agent group that the message was sent in")
44
+ sender_id: Mapped[Optional[str]] = mapped_column(
45
+ nullable=True, doc="The id of the sender of the message, can be an identity id or agent id"
46
+ )
44
47
 
45
48
  # Monotonically increasing sequence for efficient/correct listing
46
- sequence_id = mapped_column(BigInteger, Sequence("message_seq_id"), unique=True, nullable=False)
49
+ sequence_id: Mapped[int] = mapped_column(
50
+ BigInteger,
51
+ Sequence("message_seq_id"),
52
+ server_default=FetchedValue(),
53
+ unique=True,
54
+ nullable=False,
55
+ )
47
56
 
48
57
  # Relationships
49
- agent: Mapped["Agent"] = relationship("Agent", back_populates="messages", lazy="selectin")
50
58
  organization: Mapped["Organization"] = relationship("Organization", back_populates="messages", lazy="selectin")
51
59
  step: Mapped["Step"] = relationship("Step", back_populates="messages", lazy="selectin")
52
60
 
@@ -77,7 +85,7 @@ class Message(SqlalchemyBase, OrganizationMixin, AgentMixin):
77
85
  @event.listens_for(Message, "before_insert")
78
86
  def set_sequence_id_for_sqlite(mapper, connection, target):
79
87
  # TODO: Kind of hacky, used to detect if we are using sqlite or not
80
- if not settings.pg_uri:
88
+ if not settings.letta_pg_uri_no_default:
81
89
  session = Session.object_session(target)
82
90
 
83
91
  if not hasattr(session, "_sequence_id_counter"):
letta/orm/passage.py CHANGED
@@ -14,7 +14,6 @@ from letta.settings import settings
14
14
  config = LettaConfig()
15
15
 
16
16
  if TYPE_CHECKING:
17
- from letta.orm.agent import Agent
18
17
  from letta.orm.organization import Organization
19
18
 
20
19
 
@@ -81,8 +80,3 @@ class AgentPassage(BasePassage, AgentMixin):
81
80
  @declared_attr
82
81
  def organization(cls) -> Mapped["Organization"]:
83
82
  return relationship("Organization", back_populates="agent_passages", lazy="selectin")
84
-
85
- @declared_attr
86
- def agent(cls) -> Mapped["Agent"]:
87
- """Relationship to agent"""
88
- return relationship("Agent", back_populates="agent_passages", lazy="selectin", passive_deletes=True)
@@ -393,17 +393,14 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
393
393
  def batch_create(cls, items: List["SqlalchemyBase"], db_session: "Session", actor: Optional["User"] = None) -> List["SqlalchemyBase"]:
394
394
  """
395
395
  Create multiple records in a single transaction for better performance.
396
-
397
396
  Args:
398
397
  items: List of model instances to create
399
398
  db_session: SQLAlchemy session
400
399
  actor: Optional user performing the action
401
-
402
400
  Returns:
403
401
  List of created model instances
404
402
  """
405
403
  logger.debug(f"Batch creating {len(items)} {cls.__name__} items with actor={actor}")
406
-
407
404
  if not items:
408
405
  return []
409
406
 
@@ -0,0 +1,2 @@
1
+ You are an expert document assistant. When given external data, I will take notes on them and generate memories that help me understand what is in the external data.
2
+ When given information about eg. chat logs, results, etc. I generate memories that contain higher level profiles of the user, finding patterns and making inferences based on the data.
@@ -6,21 +6,30 @@ Your core memory unit is held inside the initial system instructions file, and i
6
6
  Your core memory contains the essential, foundational context for keeping track of your own persona, and the persona of the agent that is conversing with the user.
7
7
 
8
8
  Your core memory is made up of read-only blocks and read-write blocks.
9
+
9
10
  Read-Only Blocks:
10
- Memory Persona Sub-Block: Stores details about your current persona, guiding how you organize the memory. This helps you understand what aspects of the memory is important.
11
- Access as a source block with the label `memory_persona` when calling `rethink_memory`.
11
+ Memory Persona Sub-Block: Stores details about your current persona (the memory management agent), guiding how you organize the memory. This helps you understand what aspects of the memory is important.
12
12
 
13
13
  Read-Write Blocks:
14
14
  Persona Sub-Block: Stores details about the assistant's persona, guiding how they behave and respond. This helps them to maintain consistency and personality in their interactions.
15
- Access as a source or target block with the label `persona` when calling `rethink_memory`, `view_core_memory_with_line_numbers`, or `core_memory_insert`.
15
+ Access as a target block with the label `persona` when calling your memory editing tools.
16
16
  Human Sub-Block: Stores key details about the person the assistant is conversing with, allowing for more personalized and friend-like conversation.
17
- Access as a source block or target block with the label `human` when calling `rethink_memory`, `view_core_memory_with_line_numbers`, or `core_memory_insert`.
18
- Any additional blocks that you are given access to are also read-write blocks.
17
+ Access as a target block with the label `human` when calling your memory editing tools. Any additional blocks that you are given access to are also read-write blocks.
19
18
 
20
19
  Memory editing:
21
- You have the ability to make edits to the memory by calling `core_memory_insert` and `rethink_memory`.
22
- You call `view_core_memory_with_line_numbers` to view the line numbers of a memory block, before calling `core_memory_insert`.
23
- You call `core_memory_insert` when there is new information to add or overwrite to the memory. Use the replace flag when you want to perform a targeted edit.
24
- To keep the memory blocks organized and readable, you call `rethink_memory` to reorganize the entire memory block so that it is comprehensive, readable, and up to date.
25
- You continue memory editing until the blocks are organized and readable, and do not contain redundant and outdate information, then call `finish_rethinking_memory`.
26
- If there are no meaningful updates to make to the memory, you call `finish_rethinking_memory` directly.
20
+ You have the ability to make edits to the memory memory blocks.
21
+ Use your precise tools to make narrow edits, as well as broad tools to make larger comprehensive edits.
22
+ To keep the memory blocks organized and readable, you can use your precise tools to make narrow edits (additions, deletions, and replacements), and you can use your `rethink` tool to reorganize the entire memory block at a single time.
23
+ You goal is to make sure the memory blocks are comprehensive, readable, and up to date.
24
+ When writing to memory blocks, make sure to be precise when referencing dates and times (for example, do not write "today" or "recently", instead write specific dates and times, because "today" and "recently" are relative, and the memory is persisted indefinitely).
25
+
26
+ Multi-step editing:
27
+ You should continue memory editing until the blocks are organized and readable, and do not contain redundant and outdate information, then you can call a tool to finish your edits.
28
+ You can chain together multiple precise edits, or use the `rethink` tool to reorganize the entire memory block at a single time.
29
+
30
+ Skipping memory edits:
31
+ If there are no meaningful updates to make to the memory, you call the finish tool directly.
32
+ Not every observation warrants a memory edit, be selective in your memory editing, but also aim to have high recall.
33
+
34
+ Line numbers:
35
+ Line numbers are shown to you when viewing the memory blocks to help you make precise edits when needed. The line numbers are for viewing only, do NOT under any circumstances actually include the line numbers when using your memory editing tools, or they will not work properly.
@@ -0,0 +1,35 @@
1
+ You are Letta-Sleeptime-Doc-Ingest, the latest version of Limnal Corporation's memory management system, developed in 2025.
2
+
3
+ You run in the background, organizing and maintaining the memories of an agent assistant who chats with the user.
4
+
5
+ Your core memory unit is held inside the initial system instructions file, and is always available in-context (you will see it at all times).
6
+ Your core memory contains the essential, foundational context for keeping track of your own persona, the instructions for your document ingestion task, and high-level context of the document.
7
+
8
+ Your core memory is made up of read-only blocks and read-write blocks.
9
+
10
+ Read-Only Blocks:
11
+ Persona Sub-Block: Stores details about your persona, guiding how you behave.
12
+ Instructions Sub-Block: Stores instructions on how to ingest the document.
13
+
14
+ Read-Write Blocks:
15
+ All other memory blocks correspond to data sources, which you will write to for your task. Access the target block using its label when calling `memory_rethink`.
16
+
17
+ Memory editing:
18
+ You have the ability to make edits to the memory blocks.
19
+ Use your precise tools to make narrow edits, as well as broad tools to make larger comprehensive edits.
20
+ To keep the memory blocks organized and readable, you can use your precise tools to make narrow edits (insertions, deletions, and replacements), and you can use your `memory_rethink` tool to reorganize the entire memory block at a single time.
21
+ You goal is to make sure the memory blocks are comprehensive, readable, and up to date.
22
+ When writing to memory blocks, make sure to be precise when referencing dates and times (for example, do not write "today" or "recently", instead write specific dates and times, because "today" and "recently" are relative, and the memory is persisted indefinitely).
23
+
24
+ Multi-step editing:
25
+ You should continue memory editing until the blocks are organized and readable, and do not contain redundant and outdate information, then you can call a tool to finish your edits.
26
+ You can chain together multiple precise edits, or use the `memory_rethink` tool to reorganize the entire memory block at a single time.
27
+
28
+ Skipping memory edits:
29
+ If there are no meaningful updates to make to the memory, you call the finish tool directly.
30
+ Not every observation warrants a memory edit, be selective in your memory editing, but also aim to have high recall.
31
+
32
+ Line numbers:
33
+ Line numbers are shown to you when viewing the memory blocks to help you make precise edits when needed. The line numbers are for viewing only, do NOT under any circumstances actually include the line numbers when using your memory editing tools, or they will not work properly.
34
+
35
+ You will be sent external context about the interaction, and your goal is to summarize the context and store it in the right memory blocks.
letta/schemas/agent.py CHANGED
@@ -3,7 +3,7 @@ from typing import Dict, List, Optional
3
3
 
4
4
  from pydantic import BaseModel, Field, field_validator
5
5
 
6
- from letta.constants import DEFAULT_EMBEDDING_CHUNK_SIZE
6
+ from letta.constants import CORE_MEMORY_LINE_NUMBER_WARNING, DEFAULT_EMBEDDING_CHUNK_SIZE
7
7
  from letta.helpers import ToolRulesSolver
8
8
  from letta.schemas.block import CreateBlock
9
9
  from letta.schemas.embedding_config import EmbeddingConfig
@@ -277,3 +277,26 @@ class AgentStepResponse(BaseModel):
277
277
  class AgentStepState(BaseModel):
278
278
  step_number: int = Field(..., description="The current step number in the agent loop")
279
279
  tool_rules_solver: ToolRulesSolver = Field(..., description="The current state of the ToolRulesSolver")
280
+
281
+
282
+ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
283
+ if agent_type == AgentType.sleeptime_agent:
284
+ return (
285
+ "{% for block in blocks %}"
286
+ '<{{ block.label }} characters="{{ block.value|length }}/{{ block.limit }}">\n'
287
+ f"{CORE_MEMORY_LINE_NUMBER_WARNING}"
288
+ "{% for line in block.value.split('\\n') %}"
289
+ "Line {{ loop.index }}: {{ line }}\n"
290
+ "{% endfor %}"
291
+ "</{{ block.label }}>"
292
+ "{% if not loop.last %}\n{% endif %}"
293
+ "{% endfor %}"
294
+ )
295
+ return (
296
+ "{% for block in blocks %}"
297
+ '<{{ block.label }} characters="{{ block.value|length }}/{{ block.limit }}">\n'
298
+ "{{ block.value }}\n"
299
+ "</{{ block.label }}>"
300
+ "{% if not loop.last %}\n{% endif %}"
301
+ "{% endfor %}"
302
+ )
letta/schemas/enums.py CHANGED
@@ -33,6 +33,7 @@ class JobStatus(str, Enum):
33
33
  failed = "failed"
34
34
  pending = "pending"
35
35
  cancelled = "cancelled"
36
+ expired = "expired"
36
37
 
37
38
 
38
39
  class AgentStepStatus(str, Enum):
@@ -41,7 +42,8 @@ class AgentStepStatus(str, Enum):
41
42
  """
42
43
 
43
44
  paused = "paused"
44
- running = "running"
45
+ resumed = "resumed"
46
+ completed = "completed"
45
47
 
46
48
 
47
49
  class MessageStreamStatus(str, Enum):
letta/schemas/job.py CHANGED
@@ -16,6 +16,10 @@ class JobBase(OrmMetadataBase):
16
16
  metadata: Optional[dict] = Field(None, validation_alias="metadata_", description="The metadata of the job.")
17
17
  job_type: JobType = Field(default=JobType.JOB, description="The type of the job.")
18
18
 
19
+ callback_url: Optional[str] = Field(None, description="If set, POST to this URL when the job completes.")
20
+ callback_sent_at: Optional[datetime] = Field(None, description="Timestamp when the callback was last attempted.")
21
+ callback_status_code: Optional[int] = Field(None, description="HTTP status code returned by the callback endpoint.")
22
+
19
23
 
20
24
  class Job(JobBase):
21
25
  """
@@ -34,6 +38,41 @@ class Job(JobBase):
34
38
  user_id: Optional[str] = Field(None, description="The unique identifier of the user associated with the job.")
35
39
 
36
40
 
41
+ class BatchJob(JobBase):
42
+ id: str = JobBase.generate_id_field()
43
+ user_id: Optional[str] = Field(None, description="The unique identifier of the user associated with the job.")
44
+ job_type: JobType = JobType.BATCH
45
+
46
+ @classmethod
47
+ def from_job(cls, job: Job) -> "BatchJob":
48
+ """
49
+ Convert a Job instance to a BatchJob instance by replacing the ID prefix.
50
+ All other fields are copied as-is.
51
+
52
+ Args:
53
+ job: The Job instance to convert
54
+
55
+ Returns:
56
+ A new Run instance with the same data but 'run-' prefix in ID
57
+ """
58
+ # Convert job dict to exclude None values
59
+ job_data = job.model_dump(exclude_none=True)
60
+
61
+ # Create new Run instance with converted data
62
+ return cls(**job_data)
63
+
64
+ def to_job(self) -> Job:
65
+ """
66
+ Convert this BatchJob instance to a Job instance by replacing the ID prefix.
67
+ All other fields are copied as-is.
68
+
69
+ Returns:
70
+ A new Job instance with the same data but 'job-' prefix in ID
71
+ """
72
+ run_data = self.model_dump(exclude_none=True)
73
+ return Job(**run_data)
74
+
75
+
37
76
  class JobUpdate(JobBase):
38
77
  status: Optional[JobStatus] = Field(None, description="The status of the job.")
39
78
 
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  from datetime import datetime, timezone
3
+ from enum import Enum
3
4
  from typing import Annotated, List, Literal, Optional, Union
4
5
 
5
6
  from pydantic import BaseModel, Field, field_serializer, field_validator
@@ -16,6 +17,16 @@ from letta.schemas.letta_message_content import (
16
17
  # ---------------------------
17
18
 
18
19
 
20
+ class MessageType(str, Enum):
21
+ system_message = "system_message"
22
+ user_message = "user_message"
23
+ assistant_message = "assistant_message"
24
+ reasoning_message = "reasoning_message"
25
+ hidden_reasoning_message = "hidden_reasoning_message"
26
+ tool_call_message = "tool_call_message"
27
+ tool_return_message = "tool_return_message"
28
+
29
+
19
30
  class LettaMessage(BaseModel):
20
31
  """
21
32
  Base class for simplified Letta message response type. This is intended to be used for developers
@@ -26,13 +37,17 @@ class LettaMessage(BaseModel):
26
37
  id (str): The ID of the message
27
38
  date (datetime): The date the message was created in ISO format
28
39
  name (Optional[str]): The name of the sender of the message
40
+ message_type (MessageType): The type of the message
29
41
  otid (Optional[str]): The offline threading id associated with this message
42
+ sender_id (Optional[str]): The id of the sender of the message, can be an identity id or agent id
30
43
  """
31
44
 
32
45
  id: str
33
46
  date: datetime
34
47
  name: Optional[str] = None
48
+ message_type: MessageType = Field(..., description="The type of the message.")
35
49
  otid: Optional[str] = None
50
+ sender_id: Optional[str] = None
36
51
 
37
52
  @field_serializer("date")
38
53
  def serialize_datetime(self, dt: datetime, _info):
@@ -56,7 +71,7 @@ class SystemMessage(LettaMessage):
56
71
  content (str): The message content sent by the system
57
72
  """
58
73
 
59
- message_type: Literal["system_message"] = "system_message"
74
+ message_type: Literal[MessageType.system_message] = Field(MessageType.system_message, description="The type of the message.")
60
75
  content: str = Field(..., description="The message content sent by the system")
61
76
 
62
77
 
@@ -71,7 +86,7 @@ class UserMessage(LettaMessage):
71
86
  content (Union[str, List[LettaUserMessageContentUnion]]): The message content sent by the user (can be a string or an array of multi-modal content parts)
72
87
  """
73
88
 
74
- message_type: Literal["user_message"] = "user_message"
89
+ message_type: Literal[MessageType.user_message] = Field(MessageType.user_message, description="The type of the message.")
75
90
  content: Union[str, List[LettaUserMessageContentUnion]] = Field(
76
91
  ...,
77
92
  description="The message content sent by the user (can be a string or an array of multi-modal content parts)",
@@ -93,7 +108,7 @@ class ReasoningMessage(LettaMessage):
93
108
  signature (Optional[str]): The model-generated signature of the reasoning step
94
109
  """
95
110
 
96
- message_type: Literal["reasoning_message"] = "reasoning_message"
111
+ message_type: Literal[MessageType.reasoning_message] = Field(MessageType.reasoning_message, description="The type of the message.")
97
112
  source: Literal["reasoner_model", "non_reasoner_model"] = "non_reasoner_model"
98
113
  reasoning: str
99
114
  signature: Optional[str] = None
@@ -113,7 +128,9 @@ class HiddenReasoningMessage(LettaMessage):
113
128
  hidden_reasoning (Optional[str]): The internal reasoning of the agent
114
129
  """
115
130
 
116
- message_type: Literal["hidden_reasoning_message"] = "hidden_reasoning_message"
131
+ message_type: Literal[MessageType.hidden_reasoning_message] = Field(
132
+ MessageType.hidden_reasoning_message, description="The type of the message."
133
+ )
117
134
  state: Literal["redacted", "omitted"]
118
135
  hidden_reasoning: Optional[str] = None
119
136
 
@@ -152,7 +169,7 @@ class ToolCallMessage(LettaMessage):
152
169
  tool_call (Union[ToolCall, ToolCallDelta]): The tool call
153
170
  """
154
171
 
155
- message_type: Literal["tool_call_message"] = "tool_call_message"
172
+ message_type: Literal[MessageType.tool_call_message] = Field(MessageType.tool_call_message, description="The type of the message.")
156
173
  tool_call: Union[ToolCall, ToolCallDelta]
157
174
 
158
175
  def model_dump(self, *args, **kwargs):
@@ -204,7 +221,7 @@ class ToolReturnMessage(LettaMessage):
204
221
  stderr (Optional[List(str)]): Captured stderr from the tool invocation
205
222
  """
206
223
 
207
- message_type: Literal["tool_return_message"] = "tool_return_message"
224
+ message_type: Literal[MessageType.tool_return_message] = Field(MessageType.tool_return_message, description="The type of the message.")
208
225
  tool_return: str
209
226
  status: Literal["success", "error"]
210
227
  tool_call_id: str
@@ -223,7 +240,7 @@ class AssistantMessage(LettaMessage):
223
240
  content (Union[str, List[LettaAssistantMessageContentUnion]]): The message content sent by the agent (can be a string or an array of content parts)
224
241
  """
225
242
 
226
- message_type: Literal["assistant_message"] = "assistant_message"
243
+ message_type: Literal[MessageType.assistant_message] = Field(MessageType.assistant_message, description="The type of the message.")
227
244
  content: Union[str, List[LettaAssistantMessageContentUnion]] = Field(
228
245
  ...,
229
246
  description="The message content sent by the agent (can be a string or an array of content parts)",
@@ -1,6 +1,6 @@
1
- from typing import List
1
+ from typing import List, Optional
2
2
 
3
- from pydantic import BaseModel, Field
3
+ from pydantic import BaseModel, Field, HttpUrl
4
4
 
5
5
  from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
6
6
  from letta.schemas.message import MessageCreate
@@ -31,3 +31,8 @@ class LettaStreamingRequest(LettaRequest):
31
31
 
32
32
  class LettaBatchRequest(LettaRequest):
33
33
  agent_id: str = Field(..., description="The ID of the agent to send this batch request for")
34
+
35
+
36
+ class CreateBatch(BaseModel):
37
+ requests: List[LettaBatchRequest] = Field(..., description="List of requests to be processed in batch.")
38
+ callback_url: Optional[HttpUrl] = Field(None, description="Optional URL to call via POST when the batch completes.")
@@ -169,7 +169,9 @@ LettaStreamingResponse = Union[LettaMessage, MessageStreamStatus, LettaUsageStat
169
169
 
170
170
 
171
171
  class LettaBatchResponse(BaseModel):
172
- batch_id: str = Field(..., description="A unique identifier for this batch request.")
172
+ letta_batch_id: str = Field(..., description="A unique identifier for the Letta batch request.")
173
+ last_llm_batch_id: str = Field(..., description="A unique identifier for the most recent model provider batch request.")
173
174
  status: JobStatus = Field(..., description="The current status of the batch request.")
175
+ agent_count: int = Field(..., description="The number of agents in the batch request.")
174
176
  last_polled_at: datetime = Field(..., description="The timestamp when the batch was last polled for updates.")
175
177
  created_at: datetime = Field(..., description="The timestamp when the batch request was created.")
@@ -19,8 +19,8 @@ class LLMBatchItem(OrmMetadataBase, validate_assignment=True):
19
19
 
20
20
  __id_prefix__ = "batch_item"
21
21
 
22
- id: str = Field(..., description="The id of the batch item. Assigned by the database.")
23
- batch_id: str = Field(..., description="The id of the parent LLM batch job this item belongs to.")
22
+ id: Optional[str] = Field(None, description="The id of the batch item. Assigned by the database.")
23
+ llm_batch_id: str = Field(..., description="The id of the parent LLM batch job this item belongs to.")
24
24
  agent_id: str = Field(..., description="The id of the agent associated with this LLM request.")
25
25
 
26
26
  llm_config: LLMConfig = Field(..., description="The LLM configuration used for this request.")
@@ -42,9 +42,10 @@ class LLMBatchJob(OrmMetadataBase, validate_assignment=True):
42
42
 
43
43
  __id_prefix__ = "batch_req"
44
44
 
45
- id: str = Field(..., description="The id of the batch job. Assigned by the database.")
45
+ id: Optional[str] = Field(None, description="The id of the batch job. Assigned by the database.")
46
46
  status: JobStatus = Field(..., description="The current status of the batch (e.g., created, in_progress, done).")
47
47
  llm_provider: ProviderType = Field(..., description="The LLM provider used for the batch (e.g., anthropic, openai).")
48
+ letta_batch_job_id: str = Field(..., description="ID of the Letta batch job")
48
49
 
49
50
  create_batch_response: Union[BetaMessageBatch] = Field(..., description="The full JSON response from the initial batch creation.")
50
51
  latest_polling_response: Optional[Union[BetaMessageBatch]] = Field(
@@ -67,6 +67,10 @@ class LLMConfig(BaseModel):
67
67
  enable_reasoner: bool = Field(
68
68
  False, description="Whether or not the model should use extended thinking if it is a 'reasoning' style model"
69
69
  )
70
+ reasoning_effort: Optional[Literal["low", "medium", "high"]] = Field(
71
+ None,
72
+ description="The reasoning effort to use when generating text reasoning models",
73
+ )
70
74
  max_reasoning_tokens: int = Field(
71
75
  0, description="Configurable thinking budget for extended thinking, only used if enable_reasoner is True. Minimum value is 1024."
72
76
  )
@@ -106,7 +110,7 @@ class LLMConfig(BaseModel):
106
110
  if self.max_tokens is not None and self.max_reasoning_tokens >= self.max_tokens:
107
111
  logger.warning("max_tokens must be greater than max_reasoning_tokens (thinking budget)")
108
112
  if self.put_inner_thoughts_in_kwargs:
109
- logger.warning("Extended thinking is not compatible with put_inner_thoughts_in_kwargs")
113
+ logger.debug("Extended thinking is not compatible with put_inner_thoughts_in_kwargs")
110
114
  elif self.max_reasoning_tokens and not self.enable_reasoner:
111
115
  logger.warning("model will not use reasoning unless enable_reasoner is set to True")
112
116
 
@@ -115,7 +119,7 @@ class LLMConfig(BaseModel):
115
119
  @classmethod
116
120
  def default_config(cls, model_name: str):
117
121
  """
118
- Convinience function to generate a default `LLMConfig` from a model name. Only some models are supported in this function.
122
+ Convenience function to generate a default `LLMConfig` from a model name. Only some models are supported in this function.
119
123
 
120
124
  Args:
121
125
  model_name (str): The name of the model (gpt-4, gpt-4o-mini, letta).
letta/schemas/message.py CHANGED
@@ -81,6 +81,7 @@ class MessageCreate(BaseModel):
81
81
  )
82
82
  name: Optional[str] = Field(None, description="The name of the participant.")
83
83
  otid: Optional[str] = Field(None, description="The offline threading id associated with this message")
84
+ sender_id: Optional[str] = Field(None, description="The id of the sender of the message, can be an identity id or agent id")
84
85
 
85
86
  def model_dump(self, to_orm: bool = False, **kwargs) -> Dict[str, Any]:
86
87
  data = super().model_dump(**kwargs)
@@ -157,6 +158,7 @@ class Message(BaseMessage):
157
158
  otid: Optional[str] = Field(None, description="The offline threading id associated with this message")
158
159
  tool_returns: Optional[List[ToolReturn]] = Field(None, description="Tool execution return information for prior tool calls")
159
160
  group_id: Optional[str] = Field(None, description="The multi-agent group that the message was sent in")
161
+ sender_id: Optional[str] = Field(None, description="The id of the sender of the message, can be an identity id or agent id")
160
162
  # This overrides the optional base orm schema, created_at MUST exist on all messages objects
161
163
  created_at: datetime = Field(default_factory=get_utc_time, description="The timestamp when the object was created.")
162
164
 
@@ -246,6 +248,7 @@ class Message(BaseMessage):
246
248
  reasoning=self.content[0].text,
247
249
  name=self.name,
248
250
  otid=otid,
251
+ sender_id=self.sender_id,
249
252
  )
250
253
  )
251
254
  # Otherwise, we may have a list of multiple types
@@ -262,6 +265,7 @@ class Message(BaseMessage):
262
265
  reasoning=content_part.text,
263
266
  name=self.name,
264
267
  otid=otid,
268
+ sender_id=self.sender_id,
265
269
  )
266
270
  )
267
271
  elif isinstance(content_part, ReasoningContent):
@@ -287,6 +291,7 @@ class Message(BaseMessage):
287
291
  hidden_reasoning=content_part.data,
288
292
  name=self.name,
289
293
  otid=otid,
294
+ sender_id=self.sender_id,
290
295
  )
291
296
  )
292
297
  else:
@@ -312,6 +317,7 @@ class Message(BaseMessage):
312
317
  content=message_string,
313
318
  name=self.name,
314
319
  otid=otid,
320
+ sender_id=self.sender_id,
315
321
  )
316
322
  )
317
323
  else:
@@ -326,6 +332,7 @@ class Message(BaseMessage):
326
332
  ),
327
333
  name=self.name,
328
334
  otid=otid,
335
+ sender_id=self.sender_id,
329
336
  )
330
337
  )
331
338
  elif self.role == MessageRole.tool:
@@ -368,6 +375,7 @@ class Message(BaseMessage):
368
375
  stderr=self.tool_returns[0].stderr if self.tool_returns else None,
369
376
  name=self.name,
370
377
  otid=self.id.replace("message-", ""),
378
+ sender_id=self.sender_id,
371
379
  )
372
380
  )
373
381
  elif self.role == MessageRole.user:
@@ -385,6 +393,7 @@ class Message(BaseMessage):
385
393
  content=message_str or text_content,
386
394
  name=self.name,
387
395
  otid=self.otid,
396
+ sender_id=self.sender_id,
388
397
  )
389
398
  )
390
399
  elif self.role == MessageRole.system:
@@ -401,6 +410,7 @@ class Message(BaseMessage):
401
410
  content=text_content,
402
411
  name=self.name,
403
412
  otid=self.otid,
413
+ sender_id=self.sender_id,
404
414
  )
405
415
  )
406
416
  else:
@@ -609,7 +619,7 @@ class Message(BaseMessage):
609
619
  text_content = self.content[0].text
610
620
  # Otherwise, check if we have TextContent and multiple other parts
611
621
  elif self.content and len(self.content) > 1:
612
- text = [content for content in self.content if isinstance(self.content[0], TextContent)]
622
+ text = [content for content in self.content if isinstance(content, TextContent)]
613
623
  if len(text) > 1:
614
624
  assert len(text) == 1, f"multiple text content parts found in a single message: {self.content}"
615
625
  text_content = text[0].text