letta-nightly 0.7.15.dev20250514104255__py3-none-any.whl → 0.7.16.dev20250515205957__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 (36) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +12 -0
  3. letta/agents/helpers.py +48 -5
  4. letta/agents/letta_agent.py +46 -18
  5. letta/agents/letta_agent_batch.py +44 -26
  6. letta/agents/voice_sleeptime_agent.py +6 -4
  7. letta/client/client.py +16 -1
  8. letta/constants.py +3 -0
  9. letta/functions/async_composio_toolset.py +1 -1
  10. letta/groups/sleeptime_multi_agent.py +1 -0
  11. letta/interfaces/anthropic_streaming_interface.py +40 -6
  12. letta/jobs/llm_batch_job_polling.py +6 -2
  13. letta/orm/agent.py +102 -1
  14. letta/orm/block.py +3 -0
  15. letta/orm/sqlalchemy_base.py +365 -133
  16. letta/schemas/agent.py +10 -2
  17. letta/schemas/block.py +3 -0
  18. letta/schemas/memory.py +7 -2
  19. letta/server/rest_api/routers/v1/agents.py +13 -13
  20. letta/server/rest_api/routers/v1/messages.py +6 -6
  21. letta/server/rest_api/routers/v1/tools.py +3 -3
  22. letta/server/server.py +74 -0
  23. letta/services/agent_manager.py +421 -7
  24. letta/services/block_manager.py +12 -8
  25. letta/services/helpers/agent_manager_helper.py +19 -0
  26. letta/services/job_manager.py +99 -0
  27. letta/services/llm_batch_manager.py +28 -27
  28. letta/services/message_manager.py +51 -19
  29. letta/services/tool_executor/tool_executor.py +19 -1
  30. letta/services/tool_manager.py +13 -3
  31. letta/types/__init__.py +0 -0
  32. {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/METADATA +3 -3
  33. {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/RECORD +36 -35
  34. {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/LICENSE +0 -0
  35. {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/WHEEL +0 -0
  36. {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/entry_points.txt +0 -0
@@ -114,154 +114,323 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
114
114
  if before_obj and after_obj and before_obj.created_at < after_obj.created_at:
115
115
  raise ValueError("'before' reference must be later than 'after' reference")
116
116
 
117
- query = select(cls)
117
+ query = cls._list_preprocess(
118
+ before_obj=before_obj,
119
+ after_obj=after_obj,
120
+ start_date=start_date,
121
+ end_date=end_date,
122
+ limit=limit,
123
+ query_text=query_text,
124
+ query_embedding=query_embedding,
125
+ ascending=ascending,
126
+ tags=tags,
127
+ match_all_tags=match_all_tags,
128
+ actor=actor,
129
+ access=access,
130
+ access_type=access_type,
131
+ join_model=join_model,
132
+ join_conditions=join_conditions,
133
+ identifier_keys=identifier_keys,
134
+ identity_id=identity_id,
135
+ **kwargs,
136
+ )
137
+
138
+ # Execute the query
139
+ results = session.execute(query)
140
+
141
+ results = list(results.scalars())
142
+ results = cls._list_postprocess(
143
+ before=before,
144
+ after=after,
145
+ limit=limit,
146
+ results=results,
147
+ )
118
148
 
119
- if join_model and join_conditions:
120
- query = query.join(join_model, and_(*join_conditions))
149
+ return results
121
150
 
122
- # Apply access predicate if actor is provided
123
- if actor:
124
- query = cls.apply_access_predicate(query, actor, access, access_type)
151
+ @classmethod
152
+ @handle_db_timeout
153
+ async def list_async(
154
+ cls,
155
+ *,
156
+ db_session: "AsyncSession",
157
+ before: Optional[str] = None,
158
+ after: Optional[str] = None,
159
+ start_date: Optional[datetime] = None,
160
+ end_date: Optional[datetime] = None,
161
+ limit: Optional[int] = 50,
162
+ query_text: Optional[str] = None,
163
+ query_embedding: Optional[List[float]] = None,
164
+ ascending: bool = True,
165
+ tags: Optional[List[str]] = None,
166
+ match_all_tags: bool = False,
167
+ actor: Optional["User"] = None,
168
+ access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
169
+ access_type: AccessType = AccessType.ORGANIZATION,
170
+ join_model: Optional[Base] = None,
171
+ join_conditions: Optional[Union[Tuple, List]] = None,
172
+ identifier_keys: Optional[List[str]] = None,
173
+ identity_id: Optional[str] = None,
174
+ **kwargs,
175
+ ) -> List["SqlalchemyBase"]:
176
+ """
177
+ Async version of list method above.
178
+ NOTE: Keep in sync.
179
+ List records with before/after pagination, ordering by created_at.
180
+ Can use both before and after to fetch a window of records.
125
181
 
126
- # Handle tag filtering if the model has tags
127
- if tags and hasattr(cls, "tags"):
128
- query = select(cls)
129
-
130
- if match_all_tags:
131
- # Match ALL tags - use subqueries
132
- subquery = (
133
- select(cls.tags.property.mapper.class_.agent_id)
134
- .where(cls.tags.property.mapper.class_.tag.in_(tags))
135
- .group_by(cls.tags.property.mapper.class_.agent_id)
136
- .having(func.count() == len(tags))
137
- )
138
- query = query.filter(cls.id.in_(subquery))
139
- else:
140
- # Match ANY tag - use join and filter
141
- query = (
142
- query.join(cls.tags).filter(cls.tags.property.mapper.class_.tag.in_(tags)).distinct(cls.id).order_by(cls.id)
143
- ) # Deduplicate results
182
+ Args:
183
+ db_session: SQLAlchemy session
184
+ before: ID of item to paginate before (upper bound)
185
+ after: ID of item to paginate after (lower bound)
186
+ start_date: Filter items after this date
187
+ end_date: Filter items before this date
188
+ limit: Maximum number of items to return
189
+ query_text: Text to search for
190
+ query_embedding: Vector to search for similar embeddings
191
+ ascending: Sort direction
192
+ tags: List of tags to filter by
193
+ match_all_tags: If True, return items matching all tags. If False, match any tag.
194
+ **kwargs: Additional filters to apply
195
+ """
196
+ if start_date and end_date and start_date > end_date:
197
+ raise ValueError("start_date must be earlier than or equal to end_date")
198
+
199
+ logger.debug(f"Listing {cls.__name__} with kwarg filters {kwargs}")
200
+
201
+ async with db_session as session:
202
+ # Get the reference objects for pagination
203
+ before_obj = None
204
+ after_obj = None
144
205
 
145
- # select distinct primary key
146
- query = query.distinct(cls.id).order_by(cls.id)
206
+ if before:
207
+ before_obj = await session.get(cls, before)
208
+ if not before_obj:
209
+ raise NoResultFound(f"No {cls.__name__} found with id {before}")
147
210
 
148
- if identifier_keys and hasattr(cls, "identities"):
149
- query = query.join(cls.identities).filter(cls.identities.property.mapper.class_.identifier_key.in_(identifier_keys))
211
+ if after:
212
+ after_obj = await session.get(cls, after)
213
+ if not after_obj:
214
+ raise NoResultFound(f"No {cls.__name__} found with id {after}")
150
215
 
151
- # given the identity_id, we can find within the agents table any agents that have the identity_id in their identity_ids
152
- if identity_id and hasattr(cls, "identities"):
153
- query = query.join(cls.identities).filter(cls.identities.property.mapper.class_.id == identity_id)
216
+ # Validate that before comes after the after object if both are provided
217
+ if before_obj and after_obj and before_obj.created_at < after_obj.created_at:
218
+ raise ValueError("'before' reference must be later than 'after' reference")
154
219
 
155
- # Apply filtering logic from kwargs
156
- for key, value in kwargs.items():
157
- if "." in key:
158
- # Handle joined table columns
159
- table_name, column_name = key.split(".")
160
- joined_table = locals().get(table_name) or globals().get(table_name)
161
- column = getattr(joined_table, column_name)
162
- else:
163
- # Handle columns from main table
164
- column = getattr(cls, key)
165
-
166
- if isinstance(value, (list, tuple, set)):
167
- query = query.where(column.in_(value))
168
- else:
169
- query = query.where(column == value)
170
-
171
- # Date range filtering
172
- if start_date:
173
- query = query.filter(cls.created_at > start_date)
174
- if end_date:
175
- query = query.filter(cls.created_at < end_date)
176
-
177
- # Handle pagination based on before/after
178
- if before or after:
179
- conditions = []
180
-
181
- if before and after:
182
- # Window-based query - get records between before and after
183
- conditions = [
184
- or_(cls.created_at < before_obj.created_at, and_(cls.created_at == before_obj.created_at, cls.id < before_obj.id)),
185
- or_(cls.created_at > after_obj.created_at, and_(cls.created_at == after_obj.created_at, cls.id > after_obj.id)),
186
- ]
187
- else:
188
- # Pure pagination query
189
- if before:
190
- conditions.append(
191
- or_(
192
- cls.created_at < before_obj.created_at,
193
- and_(cls.created_at == before_obj.created_at, cls.id < before_obj.id),
194
- )
220
+ query = cls._list_preprocess(
221
+ before_obj=before_obj,
222
+ after_obj=after_obj,
223
+ start_date=start_date,
224
+ end_date=end_date,
225
+ limit=limit,
226
+ query_text=query_text,
227
+ query_embedding=query_embedding,
228
+ ascending=ascending,
229
+ tags=tags,
230
+ match_all_tags=match_all_tags,
231
+ actor=actor,
232
+ access=access,
233
+ access_type=access_type,
234
+ join_model=join_model,
235
+ join_conditions=join_conditions,
236
+ identifier_keys=identifier_keys,
237
+ identity_id=identity_id,
238
+ **kwargs,
239
+ )
240
+
241
+ # Execute the query
242
+ results = await session.execute(query)
243
+
244
+ results = list(results.scalars())
245
+ results = cls._list_postprocess(
246
+ before=before,
247
+ after=after,
248
+ limit=limit,
249
+ results=results,
250
+ )
251
+
252
+ return results
253
+
254
+ @classmethod
255
+ def _list_preprocess(
256
+ cls,
257
+ *,
258
+ before_obj,
259
+ after_obj,
260
+ start_date: Optional[datetime] = None,
261
+ end_date: Optional[datetime] = None,
262
+ limit: Optional[int] = 50,
263
+ query_text: Optional[str] = None,
264
+ query_embedding: Optional[List[float]] = None,
265
+ ascending: bool = True,
266
+ tags: Optional[List[str]] = None,
267
+ match_all_tags: bool = False,
268
+ actor: Optional["User"] = None,
269
+ access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
270
+ access_type: AccessType = AccessType.ORGANIZATION,
271
+ join_model: Optional[Base] = None,
272
+ join_conditions: Optional[Union[Tuple, List]] = None,
273
+ identifier_keys: Optional[List[str]] = None,
274
+ identity_id: Optional[str] = None,
275
+ **kwargs,
276
+ ):
277
+ """
278
+ Constructs the query for listing records.
279
+ """
280
+ query = select(cls)
281
+
282
+ if join_model and join_conditions:
283
+ query = query.join(join_model, and_(*join_conditions))
284
+
285
+ # Apply access predicate if actor is provided
286
+ if actor:
287
+ query = cls.apply_access_predicate(query, actor, access, access_type)
288
+
289
+ # Handle tag filtering if the model has tags
290
+ if tags and hasattr(cls, "tags"):
291
+ query = select(cls)
292
+
293
+ if match_all_tags:
294
+ # Match ALL tags - use subqueries
295
+ subquery = (
296
+ select(cls.tags.property.mapper.class_.agent_id)
297
+ .where(cls.tags.property.mapper.class_.tag.in_(tags))
298
+ .group_by(cls.tags.property.mapper.class_.agent_id)
299
+ .having(func.count() == len(tags))
300
+ )
301
+ query = query.filter(cls.id.in_(subquery))
302
+ else:
303
+ # Match ANY tag - use join and filter
304
+ query = (
305
+ query.join(cls.tags).filter(cls.tags.property.mapper.class_.tag.in_(tags)).distinct(cls.id).order_by(cls.id)
306
+ ) # Deduplicate results
307
+
308
+ # select distinct primary key
309
+ query = query.distinct(cls.id).order_by(cls.id)
310
+
311
+ if identifier_keys and hasattr(cls, "identities"):
312
+ query = query.join(cls.identities).filter(cls.identities.property.mapper.class_.identifier_key.in_(identifier_keys))
313
+
314
+ # given the identity_id, we can find within the agents table any agents that have the identity_id in their identity_ids
315
+ if identity_id and hasattr(cls, "identities"):
316
+ query = query.join(cls.identities).filter(cls.identities.property.mapper.class_.id == identity_id)
317
+
318
+ # Apply filtering logic from kwargs
319
+ for key, value in kwargs.items():
320
+ if "." in key:
321
+ # Handle joined table columns
322
+ table_name, column_name = key.split(".")
323
+ joined_table = locals().get(table_name) or globals().get(table_name)
324
+ column = getattr(joined_table, column_name)
325
+ else:
326
+ # Handle columns from main table
327
+ column = getattr(cls, key)
328
+
329
+ if isinstance(value, (list, tuple, set)):
330
+ query = query.where(column.in_(value))
331
+ else:
332
+ query = query.where(column == value)
333
+
334
+ # Date range filtering
335
+ if start_date:
336
+ query = query.filter(cls.created_at > start_date)
337
+ if end_date:
338
+ query = query.filter(cls.created_at < end_date)
339
+
340
+ # Handle pagination based on before/after
341
+ if before_obj or after_obj:
342
+ conditions = []
343
+
344
+ if before_obj and after_obj:
345
+ # Window-based query - get records between before and after
346
+ conditions = [
347
+ or_(cls.created_at < before_obj.created_at, and_(cls.created_at == before_obj.created_at, cls.id < before_obj.id)),
348
+ or_(cls.created_at > after_obj.created_at, and_(cls.created_at == after_obj.created_at, cls.id > after_obj.id)),
349
+ ]
350
+ else:
351
+ # Pure pagination query
352
+ if before_obj:
353
+ conditions.append(
354
+ or_(
355
+ cls.created_at < before_obj.created_at,
356
+ and_(cls.created_at == before_obj.created_at, cls.id < before_obj.id),
195
357
  )
196
- if after:
197
- conditions.append(
198
- or_(
199
- cls.created_at > after_obj.created_at,
200
- and_(cls.created_at == after_obj.created_at, cls.id > after_obj.id),
201
- )
358
+ )
359
+ if after_obj:
360
+ conditions.append(
361
+ or_(
362
+ cls.created_at > after_obj.created_at,
363
+ and_(cls.created_at == after_obj.created_at, cls.id > after_obj.id),
202
364
  )
203
-
204
- if conditions:
205
- query = query.where(and_(*conditions))
206
-
207
- # Text search
208
- if query_text:
209
- if hasattr(cls, "text"):
210
- query = query.filter(func.lower(cls.text).contains(func.lower(query_text)))
211
- elif hasattr(cls, "name"):
212
- # Special case for Agent model - search across name
213
- query = query.filter(func.lower(cls.name).contains(func.lower(query_text)))
214
-
215
- # Embedding search (for Passages)
216
- is_ordered = False
217
- if query_embedding:
218
- if not hasattr(cls, "embedding"):
219
- raise ValueError(f"Class {cls.__name__} does not have an embedding column")
220
-
221
- from letta.settings import settings
222
-
223
- if settings.letta_pg_uri_no_default:
224
- # PostgreSQL with pgvector
225
- query = query.order_by(cls.embedding.cosine_distance(query_embedding).asc())
226
- else:
227
- # SQLite with custom vector type
228
- query_embedding_binary = adapt_array(query_embedding)
229
- query = query.order_by(
230
- func.cosine_distance(cls.embedding, query_embedding_binary).asc(),
231
- cls.created_at.asc() if ascending else cls.created_at.desc(),
232
- cls.id.asc(),
233
365
  )
234
- is_ordered = True
235
366
 
236
- # Handle soft deletes
237
- if hasattr(cls, "is_deleted"):
238
- query = query.where(cls.is_deleted == False)
367
+ if conditions:
368
+ query = query.where(and_(*conditions))
369
+
370
+ # Text search
371
+ if query_text:
372
+ if hasattr(cls, "text"):
373
+ query = query.filter(func.lower(cls.text).contains(func.lower(query_text)))
374
+ elif hasattr(cls, "name"):
375
+ # Special case for Agent model - search across name
376
+ query = query.filter(func.lower(cls.name).contains(func.lower(query_text)))
239
377
 
240
- # Apply ordering
241
- if not is_ordered:
242
- if ascending:
243
- query = query.order_by(cls.created_at.asc(), cls.id.asc())
244
- else:
245
- query = query.order_by(cls.created_at.desc(), cls.id.desc())
246
-
247
- # Apply limit, adjusting for both bounds if necessary
248
- if before and after:
249
- # When both bounds are provided, we need to fetch enough records to satisfy
250
- # the limit while respecting both bounds. We'll fetch more and then trim.
251
- query = query.limit(limit * 2)
378
+ # Embedding search (for Passages)
379
+ is_ordered = False
380
+ if query_embedding:
381
+ if not hasattr(cls, "embedding"):
382
+ raise ValueError(f"Class {cls.__name__} does not have an embedding column")
383
+
384
+ from letta.settings import settings
385
+
386
+ if settings.letta_pg_uri_no_default:
387
+ # PostgreSQL with pgvector
388
+ query = query.order_by(cls.embedding.cosine_distance(query_embedding).asc())
252
389
  else:
253
- query = query.limit(limit)
390
+ # SQLite with custom vector type
391
+ query_embedding_binary = adapt_array(query_embedding)
392
+ query = query.order_by(
393
+ func.cosine_distance(cls.embedding, query_embedding_binary).asc(),
394
+ cls.created_at.asc() if ascending else cls.created_at.desc(),
395
+ cls.id.asc(),
396
+ )
397
+ is_ordered = True
398
+
399
+ # Handle soft deletes
400
+ if hasattr(cls, "is_deleted"):
401
+ query = query.where(cls.is_deleted == False)
254
402
 
255
- results = list(session.execute(query).scalars())
403
+ # Apply ordering
404
+ if not is_ordered:
405
+ if ascending:
406
+ query = query.order_by(cls.created_at.asc(), cls.id.asc())
407
+ else:
408
+ query = query.order_by(cls.created_at.desc(), cls.id.desc())
256
409
 
257
- # If we have both bounds, take the middle portion
258
- if before and after and len(results) > limit:
259
- middle = len(results) // 2
260
- start = max(0, middle - limit // 2)
261
- end = min(len(results), start + limit)
262
- results = results[start:end]
410
+ # Apply limit, adjusting for both bounds if necessary
411
+ if before_obj and after_obj:
412
+ # When both bounds are provided, we need to fetch enough records to satisfy
413
+ # the limit while respecting both bounds. We'll fetch more and then trim.
414
+ query = query.limit(limit * 2)
415
+ else:
416
+ query = query.limit(limit)
417
+ return query
263
418
 
264
- return results
419
+ @classmethod
420
+ def _list_postprocess(
421
+ cls,
422
+ before: str | None,
423
+ after: str | None,
424
+ limit: int | None,
425
+ results: list,
426
+ ):
427
+ # If we have both bounds, take the middle portion
428
+ if before and after and len(results) > limit:
429
+ middle = len(results) // 2
430
+ start = max(0, middle - limit // 2)
431
+ end = min(len(results), start + limit)
432
+ results = results[start:end]
433
+ return results
265
434
 
266
435
  @classmethod
267
436
  @handle_db_timeout
@@ -305,7 +474,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
305
474
  @handle_db_timeout
306
475
  async def read_async(
307
476
  cls,
308
- db_session: "Session",
477
+ db_session: "AsyncSession",
309
478
  identifier: Optional[str] = None,
310
479
  actor: Optional["User"] = None,
311
480
  access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
@@ -462,6 +631,24 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
462
631
  except (DBAPIError, IntegrityError) as e:
463
632
  self._handle_dbapi_error(e)
464
633
 
634
+ @handle_db_timeout
635
+ async def create_async(self, db_session: "AsyncSession", actor: Optional["User"] = None, no_commit: bool = False) -> "SqlalchemyBase":
636
+ """Async version of create function"""
637
+ logger.debug(f"Creating {self.__class__.__name__} with ID: {self.id} with actor={actor}")
638
+
639
+ if actor:
640
+ self._set_created_and_updated_by_fields(actor.id)
641
+ try:
642
+ db_session.add(self)
643
+ if no_commit:
644
+ await db_session.flush() # no commit, just flush to get PK
645
+ else:
646
+ await db_session.commit()
647
+ await db_session.refresh(self)
648
+ return self
649
+ except (DBAPIError, IntegrityError) as e:
650
+ self._handle_dbapi_error(e)
651
+
465
652
  @classmethod
466
653
  @handle_db_timeout
467
654
  def batch_create(cls, items: List["SqlalchemyBase"], db_session: "Session", actor: Optional["User"] = None) -> List["SqlalchemyBase"]:
@@ -503,6 +690,51 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
503
690
  except (DBAPIError, IntegrityError) as e:
504
691
  cls._handle_dbapi_error(e)
505
692
 
693
+ @classmethod
694
+ @handle_db_timeout
695
+ async def batch_create_async(
696
+ cls, items: List["SqlalchemyBase"], db_session: "AsyncSession", actor: Optional["User"] = None
697
+ ) -> List["SqlalchemyBase"]:
698
+ """
699
+ Async version of batch_create method.
700
+ Create multiple records in a single transaction for better performance.
701
+ Args:
702
+ items: List of model instances to create
703
+ db_session: AsyncSession session
704
+ actor: Optional user performing the action
705
+ Returns:
706
+ List of created model instances
707
+ """
708
+ logger.debug(f"Async batch creating {len(items)} {cls.__name__} items with actor={actor}")
709
+ if not items:
710
+ return []
711
+
712
+ # Set created/updated by fields if actor is provided
713
+ if actor:
714
+ for item in items:
715
+ item._set_created_and_updated_by_fields(actor.id)
716
+
717
+ try:
718
+ async with db_session as session:
719
+ session.add_all(items)
720
+ await session.flush() # Flush to generate IDs but don't commit yet
721
+
722
+ # Collect IDs to fetch the complete objects after commit
723
+ item_ids = [item.id for item in items]
724
+
725
+ await session.commit()
726
+
727
+ # Re-query the objects to get them with relationships loaded
728
+ query = select(cls).where(cls.id.in_(item_ids))
729
+ if hasattr(cls, "created_at"):
730
+ query = query.order_by(cls.created_at)
731
+
732
+ result = await session.execute(query)
733
+ return list(result.scalars())
734
+
735
+ except (DBAPIError, IntegrityError) as e:
736
+ cls._handle_dbapi_error(e)
737
+
506
738
  @handle_db_timeout
507
739
  def delete(self, db_session: "Session", actor: Optional["User"] = None) -> "SqlalchemyBase":
508
740
  logger.debug(f"Soft deleting {self.__class__.__name__} with ID: {self.id} with actor={actor}")
letta/schemas/agent.py CHANGED
@@ -312,9 +312,17 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
312
312
  )
313
313
  return (
314
314
  "{% for block in blocks %}"
315
- '<{{ block.label }} characters="{{ block.value|length }}/{{ block.limit }}">\n'
315
+ "<{{ block.label }}>\n"
316
+ "<description>\n"
317
+ "{{ block.description }}\n"
318
+ "</description>\n"
319
+ "<metadata>\n"
320
+ '{% if block.read_only %}read_only="true" {% endif %}chars_current="{{ block.value|length }}" chars_limit="{{ block.limit }}"\n'
321
+ "</metadata>\n"
322
+ "<value>\n"
316
323
  "{{ block.value }}\n"
317
- "</{{ block.label }}>"
324
+ "</value>\n"
325
+ "</{{ block.label }}>\n"
318
326
  "{% if not loop.last %}\n{% endif %}"
319
327
  "{% endfor %}"
320
328
  )
letta/schemas/block.py CHANGED
@@ -25,6 +25,9 @@ class BaseBlock(LettaBase, validate_assignment=True):
25
25
  # context window label
26
26
  label: Optional[str] = Field(None, description="Label of the block (e.g. 'human', 'persona') in the context window.")
27
27
 
28
+ # permissions of the agent
29
+ read_only: bool = Field(False, description="Whether the agent has read-only access to the block.")
30
+
28
31
  # metadata
29
32
  description: Optional[str] = Field(None, description="Description of the block.")
30
33
  metadata: Optional[dict] = Field({}, description="Metadata of the block.")
letta/schemas/memory.py CHANGED
@@ -69,9 +69,14 @@ class Memory(BaseModel, validate_assignment=True):
69
69
  # Memory.template is a Jinja2 template for compiling memory module into a prompt string.
70
70
  prompt_template: str = Field(
71
71
  default="{% for block in blocks %}"
72
- '<{{ block.label }} characters="{{ block.value|length }}/{{ block.limit }}">\n'
72
+ "<{{ block.label }}>\n"
73
+ "<metadata>"
74
+ 'read_only="{{ block.read_only}}" chars_current="{{ block.value|length }}" chars_limit="{{ block.limit }}"'
75
+ "</metadata>"
76
+ "<value>"
73
77
  "{{ block.value }}\n"
74
- "</{{ block.label }}>"
78
+ "</value>"
79
+ "</{{ block.label }}>\n"
75
80
  "{% if not loop.last %}\n{% endif %}"
76
81
  "{% endfor %}",
77
82
  description="Jinja2 template for compiling memory blocks into a prompt string",