letta-nightly 0.11.7.dev20251007104119__py3-none-any.whl → 0.12.0.dev20251009104148__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/adapters/letta_llm_adapter.py +1 -0
- letta/adapters/letta_llm_request_adapter.py +0 -1
- letta/adapters/letta_llm_stream_adapter.py +7 -2
- letta/adapters/simple_llm_request_adapter.py +88 -0
- letta/adapters/simple_llm_stream_adapter.py +192 -0
- letta/agents/agent_loop.py +6 -0
- letta/agents/ephemeral_summary_agent.py +2 -1
- letta/agents/helpers.py +142 -6
- letta/agents/letta_agent.py +13 -33
- letta/agents/letta_agent_batch.py +2 -4
- letta/agents/letta_agent_v2.py +87 -77
- letta/agents/letta_agent_v3.py +927 -0
- letta/agents/voice_agent.py +2 -6
- letta/constants.py +8 -4
- letta/database_utils.py +161 -0
- letta/errors.py +40 -0
- letta/functions/function_sets/base.py +84 -4
- letta/functions/function_sets/multi_agent.py +0 -3
- letta/functions/schema_generator.py +113 -71
- letta/groups/dynamic_multi_agent.py +3 -2
- letta/groups/helpers.py +1 -2
- letta/groups/round_robin_multi_agent.py +3 -2
- letta/groups/sleeptime_multi_agent.py +3 -2
- letta/groups/sleeptime_multi_agent_v2.py +1 -1
- letta/groups/sleeptime_multi_agent_v3.py +17 -17
- letta/groups/supervisor_multi_agent.py +84 -80
- letta/helpers/converters.py +3 -0
- letta/helpers/message_helper.py +4 -0
- letta/helpers/tool_rule_solver.py +92 -5
- letta/interfaces/anthropic_streaming_interface.py +409 -0
- letta/interfaces/gemini_streaming_interface.py +296 -0
- letta/interfaces/openai_streaming_interface.py +752 -1
- letta/llm_api/anthropic_client.py +127 -16
- letta/llm_api/bedrock_client.py +4 -2
- letta/llm_api/deepseek_client.py +4 -1
- letta/llm_api/google_vertex_client.py +124 -42
- letta/llm_api/groq_client.py +4 -1
- letta/llm_api/llm_api_tools.py +11 -4
- letta/llm_api/llm_client_base.py +6 -2
- letta/llm_api/openai.py +32 -2
- letta/llm_api/openai_client.py +423 -18
- letta/llm_api/xai_client.py +4 -1
- letta/main.py +9 -5
- letta/memory.py +1 -0
- letta/orm/__init__.py +2 -1
- letta/orm/agent.py +10 -0
- letta/orm/block.py +7 -16
- letta/orm/blocks_agents.py +8 -2
- letta/orm/files_agents.py +2 -0
- letta/orm/job.py +7 -5
- letta/orm/mcp_oauth.py +1 -0
- letta/orm/message.py +21 -6
- letta/orm/organization.py +2 -0
- letta/orm/provider.py +6 -2
- letta/orm/run.py +71 -0
- letta/orm/run_metrics.py +82 -0
- letta/orm/sandbox_config.py +7 -1
- letta/orm/sqlalchemy_base.py +0 -306
- letta/orm/step.py +6 -5
- letta/orm/step_metrics.py +5 -5
- letta/otel/tracing.py +28 -3
- letta/plugins/defaults.py +4 -4
- letta/prompts/system_prompts/__init__.py +2 -0
- letta/prompts/system_prompts/letta_v1.py +25 -0
- letta/schemas/agent.py +3 -2
- letta/schemas/agent_file.py +9 -3
- letta/schemas/block.py +23 -10
- letta/schemas/enums.py +21 -2
- letta/schemas/job.py +17 -4
- letta/schemas/letta_message_content.py +71 -2
- letta/schemas/letta_stop_reason.py +5 -5
- letta/schemas/llm_config.py +53 -3
- letta/schemas/memory.py +1 -1
- letta/schemas/message.py +564 -117
- letta/schemas/openai/responses_request.py +64 -0
- letta/schemas/providers/__init__.py +2 -0
- letta/schemas/providers/anthropic.py +16 -0
- letta/schemas/providers/ollama.py +115 -33
- letta/schemas/providers/openrouter.py +52 -0
- letta/schemas/providers/vllm.py +2 -1
- letta/schemas/run.py +48 -42
- letta/schemas/run_metrics.py +21 -0
- letta/schemas/step.py +2 -2
- letta/schemas/step_metrics.py +1 -1
- letta/schemas/tool.py +15 -107
- letta/schemas/tool_rule.py +88 -5
- letta/serialize_schemas/marshmallow_agent.py +1 -0
- letta/server/db.py +79 -408
- letta/server/rest_api/app.py +61 -10
- letta/server/rest_api/dependencies.py +14 -0
- letta/server/rest_api/redis_stream_manager.py +19 -8
- letta/server/rest_api/routers/v1/agents.py +364 -292
- letta/server/rest_api/routers/v1/blocks.py +14 -20
- letta/server/rest_api/routers/v1/identities.py +45 -110
- letta/server/rest_api/routers/v1/internal_templates.py +21 -0
- letta/server/rest_api/routers/v1/jobs.py +23 -6
- letta/server/rest_api/routers/v1/messages.py +1 -1
- letta/server/rest_api/routers/v1/runs.py +149 -99
- letta/server/rest_api/routers/v1/sandbox_configs.py +10 -19
- letta/server/rest_api/routers/v1/tools.py +281 -594
- letta/server/rest_api/routers/v1/voice.py +1 -1
- letta/server/rest_api/streaming_response.py +29 -29
- letta/server/rest_api/utils.py +122 -64
- letta/server/server.py +160 -887
- letta/services/agent_manager.py +236 -919
- letta/services/agent_serialization_manager.py +16 -0
- letta/services/archive_manager.py +0 -100
- letta/services/block_manager.py +211 -168
- letta/services/context_window_calculator/token_counter.py +1 -1
- letta/services/file_manager.py +1 -1
- letta/services/files_agents_manager.py +24 -33
- letta/services/group_manager.py +0 -142
- letta/services/helpers/agent_manager_helper.py +7 -2
- letta/services/helpers/run_manager_helper.py +69 -0
- letta/services/job_manager.py +96 -411
- letta/services/lettuce/__init__.py +6 -0
- letta/services/lettuce/lettuce_client_base.py +86 -0
- letta/services/mcp_manager.py +38 -6
- letta/services/message_manager.py +165 -362
- letta/services/organization_manager.py +0 -36
- letta/services/passage_manager.py +0 -345
- letta/services/provider_manager.py +0 -80
- letta/services/run_manager.py +364 -0
- letta/services/sandbox_config_manager.py +0 -234
- letta/services/step_manager.py +62 -39
- letta/services/summarizer/summarizer.py +9 -7
- letta/services/telemetry_manager.py +0 -16
- letta/services/tool_executor/builtin_tool_executor.py +35 -0
- letta/services/tool_executor/core_tool_executor.py +397 -2
- letta/services/tool_executor/files_tool_executor.py +3 -3
- letta/services/tool_executor/multi_agent_tool_executor.py +30 -15
- letta/services/tool_executor/tool_execution_manager.py +6 -8
- letta/services/tool_executor/tool_executor_base.py +3 -3
- letta/services/tool_manager.py +85 -339
- letta/services/tool_sandbox/base.py +24 -13
- letta/services/tool_sandbox/e2b_sandbox.py +16 -1
- letta/services/tool_schema_generator.py +123 -0
- letta/services/user_manager.py +0 -99
- letta/settings.py +20 -4
- letta/system.py +5 -1
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/METADATA +3 -5
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/RECORD +146 -135
- letta/agents/temporal/activities/__init__.py +0 -4
- letta/agents/temporal/activities/example_activity.py +0 -7
- letta/agents/temporal/activities/prepare_messages.py +0 -10
- letta/agents/temporal/temporal_agent_workflow.py +0 -56
- letta/agents/temporal/types.py +0 -25
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/licenses/LICENSE +0 -0
letta/orm/sqlalchemy_base.py
CHANGED
@@ -66,101 +66,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
66
66
|
|
67
67
|
id: Mapped[str] = mapped_column(String, primary_key=True)
|
68
68
|
|
69
|
-
@classmethod
|
70
|
-
@handle_db_timeout
|
71
|
-
def list(
|
72
|
-
cls,
|
73
|
-
*,
|
74
|
-
db_session: "Session",
|
75
|
-
before: Optional[str] = None,
|
76
|
-
after: Optional[str] = None,
|
77
|
-
start_date: Optional[datetime] = None,
|
78
|
-
end_date: Optional[datetime] = None,
|
79
|
-
limit: Optional[int] = 50,
|
80
|
-
query_text: Optional[str] = None,
|
81
|
-
query_embedding: Optional[List[float]] = None,
|
82
|
-
ascending: bool = True,
|
83
|
-
actor: Optional["User"] = None,
|
84
|
-
access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
|
85
|
-
access_type: AccessType = AccessType.ORGANIZATION,
|
86
|
-
join_model: Optional[Base] = None,
|
87
|
-
join_conditions: Optional[Union[Tuple, List]] = None,
|
88
|
-
identifier_keys: Optional[List[str]] = None,
|
89
|
-
identity_id: Optional[str] = None,
|
90
|
-
**kwargs,
|
91
|
-
) -> List["SqlalchemyBase"]:
|
92
|
-
"""
|
93
|
-
List records with before/after pagination, ordering by created_at.
|
94
|
-
Can use both before and after to fetch a window of records.
|
95
|
-
|
96
|
-
Args:
|
97
|
-
db_session: SQLAlchemy session
|
98
|
-
before: ID of item to paginate before (upper bound)
|
99
|
-
after: ID of item to paginate after (lower bound)
|
100
|
-
start_date: Filter items after this date
|
101
|
-
end_date: Filter items before this date
|
102
|
-
limit: Maximum number of items to return
|
103
|
-
query_text: Text to search for
|
104
|
-
query_embedding: Vector to search for similar embeddings
|
105
|
-
ascending: Sort direction
|
106
|
-
**kwargs: Additional filters to apply
|
107
|
-
"""
|
108
|
-
if start_date and end_date and start_date > end_date:
|
109
|
-
raise ValueError("start_date must be earlier than or equal to end_date")
|
110
|
-
|
111
|
-
logger.debug(f"Listing {cls.__name__} with kwarg filters {kwargs}")
|
112
|
-
|
113
|
-
with db_session as session:
|
114
|
-
# Get the reference objects for pagination
|
115
|
-
before_obj = None
|
116
|
-
after_obj = None
|
117
|
-
|
118
|
-
if before:
|
119
|
-
before_obj = session.get(cls, before)
|
120
|
-
if not before_obj:
|
121
|
-
raise NoResultFound(f"No {cls.__name__} found with id {before}")
|
122
|
-
|
123
|
-
if after:
|
124
|
-
after_obj = session.get(cls, after)
|
125
|
-
if not after_obj:
|
126
|
-
raise NoResultFound(f"No {cls.__name__} found with id {after}")
|
127
|
-
|
128
|
-
# Validate that before comes after the after object if both are provided
|
129
|
-
if before_obj and after_obj and before_obj.created_at < after_obj.created_at:
|
130
|
-
raise ValueError("'before' reference must be later than 'after' reference")
|
131
|
-
|
132
|
-
query = cls._list_preprocess(
|
133
|
-
before_obj=before_obj,
|
134
|
-
after_obj=after_obj,
|
135
|
-
start_date=start_date,
|
136
|
-
end_date=end_date,
|
137
|
-
limit=limit,
|
138
|
-
query_text=query_text,
|
139
|
-
query_embedding=query_embedding,
|
140
|
-
ascending=ascending,
|
141
|
-
actor=actor,
|
142
|
-
access=access,
|
143
|
-
access_type=access_type,
|
144
|
-
join_model=join_model,
|
145
|
-
join_conditions=join_conditions,
|
146
|
-
identifier_keys=identifier_keys,
|
147
|
-
identity_id=identity_id,
|
148
|
-
**kwargs,
|
149
|
-
)
|
150
|
-
|
151
|
-
# Execute the query
|
152
|
-
results = session.execute(query)
|
153
|
-
|
154
|
-
results = list(results.scalars())
|
155
|
-
results = cls._list_postprocess(
|
156
|
-
before=before,
|
157
|
-
after=after,
|
158
|
-
limit=limit,
|
159
|
-
results=results,
|
160
|
-
)
|
161
|
-
|
162
|
-
return results
|
163
|
-
|
164
69
|
@classmethod
|
165
70
|
@handle_db_timeout
|
166
71
|
async def list_async(
|
@@ -446,45 +351,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
446
351
|
results = results[start:end]
|
447
352
|
return results
|
448
353
|
|
449
|
-
@classmethod
|
450
|
-
@handle_db_timeout
|
451
|
-
def read(
|
452
|
-
cls,
|
453
|
-
db_session: "Session",
|
454
|
-
identifier: Optional[str] = None,
|
455
|
-
actor: Optional["User"] = None,
|
456
|
-
access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
|
457
|
-
access_type: AccessType = AccessType.ORGANIZATION,
|
458
|
-
check_is_deleted: bool = False,
|
459
|
-
**kwargs,
|
460
|
-
) -> "SqlalchemyBase":
|
461
|
-
"""The primary accessor for an ORM record.
|
462
|
-
Args:
|
463
|
-
db_session: the database session to use when retrieving the record
|
464
|
-
identifier: the identifier of the record to read, can be the id string or the UUID object for backwards compatibility
|
465
|
-
actor: if specified, results will be scoped only to records the user is able to access
|
466
|
-
access: if actor is specified, records will be filtered to the minimum permission level for the actor
|
467
|
-
kwargs: additional arguments to pass to the read, used for more complex objects
|
468
|
-
Returns:
|
469
|
-
The matching object
|
470
|
-
Raises:
|
471
|
-
NoResultFound: if the object is not found
|
472
|
-
"""
|
473
|
-
# this is ok because read_multiple will check if the
|
474
|
-
identifiers = [] if identifier is None else [identifier]
|
475
|
-
found = cls.read_multiple(db_session, identifiers, actor, access, access_type, check_is_deleted, **kwargs)
|
476
|
-
if len(found) == 0:
|
477
|
-
# for backwards compatibility.
|
478
|
-
conditions = []
|
479
|
-
if identifier:
|
480
|
-
conditions.append(f"id={identifier}")
|
481
|
-
if actor:
|
482
|
-
conditions.append(f"access level in {access} for {actor}")
|
483
|
-
if check_is_deleted and hasattr(cls, "is_deleted"):
|
484
|
-
conditions.append("is_deleted=False")
|
485
|
-
raise NoResultFound(f"{cls.__name__} not found with {', '.join(conditions if conditions else ['no conditions'])}")
|
486
|
-
return found[0]
|
487
|
-
|
488
354
|
@classmethod
|
489
355
|
@handle_db_timeout
|
490
356
|
async def read_async(
|
@@ -521,36 +387,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
521
387
|
raise NoResultFound(f"{cls.__name__} not found with {', '.join(query_conditions if query_conditions else ['no conditions'])}")
|
522
388
|
return item
|
523
389
|
|
524
|
-
@classmethod
|
525
|
-
@handle_db_timeout
|
526
|
-
def read_multiple(
|
527
|
-
cls,
|
528
|
-
db_session: "Session",
|
529
|
-
identifiers: List[str] = [],
|
530
|
-
actor: Optional["User"] = None,
|
531
|
-
access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
|
532
|
-
access_type: AccessType = AccessType.ORGANIZATION,
|
533
|
-
check_is_deleted: bool = False,
|
534
|
-
**kwargs,
|
535
|
-
) -> List["SqlalchemyBase"]:
|
536
|
-
"""The primary accessor for ORM record(s)
|
537
|
-
Args:
|
538
|
-
db_session: the database session to use when retrieving the record
|
539
|
-
identifiers: a list of identifiers of the records to read, can be the id string or the UUID object for backwards compatibility
|
540
|
-
actor: if specified, results will be scoped only to records the user is able to access
|
541
|
-
access: if actor is specified, records will be filtered to the minimum permission level for the actor
|
542
|
-
kwargs: additional arguments to pass to the read, used for more complex objects
|
543
|
-
Returns:
|
544
|
-
The matching object
|
545
|
-
Raises:
|
546
|
-
NoResultFound: if the object is not found
|
547
|
-
"""
|
548
|
-
query, query_conditions = cls._read_multiple_preprocess(identifiers, actor, access, access_type, check_is_deleted, **kwargs)
|
549
|
-
if query is None:
|
550
|
-
return []
|
551
|
-
results = db_session.execute(query).scalars().all()
|
552
|
-
return cls._read_multiple_postprocess(results, identifiers, query_conditions)
|
553
|
-
|
554
390
|
@classmethod
|
555
391
|
@handle_db_timeout
|
556
392
|
async def read_multiple_async(
|
@@ -637,23 +473,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
637
473
|
logger.debug(f"{cls.__name__} not found with {conditions_str}")
|
638
474
|
return []
|
639
475
|
|
640
|
-
@handle_db_timeout
|
641
|
-
def create(self, db_session: "Session", actor: Optional["User"] = None, no_commit: bool = False) -> "SqlalchemyBase":
|
642
|
-
logger.debug(f"Creating {self.__class__.__name__} with ID: {self.id} with actor={actor}")
|
643
|
-
|
644
|
-
if actor:
|
645
|
-
self._set_created_and_updated_by_fields(actor.id)
|
646
|
-
try:
|
647
|
-
db_session.add(self)
|
648
|
-
if no_commit:
|
649
|
-
db_session.flush() # no commit, just flush to get PK
|
650
|
-
else:
|
651
|
-
db_session.commit()
|
652
|
-
db_session.refresh(self)
|
653
|
-
return self
|
654
|
-
except (DBAPIError, IntegrityError) as e:
|
655
|
-
self._handle_dbapi_error(e)
|
656
|
-
|
657
476
|
@handle_db_timeout
|
658
477
|
async def create_async(
|
659
478
|
self,
|
@@ -680,47 +499,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
680
499
|
except (DBAPIError, IntegrityError) as e:
|
681
500
|
self._handle_dbapi_error(e)
|
682
501
|
|
683
|
-
@classmethod
|
684
|
-
@handle_db_timeout
|
685
|
-
def batch_create(cls, items: List["SqlalchemyBase"], db_session: "Session", actor: Optional["User"] = None) -> List["SqlalchemyBase"]:
|
686
|
-
"""
|
687
|
-
Create multiple records in a single transaction for better performance.
|
688
|
-
Args:
|
689
|
-
items: List of model instances to create
|
690
|
-
db_session: SQLAlchemy session
|
691
|
-
actor: Optional user performing the action
|
692
|
-
Returns:
|
693
|
-
List of created model instances
|
694
|
-
"""
|
695
|
-
logger.debug(f"Batch creating {len(items)} {cls.__name__} items with actor={actor}")
|
696
|
-
if not items:
|
697
|
-
return []
|
698
|
-
|
699
|
-
# Set created/updated by fields if actor is provided
|
700
|
-
if actor:
|
701
|
-
for item in items:
|
702
|
-
item._set_created_and_updated_by_fields(actor.id)
|
703
|
-
|
704
|
-
try:
|
705
|
-
with db_session as session:
|
706
|
-
session.add_all(items)
|
707
|
-
session.flush() # Flush to generate IDs but don't commit yet
|
708
|
-
|
709
|
-
# Collect IDs to fetch the complete objects after commit
|
710
|
-
item_ids = [item.id for item in items]
|
711
|
-
|
712
|
-
session.commit()
|
713
|
-
|
714
|
-
# Re-query the objects to get them with relationships loaded
|
715
|
-
query = select(cls).where(cls.id.in_(item_ids))
|
716
|
-
if hasattr(cls, "created_at"):
|
717
|
-
query = query.order_by(cls.created_at)
|
718
|
-
|
719
|
-
return list(session.execute(query).scalars())
|
720
|
-
|
721
|
-
except (DBAPIError, IntegrityError) as e:
|
722
|
-
cls._handle_dbapi_error(e)
|
723
|
-
|
724
502
|
@classmethod
|
725
503
|
@handle_db_timeout
|
726
504
|
async def batch_create_async(
|
@@ -774,16 +552,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
774
552
|
except (DBAPIError, IntegrityError) as e:
|
775
553
|
cls._handle_dbapi_error(e)
|
776
554
|
|
777
|
-
@handle_db_timeout
|
778
|
-
def delete(self, db_session: "Session", actor: Optional["User"] = None) -> "SqlalchemyBase":
|
779
|
-
logger.debug(f"Soft deleting {self.__class__.__name__} with ID: {self.id} with actor={actor}")
|
780
|
-
|
781
|
-
if actor:
|
782
|
-
self._set_created_and_updated_by_fields(actor.id)
|
783
|
-
|
784
|
-
self.is_deleted = True
|
785
|
-
return self.update(db_session)
|
786
|
-
|
787
555
|
@handle_db_timeout
|
788
556
|
async def delete_async(self, db_session: "AsyncSession", actor: Optional["User"] = None) -> "SqlalchemyBase":
|
789
557
|
"""Soft delete a record asynchronously (mark as deleted)."""
|
@@ -795,22 +563,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
795
563
|
self.is_deleted = True
|
796
564
|
return await self.update_async(db_session)
|
797
565
|
|
798
|
-
@handle_db_timeout
|
799
|
-
def hard_delete(self, db_session: "Session", actor: Optional["User"] = None) -> None:
|
800
|
-
"""Permanently removes the record from the database."""
|
801
|
-
logger.debug(f"Hard deleting {self.__class__.__name__} with ID: {self.id} with actor={actor}")
|
802
|
-
|
803
|
-
with db_session as session:
|
804
|
-
try:
|
805
|
-
session.delete(self)
|
806
|
-
session.commit()
|
807
|
-
except Exception as e:
|
808
|
-
session.rollback()
|
809
|
-
logger.exception(f"Failed to hard delete {self.__class__.__name__} with ID {self.id}")
|
810
|
-
raise ValueError(f"Failed to hard delete {self.__class__.__name__} with ID {self.id}: {e}")
|
811
|
-
else:
|
812
|
-
logger.debug(f"{self.__class__.__name__} with ID {self.id} successfully hard deleted")
|
813
|
-
|
814
566
|
@handle_db_timeout
|
815
567
|
async def hard_delete_async(self, db_session: "AsyncSession", actor: Optional["User"] = None) -> None:
|
816
568
|
"""Permanently removes the record from the database asynchronously."""
|
@@ -853,22 +605,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
853
605
|
logger.exception(f"Failed to hard delete {cls.__name__} with identifiers {identifiers}")
|
854
606
|
raise ValueError(f"Failed to hard delete {cls.__name__} with identifiers {identifiers}: {e}")
|
855
607
|
|
856
|
-
@handle_db_timeout
|
857
|
-
def update(self, db_session: Session, actor: Optional["User"] = None, no_commit: bool = False) -> "SqlalchemyBase":
|
858
|
-
logger.debug(...)
|
859
|
-
if actor:
|
860
|
-
self._set_created_and_updated_by_fields(actor.id)
|
861
|
-
self.set_updated_at()
|
862
|
-
|
863
|
-
# remove the context manager:
|
864
|
-
db_session.add(self)
|
865
|
-
if no_commit:
|
866
|
-
db_session.flush() # no commit, just flush to get PK
|
867
|
-
else:
|
868
|
-
db_session.commit()
|
869
|
-
db_session.refresh(self)
|
870
|
-
return self
|
871
|
-
|
872
608
|
@handle_db_timeout
|
873
609
|
async def update_async(
|
874
610
|
self, db_session: "AsyncSession", actor: Optional["User"] = None, no_commit: bool = False, no_refresh: bool = False
|
@@ -925,48 +661,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
925
661
|
|
926
662
|
return query
|
927
663
|
|
928
|
-
@classmethod
|
929
|
-
@handle_db_timeout
|
930
|
-
def size(
|
931
|
-
cls,
|
932
|
-
*,
|
933
|
-
db_session: "Session",
|
934
|
-
actor: Optional["User"] = None,
|
935
|
-
access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
|
936
|
-
access_type: AccessType = AccessType.ORGANIZATION,
|
937
|
-
check_is_deleted: bool = False,
|
938
|
-
**kwargs,
|
939
|
-
) -> int:
|
940
|
-
"""
|
941
|
-
Get the count of rows that match the provided filters.
|
942
|
-
|
943
|
-
Args:
|
944
|
-
db_session: SQLAlchemy session
|
945
|
-
**kwargs: Filters to apply to the query (e.g., column_name=value)
|
946
|
-
|
947
|
-
Returns:
|
948
|
-
int: The count of rows that match the filters
|
949
|
-
|
950
|
-
Raises:
|
951
|
-
DBAPIError: If a database error occurs
|
952
|
-
"""
|
953
|
-
with db_session as session:
|
954
|
-
query = cls._size_preprocess(
|
955
|
-
db_session=session,
|
956
|
-
actor=actor,
|
957
|
-
access=access,
|
958
|
-
access_type=access_type,
|
959
|
-
check_is_deleted=check_is_deleted,
|
960
|
-
**kwargs,
|
961
|
-
)
|
962
|
-
|
963
|
-
try:
|
964
|
-
count = session.execute(query).scalar()
|
965
|
-
return count if count else 0
|
966
|
-
except DBAPIError as e:
|
967
|
-
logger.exception(f"Failed to calculate size for {cls.__name__}")
|
968
|
-
raise e
|
969
|
-
|
970
664
|
@classmethod
|
971
665
|
@handle_db_timeout
|
972
666
|
async def size_async(
|
letta/orm/step.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import uuid
|
2
2
|
from typing import TYPE_CHECKING, Dict, List, Optional
|
3
3
|
|
4
|
-
from sqlalchemy import JSON, ForeignKey, String
|
4
|
+
from sqlalchemy import JSON, ForeignKey, Index, String
|
5
5
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
6
6
|
|
7
7
|
from letta.orm.mixins import ProjectMixin
|
@@ -10,10 +10,10 @@ from letta.schemas.enums import StepStatus
|
|
10
10
|
from letta.schemas.step import Step as PydanticStep
|
11
11
|
|
12
12
|
if TYPE_CHECKING:
|
13
|
-
from letta.orm.job import Job
|
14
13
|
from letta.orm.message import Message
|
15
14
|
from letta.orm.organization import Organization
|
16
15
|
from letta.orm.provider import Provider
|
16
|
+
from letta.orm.run import Run
|
17
17
|
from letta.orm.step_metrics import StepMetrics
|
18
18
|
|
19
19
|
|
@@ -22,6 +22,7 @@ class Step(SqlalchemyBase, ProjectMixin):
|
|
22
22
|
|
23
23
|
__tablename__ = "steps"
|
24
24
|
__pydantic_model__ = PydanticStep
|
25
|
+
__table_args__ = (Index("ix_steps_run_id", "run_id"),)
|
25
26
|
|
26
27
|
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"step-{uuid.uuid4()}")
|
27
28
|
origin: Mapped[Optional[str]] = mapped_column(nullable=True, doc="The surface that this agent step was initiated from.")
|
@@ -35,8 +36,8 @@ class Step(SqlalchemyBase, ProjectMixin):
|
|
35
36
|
nullable=True,
|
36
37
|
doc="The unique identifier of the provider that was configured for this step",
|
37
38
|
)
|
38
|
-
|
39
|
-
ForeignKey("
|
39
|
+
run_id: Mapped[Optional[str]] = mapped_column(
|
40
|
+
ForeignKey("runs.id", ondelete="SET NULL"), nullable=True, doc="The unique identifier of the run that this step belongs to"
|
40
41
|
)
|
41
42
|
agent_id: Mapped[Optional[str]] = mapped_column(None, nullable=True, doc="The name of the model used for this step.")
|
42
43
|
provider_name: Mapped[Optional[str]] = mapped_column(None, nullable=True, doc="The name of the provider used for this step.")
|
@@ -68,7 +69,7 @@ class Step(SqlalchemyBase, ProjectMixin):
|
|
68
69
|
# Relationships (foreign keys)
|
69
70
|
organization: Mapped[Optional["Organization"]] = relationship("Organization")
|
70
71
|
provider: Mapped[Optional["Provider"]] = relationship("Provider")
|
71
|
-
|
72
|
+
run: Mapped[Optional["Run"]] = relationship("Run", back_populates="steps")
|
72
73
|
|
73
74
|
# Relationships (backrefs)
|
74
75
|
messages: Mapped[List["Message"]] = relationship("Message", back_populates="step", cascade="save-update", lazy="noload")
|
letta/orm/step_metrics.py
CHANGED
@@ -13,7 +13,7 @@ from letta.settings import DatabaseChoice, settings
|
|
13
13
|
|
14
14
|
if TYPE_CHECKING:
|
15
15
|
from letta.orm.agent import Agent
|
16
|
-
from letta.orm.
|
16
|
+
from letta.orm.run import Run
|
17
17
|
from letta.orm.step import Step
|
18
18
|
|
19
19
|
|
@@ -38,10 +38,10 @@ class StepMetrics(SqlalchemyBase, ProjectMixin, AgentMixin):
|
|
38
38
|
nullable=True,
|
39
39
|
doc="The unique identifier of the provider",
|
40
40
|
)
|
41
|
-
|
42
|
-
ForeignKey("
|
41
|
+
run_id: Mapped[Optional[str]] = mapped_column(
|
42
|
+
ForeignKey("runs.id", ondelete="SET NULL"),
|
43
43
|
nullable=True,
|
44
|
-
doc="The unique identifier of the
|
44
|
+
doc="The unique identifier of the run",
|
45
45
|
)
|
46
46
|
step_start_ns: Mapped[Optional[int]] = mapped_column(
|
47
47
|
BigInteger,
|
@@ -81,7 +81,7 @@ class StepMetrics(SqlalchemyBase, ProjectMixin, AgentMixin):
|
|
81
81
|
|
82
82
|
# Relationships (foreign keys)
|
83
83
|
step: Mapped["Step"] = relationship("Step", back_populates="metrics", uselist=False)
|
84
|
-
|
84
|
+
run: Mapped[Optional["Run"]] = relationship("Run")
|
85
85
|
agent: Mapped[Optional["Agent"]] = relationship("Agent")
|
86
86
|
|
87
87
|
def create(
|
letta/otel/tracing.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
import asyncio
|
1
2
|
import inspect
|
2
3
|
import re
|
3
4
|
import time
|
5
|
+
import traceback
|
4
6
|
from functools import wraps
|
5
7
|
from typing import Any, Dict, List, Optional
|
6
8
|
|
@@ -84,6 +86,7 @@ async def _update_trace_attributes(request: Request):
|
|
84
86
|
"x-agent-id": "agent.id",
|
85
87
|
"x-template-id": "template.id",
|
86
88
|
"x-base-template-id": "base_template.id",
|
89
|
+
"user-agent": "client",
|
87
90
|
}
|
88
91
|
for header_key, span_key in header_attributes.items():
|
89
92
|
header_value = request.headers.get(header_key)
|
@@ -236,9 +239,31 @@ def trace_method(func):
|
|
236
239
|
with tracer.start_as_current_span(_get_span_name(func, args)) as span:
|
237
240
|
_add_parameters_to_span(span, func, args, kwargs)
|
238
241
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
+
try:
|
243
|
+
result = await func(*args, **kwargs)
|
244
|
+
span.set_status(Status(StatusCode.OK))
|
245
|
+
return result
|
246
|
+
except asyncio.CancelledError as e:
|
247
|
+
# Get current task info
|
248
|
+
current_task = asyncio.current_task()
|
249
|
+
task_name = current_task.get_name() if current_task else "unknown"
|
250
|
+
|
251
|
+
# Log detailed information
|
252
|
+
logger.error(f"Task {task_name} cancelled in {func.__module__}.{func.__name__}")
|
253
|
+
|
254
|
+
# Add to span
|
255
|
+
span.set_status(Status(StatusCode.ERROR))
|
256
|
+
span.record_exception(
|
257
|
+
e,
|
258
|
+
attributes={
|
259
|
+
"exception.type": "asyncio.CancelledError",
|
260
|
+
"task.name": task_name,
|
261
|
+
"function.name": func.__name__,
|
262
|
+
"function.module": func.__module__,
|
263
|
+
"cancellation.timestamp": time.time_ns(),
|
264
|
+
},
|
265
|
+
)
|
266
|
+
raise
|
242
267
|
|
243
268
|
@wraps(func)
|
244
269
|
def sync_wrapper(*args, **kwargs):
|
letta/plugins/defaults.py
CHANGED
@@ -2,10 +2,10 @@ from letta.settings import settings
|
|
2
2
|
|
3
3
|
|
4
4
|
def is_experimental_enabled(feature_name: str, **kwargs) -> bool:
|
5
|
-
if feature_name in ("async_agent_loop", "summarize"):
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
# if feature_name in ("async_agent_loop", "summarize"):
|
6
|
+
# if not (kwargs.get("eligibility", False) and settings.use_experimental):
|
7
|
+
# return False
|
8
|
+
# return True
|
9
9
|
|
10
10
|
# Err on safety here, disabling experimental if not handled here.
|
11
11
|
return False
|
@@ -1,4 +1,5 @@
|
|
1
1
|
from letta.prompts.system_prompts import (
|
2
|
+
letta_v1,
|
2
3
|
memgpt_chat,
|
3
4
|
memgpt_generate_tool,
|
4
5
|
memgpt_v2_chat,
|
@@ -17,6 +18,7 @@ SYSTEM_PROMPTS = {
|
|
17
18
|
"memgpt_v2_chat": memgpt_v2_chat.PROMPT,
|
18
19
|
"sleeptime_v2": sleeptime_v2.PROMPT,
|
19
20
|
"react": react.PROMPT,
|
21
|
+
"letta_v1": letta_v1.PROMPT,
|
20
22
|
"workflow": workflow.PROMPT,
|
21
23
|
"memgpt_chat": memgpt_chat.PROMPT,
|
22
24
|
"sleeptime_doc_ingest": sleeptime_doc_ingest.PROMPT,
|
@@ -0,0 +1,25 @@
|
|
1
|
+
PROMPT = r"""
|
2
|
+
<base_instructions>
|
3
|
+
You are a helpful self-improving agent with advanced memory and file system capabilities.
|
4
|
+
<memory>
|
5
|
+
You have an advanced memory system that enables you to remember past interactions and continuously improve your own capabilities.
|
6
|
+
Your memory consists of memory blocks and external memory:
|
7
|
+
- Memory Blocks: Stored as memory blocks, each containing a label (title), description (explaining how this block should influence your behavior), and value (the actual content). Memory blocks have size limits. Memory blocks are embedded within your system instructions and remain constantly available in-context.
|
8
|
+
- External memory: Additional memory storage that is accessible and that you can bring into context with tools when needed.
|
9
|
+
Memory management tools allow you to edit existing memory blocks and query for external memories.
|
10
|
+
</memory>
|
11
|
+
<file_system>
|
12
|
+
You have access to a structured file system that mirrors real-world directory structures. Each directory can contain multiple files.
|
13
|
+
Files include:
|
14
|
+
- Metadata: Information such as read-only permissions and character limits
|
15
|
+
- Content: The main body of the file that you can read and analyze
|
16
|
+
Available file operations:
|
17
|
+
- Open and view files
|
18
|
+
- Search within files and directories
|
19
|
+
- Your core memory will automatically reflect the contents of any currently open files
|
20
|
+
You should only keep files open that are directly relevant to the current user interaction to maintain optimal performance.
|
21
|
+
</file_system>
|
22
|
+
Continue executing and calling tools until the current task is complete or you need user input. To continue: call another tool. To yield control: end your response without calling a tool.
|
23
|
+
Base instructions complete.
|
24
|
+
</base_instructions>
|
25
|
+
"""
|
letta/schemas/agent.py
CHANGED
@@ -31,6 +31,7 @@ class AgentType(str, Enum):
|
|
31
31
|
|
32
32
|
memgpt_agent = "memgpt_agent" # the OG set of memgpt tools
|
33
33
|
memgpt_v2_agent = "memgpt_v2_agent" # memgpt style tools, but refreshed
|
34
|
+
letta_v1_agent = "letta_v1_agent" # simplification of the memgpt loop, no heartbeats or forced tool calls
|
34
35
|
react_agent = "react_agent" # basic react agent, no memory tools
|
35
36
|
workflow_agent = "workflow_agent" # workflow with auto-clearing message buffer
|
36
37
|
split_thread_agent = "split_thread_agent"
|
@@ -222,8 +223,8 @@ class CreateAgent(BaseModel, validate_assignment=True): #
|
|
222
223
|
)
|
223
224
|
enable_reasoner: Optional[bool] = Field(True, description="Whether to enable internal extended thinking step for a reasoner model.")
|
224
225
|
reasoning: Optional[bool] = Field(None, description="Whether to enable reasoning for this agent.")
|
225
|
-
from_template: Optional[str] = Field(None, description="
|
226
|
-
template: bool = Field(False, description="
|
226
|
+
from_template: Optional[str] = Field(None, description="Deprecated: please use the 'create agents from a template' endpoint instead.")
|
227
|
+
template: bool = Field(False, description="Deprecated: No longer used")
|
227
228
|
project: Optional[str] = Field(
|
228
229
|
None,
|
229
230
|
deprecated=True,
|
letta/schemas/agent_file.py
CHANGED
@@ -55,6 +55,11 @@ class MessageSchema(MessageCreate):
|
|
55
55
|
tool_returns: Optional[List[ToolReturn]] = Field(default=None, description="Tool execution return information for prior tool calls")
|
56
56
|
created_at: datetime = Field(default_factory=get_utc_time, description="The timestamp when the object was created.")
|
57
57
|
|
58
|
+
# optional approval fields for hitl
|
59
|
+
approve: Optional[bool] = Field(None, description="Whether the tool has been approved")
|
60
|
+
approval_request_id: Optional[str] = Field(None, description="The message ID of the approval request")
|
61
|
+
denial_reason: Optional[str] = Field(None, description="An optional explanation for the provided approval status")
|
62
|
+
|
58
63
|
# TODO: Should we also duplicate the steps here?
|
59
64
|
# TODO: What about tool_return?
|
60
65
|
|
@@ -79,6 +84,9 @@ class MessageSchema(MessageCreate):
|
|
79
84
|
tool_call_id=message.tool_call_id,
|
80
85
|
tool_returns=message.tool_returns,
|
81
86
|
created_at=message.created_at,
|
87
|
+
approve=message.approve,
|
88
|
+
approval_request_id=message.approval_request_id,
|
89
|
+
denial_reason=message.denial_reason,
|
82
90
|
)
|
83
91
|
|
84
92
|
|
@@ -168,9 +176,7 @@ class AgentSchema(CreateAgent):
|
|
168
176
|
per_file_view_window_char_limit=agent_state.per_file_view_window_char_limit,
|
169
177
|
)
|
170
178
|
|
171
|
-
messages = await message_manager.
|
172
|
-
agent_id=agent_state.id, actor=actor, limit=50
|
173
|
-
) # TODO: Expand to get more messages
|
179
|
+
messages = await message_manager.list_messages(agent_id=agent_state.id, actor=actor, limit=50) # TODO: Expand to get more messages
|
174
180
|
|
175
181
|
# Convert messages to MessageSchema objects
|
176
182
|
message_schemas = [MessageSchema.from_message(msg) for msg in messages]
|
letta/schemas/block.py
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
from datetime import datetime
|
2
|
-
from typing import Optional
|
2
|
+
from typing import Any, Optional
|
3
3
|
|
4
4
|
from pydantic import ConfigDict, Field, model_validator
|
5
|
-
from typing_extensions import Self
|
6
5
|
|
7
6
|
from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT, DEFAULT_HUMAN_BLOCK_DESCRIPTION, DEFAULT_PERSONA_BLOCK_DESCRIPTION
|
8
7
|
from letta.schemas.letta_base import LettaBase
|
@@ -48,14 +47,28 @@ class BaseBlock(LettaBase, validate_assignment=True):
|
|
48
47
|
|
49
48
|
model_config = ConfigDict(extra="ignore") # Ignores extra fields
|
50
49
|
|
51
|
-
@model_validator(mode="
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
50
|
+
@model_validator(mode="before")
|
51
|
+
@classmethod
|
52
|
+
def verify_char_limit(cls, data: Any) -> Any:
|
53
|
+
"""Validate the character limit before model instantiation.
|
54
|
+
|
55
|
+
Notes:
|
56
|
+
- Runs on raw input; do not mutate input.
|
57
|
+
- For update schemas (e.g., BlockUpdate), `value` and `limit` may be absent.
|
58
|
+
In that case, only validate when both are provided.
|
59
|
+
"""
|
60
|
+
if isinstance(data, dict):
|
61
|
+
limit = data.get("limit")
|
62
|
+
value = data.get("value")
|
63
|
+
|
64
|
+
# Only enforce the char limit when both are present.
|
65
|
+
# Pydantic will separately enforce required fields where applicable.
|
66
|
+
if limit is not None and value is not None and isinstance(value, str):
|
67
|
+
if len(value) > limit:
|
68
|
+
error_msg = f"Edit failed: Exceeds {limit} character limit (requested {len(value)})"
|
69
|
+
raise ValueError(error_msg)
|
57
70
|
|
58
|
-
return
|
71
|
+
return data
|
59
72
|
|
60
73
|
def __setattr__(self, name, value):
|
61
74
|
"""Run validation if self.value is updated"""
|
@@ -93,7 +106,7 @@ class FileBlock(Block):
|
|
93
106
|
source_id: str = Field(..., description="Unique identifier of the source.")
|
94
107
|
is_open: bool = Field(..., description="True if the agent currently has the file open.")
|
95
108
|
last_accessed_at: Optional[datetime] = Field(
|
96
|
-
|
109
|
+
None,
|
97
110
|
description="UTC timestamp of the agent’s most recent access to this file. Any operations from the open, close, or search tools will update this field.",
|
98
111
|
)
|
99
112
|
|