letta-nightly 0.6.53.dev20250417104214__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.
- letta/__init__.py +1 -1
- letta/agent.py +6 -31
- letta/agents/letta_agent.py +1 -0
- letta/agents/letta_agent_batch.py +369 -18
- letta/constants.py +15 -4
- letta/functions/function_sets/base.py +168 -21
- letta/groups/sleeptime_multi_agent.py +3 -3
- letta/helpers/converters.py +1 -1
- letta/helpers/message_helper.py +1 -0
- letta/jobs/llm_batch_job_polling.py +39 -10
- letta/jobs/scheduler.py +54 -13
- letta/jobs/types.py +26 -6
- letta/llm_api/anthropic_client.py +3 -1
- letta/llm_api/llm_api_tools.py +7 -1
- letta/llm_api/openai.py +2 -0
- letta/orm/agent.py +5 -29
- letta/orm/base.py +2 -2
- letta/orm/enums.py +1 -0
- letta/orm/job.py +5 -0
- letta/orm/llm_batch_items.py +2 -2
- letta/orm/llm_batch_job.py +5 -2
- letta/orm/message.py +12 -4
- letta/orm/passage.py +0 -6
- letta/orm/sqlalchemy_base.py +0 -3
- letta/personas/examples/sleeptime_doc_persona.txt +2 -0
- letta/prompts/system/sleeptime.txt +20 -11
- letta/prompts/system/sleeptime_doc_ingest.txt +35 -0
- letta/schemas/agent.py +24 -1
- letta/schemas/enums.py +3 -1
- letta/schemas/job.py +39 -0
- letta/schemas/letta_message.py +24 -7
- letta/schemas/letta_request.py +7 -2
- letta/schemas/letta_response.py +3 -1
- letta/schemas/llm_batch_job.py +4 -3
- letta/schemas/llm_config.py +6 -2
- letta/schemas/message.py +11 -1
- letta/schemas/providers.py +10 -58
- letta/serialize_schemas/marshmallow_agent.py +25 -22
- letta/serialize_schemas/marshmallow_message.py +1 -1
- letta/server/db.py +75 -49
- letta/server/rest_api/app.py +1 -0
- letta/server/rest_api/interface.py +7 -2
- letta/server/rest_api/routers/v1/__init__.py +2 -0
- letta/server/rest_api/routers/v1/agents.py +33 -6
- letta/server/rest_api/routers/v1/messages.py +132 -0
- letta/server/rest_api/routers/v1/sources.py +21 -2
- letta/server/rest_api/utils.py +23 -10
- letta/server/server.py +67 -21
- letta/services/agent_manager.py +44 -21
- letta/services/group_manager.py +2 -2
- letta/services/helpers/agent_manager_helper.py +5 -3
- letta/services/job_manager.py +34 -5
- letta/services/llm_batch_manager.py +200 -57
- letta/services/message_manager.py +23 -1
- letta/services/passage_manager.py +2 -2
- letta/services/tool_executor/tool_execution_manager.py +13 -3
- letta/services/tool_executor/tool_execution_sandbox.py +0 -1
- letta/services/tool_executor/tool_executor.py +48 -9
- letta/services/tool_sandbox/base.py +24 -6
- letta/services/tool_sandbox/e2b_sandbox.py +25 -5
- letta/services/tool_sandbox/local_sandbox.py +23 -7
- letta/settings.py +2 -2
- {letta_nightly-0.6.53.dev20250417104214.dist-info → letta_nightly-0.6.54.dev20250419104029.dist-info}/METADATA +2 -1
- {letta_nightly-0.6.53.dev20250417104214.dist-info → letta_nightly-0.6.54.dev20250419104029.dist-info}/RECORD +67 -65
- letta/sleeptime_agent.py +0 -61
- {letta_nightly-0.6.53.dev20250417104214.dist-info → letta_nightly-0.6.54.dev20250419104029.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.53.dev20250417104214.dist-info → letta_nightly-0.6.54.dev20250419104029.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.53.dev20250417104214.dist-info → letta_nightly-0.6.54.dev20250419104029.dist-info}/entry_points.txt +0 -0
letta/orm/llm_batch_items.py
CHANGED
@@ -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("
|
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
|
-
|
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
|
|
letta/orm/llm_batch_job.py
CHANGED
@@ -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
|
-
|
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(
|
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.
|
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)
|
letta/orm/sqlalchemy_base.py
CHANGED
@@ -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
|
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
|
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
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
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
|
|
letta/schemas/letta_message.py
CHANGED
@@ -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[
|
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[
|
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[
|
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[
|
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[
|
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[
|
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[
|
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)",
|
letta/schemas/letta_request.py
CHANGED
@@ -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.")
|
letta/schemas/letta_response.py
CHANGED
@@ -169,7 +169,9 @@ LettaStreamingResponse = Union[LettaMessage, MessageStreamStatus, LettaUsageStat
|
|
169
169
|
|
170
170
|
|
171
171
|
class LettaBatchResponse(BaseModel):
|
172
|
-
|
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.")
|
letta/schemas/llm_batch_job.py
CHANGED
@@ -19,8 +19,8 @@ class LLMBatchItem(OrmMetadataBase, validate_assignment=True):
|
|
19
19
|
|
20
20
|
__id_prefix__ = "batch_item"
|
21
21
|
|
22
|
-
id: str = Field(
|
23
|
-
|
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(
|
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(
|
letta/schemas/llm_config.py
CHANGED
@@ -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.
|
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
|
-
|
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(
|
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
|