letta-nightly 0.7.30.dev20250603104343__py3-none-any.whl → 0.8.0.dev20250604104349__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 (136) hide show
  1. letta/__init__.py +7 -1
  2. letta/agent.py +14 -7
  3. letta/agents/base_agent.py +1 -0
  4. letta/agents/ephemeral_summary_agent.py +104 -0
  5. letta/agents/helpers.py +35 -3
  6. letta/agents/letta_agent.py +492 -176
  7. letta/agents/letta_agent_batch.py +22 -16
  8. letta/agents/prompts/summary_system_prompt.txt +62 -0
  9. letta/agents/voice_agent.py +22 -7
  10. letta/agents/voice_sleeptime_agent.py +13 -8
  11. letta/constants.py +33 -1
  12. letta/data_sources/connectors.py +52 -36
  13. letta/errors.py +4 -0
  14. letta/functions/ast_parsers.py +13 -30
  15. letta/functions/function_sets/base.py +3 -1
  16. letta/functions/functions.py +2 -0
  17. letta/functions/mcp_client/base_client.py +151 -97
  18. letta/functions/mcp_client/sse_client.py +49 -31
  19. letta/functions/mcp_client/stdio_client.py +107 -106
  20. letta/functions/schema_generator.py +22 -22
  21. letta/groups/helpers.py +3 -4
  22. letta/groups/sleeptime_multi_agent.py +4 -4
  23. letta/groups/sleeptime_multi_agent_v2.py +22 -0
  24. letta/helpers/composio_helpers.py +16 -0
  25. letta/helpers/converters.py +20 -0
  26. letta/helpers/datetime_helpers.py +1 -6
  27. letta/helpers/tool_rule_solver.py +2 -1
  28. letta/interfaces/anthropic_streaming_interface.py +17 -2
  29. letta/interfaces/openai_chat_completions_streaming_interface.py +1 -0
  30. letta/interfaces/openai_streaming_interface.py +18 -2
  31. letta/llm_api/anthropic_client.py +24 -3
  32. letta/llm_api/google_ai_client.py +0 -15
  33. letta/llm_api/google_vertex_client.py +6 -5
  34. letta/llm_api/llm_client_base.py +15 -0
  35. letta/llm_api/openai.py +2 -2
  36. letta/llm_api/openai_client.py +60 -8
  37. letta/orm/__init__.py +2 -0
  38. letta/orm/agent.py +45 -43
  39. letta/orm/base.py +0 -2
  40. letta/orm/block.py +1 -0
  41. letta/orm/custom_columns.py +13 -0
  42. letta/orm/enums.py +5 -0
  43. letta/orm/file.py +3 -1
  44. letta/orm/files_agents.py +68 -0
  45. letta/orm/mcp_server.py +48 -0
  46. letta/orm/message.py +1 -0
  47. letta/orm/organization.py +11 -2
  48. letta/orm/passage.py +25 -10
  49. letta/orm/sandbox_config.py +5 -2
  50. letta/orm/sqlalchemy_base.py +171 -110
  51. letta/prompts/system/memgpt_base.txt +6 -1
  52. letta/prompts/system/memgpt_v2_chat.txt +57 -0
  53. letta/prompts/system/sleeptime.txt +2 -0
  54. letta/prompts/system/sleeptime_v2.txt +28 -0
  55. letta/schemas/agent.py +87 -20
  56. letta/schemas/block.py +7 -1
  57. letta/schemas/file.py +57 -0
  58. letta/schemas/mcp.py +74 -0
  59. letta/schemas/memory.py +5 -2
  60. letta/schemas/message.py +9 -0
  61. letta/schemas/openai/openai.py +0 -6
  62. letta/schemas/providers.py +33 -4
  63. letta/schemas/tool.py +26 -21
  64. letta/schemas/tool_execution_result.py +5 -0
  65. letta/server/db.py +23 -8
  66. letta/server/rest_api/app.py +73 -56
  67. letta/server/rest_api/interface.py +4 -4
  68. letta/server/rest_api/routers/v1/agents.py +132 -47
  69. letta/server/rest_api/routers/v1/blocks.py +3 -2
  70. letta/server/rest_api/routers/v1/embeddings.py +3 -3
  71. letta/server/rest_api/routers/v1/groups.py +3 -3
  72. letta/server/rest_api/routers/v1/jobs.py +14 -17
  73. letta/server/rest_api/routers/v1/organizations.py +10 -10
  74. letta/server/rest_api/routers/v1/providers.py +12 -10
  75. letta/server/rest_api/routers/v1/runs.py +3 -3
  76. letta/server/rest_api/routers/v1/sandbox_configs.py +12 -12
  77. letta/server/rest_api/routers/v1/sources.py +108 -43
  78. letta/server/rest_api/routers/v1/steps.py +8 -6
  79. letta/server/rest_api/routers/v1/tools.py +134 -95
  80. letta/server/rest_api/utils.py +12 -1
  81. letta/server/server.py +272 -73
  82. letta/services/agent_manager.py +246 -313
  83. letta/services/block_manager.py +30 -9
  84. letta/services/context_window_calculator/__init__.py +0 -0
  85. letta/services/context_window_calculator/context_window_calculator.py +150 -0
  86. letta/services/context_window_calculator/token_counter.py +82 -0
  87. letta/services/file_processor/__init__.py +0 -0
  88. letta/services/file_processor/chunker/__init__.py +0 -0
  89. letta/services/file_processor/chunker/llama_index_chunker.py +29 -0
  90. letta/services/file_processor/embedder/__init__.py +0 -0
  91. letta/services/file_processor/embedder/openai_embedder.py +84 -0
  92. letta/services/file_processor/file_processor.py +123 -0
  93. letta/services/file_processor/parser/__init__.py +0 -0
  94. letta/services/file_processor/parser/base_parser.py +9 -0
  95. letta/services/file_processor/parser/mistral_parser.py +54 -0
  96. letta/services/file_processor/types.py +0 -0
  97. letta/services/files_agents_manager.py +184 -0
  98. letta/services/group_manager.py +118 -0
  99. letta/services/helpers/agent_manager_helper.py +76 -21
  100. letta/services/helpers/tool_execution_helper.py +3 -0
  101. letta/services/helpers/tool_parser_helper.py +100 -0
  102. letta/services/identity_manager.py +44 -42
  103. letta/services/job_manager.py +21 -10
  104. letta/services/mcp/base_client.py +5 -2
  105. letta/services/mcp/sse_client.py +3 -5
  106. letta/services/mcp/stdio_client.py +3 -5
  107. letta/services/mcp_manager.py +281 -0
  108. letta/services/message_manager.py +40 -26
  109. letta/services/organization_manager.py +55 -19
  110. letta/services/passage_manager.py +211 -13
  111. letta/services/provider_manager.py +48 -2
  112. letta/services/sandbox_config_manager.py +105 -0
  113. letta/services/source_manager.py +4 -5
  114. letta/services/step_manager.py +9 -6
  115. letta/services/summarizer/summarizer.py +50 -23
  116. letta/services/telemetry_manager.py +7 -0
  117. letta/services/tool_executor/tool_execution_manager.py +11 -52
  118. letta/services/tool_executor/tool_execution_sandbox.py +4 -34
  119. letta/services/tool_executor/tool_executor.py +107 -105
  120. letta/services/tool_manager.py +56 -17
  121. letta/services/tool_sandbox/base.py +39 -92
  122. letta/services/tool_sandbox/e2b_sandbox.py +16 -11
  123. letta/services/tool_sandbox/local_sandbox.py +51 -23
  124. letta/services/user_manager.py +36 -3
  125. letta/settings.py +10 -3
  126. letta/templates/__init__.py +0 -0
  127. letta/templates/sandbox_code_file.py.j2 +47 -0
  128. letta/templates/template_helper.py +16 -0
  129. letta/tracing.py +30 -1
  130. letta/types/__init__.py +7 -0
  131. letta/utils.py +25 -1
  132. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/METADATA +7 -2
  133. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/RECORD +136 -110
  134. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/LICENSE +0 -0
  135. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/WHEEL +0 -0
  136. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/entry_points.txt +0 -0
letta/orm/organization.py CHANGED
@@ -6,12 +6,20 @@ from letta.orm.sqlalchemy_base import SqlalchemyBase
6
6
  from letta.schemas.organization import Organization as PydanticOrganization
7
7
 
8
8
  if TYPE_CHECKING:
9
-
10
9
  from letta.orm.agent import Agent
10
+ from letta.orm.agent_passage import AgentPassage
11
+ from letta.orm.block import Block
11
12
  from letta.orm.file import FileMetadata
13
+ from letta.orm.group import Group
12
14
  from letta.orm.identity import Identity
15
+ from letta.orm.llm_batch_item import LLMBatchItem
16
+ from letta.orm.llm_batch_job import LLMBatchJob
17
+ from letta.orm.message import Message
13
18
  from letta.orm.provider import Provider
14
- from letta.orm.sandbox_config import AgentEnvironmentVariable
19
+ from letta.orm.sandbox_config import AgentEnvironmentVariable, SandboxConfig
20
+ from letta.orm.sandbox_environment_variable import SandboxEnvironmentVariable
21
+ from letta.orm.source import Source
22
+ from letta.orm.source_passage import SourcePassage
15
23
  from letta.orm.tool import Tool
16
24
  from letta.orm.user import User
17
25
 
@@ -28,6 +36,7 @@ class Organization(SqlalchemyBase):
28
36
  # relationships
29
37
  users: Mapped[List["User"]] = relationship("User", back_populates="organization", cascade="all, delete-orphan")
30
38
  tools: Mapped[List["Tool"]] = relationship("Tool", back_populates="organization", cascade="all, delete-orphan")
39
+ # mcp_servers: Mapped[List["MCPServer"]] = relationship("MCPServer", back_populates="organization", cascade="all, delete-orphan")
31
40
  blocks: Mapped[List["Block"]] = relationship("Block", back_populates="organization", cascade="all, delete-orphan")
32
41
  sources: Mapped[List["Source"]] = relationship("Source", back_populates="organization", cascade="all, delete-orphan")
33
42
  files: Mapped[List["FileMetadata"]] = relationship("FileMetadata", back_populates="organization", cascade="all, delete-orphan")
letta/orm/passage.py CHANGED
@@ -41,16 +41,6 @@ class BasePassage(SqlalchemyBase, OrganizationMixin):
41
41
  """Relationship to organization"""
42
42
  return relationship("Organization", back_populates="passages", lazy="selectin")
43
43
 
44
- @declared_attr
45
- def __table_args__(cls):
46
- if settings.letta_pg_uri_no_default:
47
- return (
48
- Index(f"{cls.__tablename__}_org_idx", "organization_id"),
49
- Index(f"{cls.__tablename__}_created_at_id_idx", "created_at", "id"),
50
- {"extend_existing": True},
51
- )
52
- return (Index(f"{cls.__tablename__}_created_at_id_idx", "created_at", "id"), {"extend_existing": True})
53
-
54
44
 
55
45
  class SourcePassage(BasePassage, FileMixin, SourceMixin):
56
46
  """Passages derived from external files/sources"""
@@ -66,6 +56,16 @@ class SourcePassage(BasePassage, FileMixin, SourceMixin):
66
56
  def organization(cls) -> Mapped["Organization"]:
67
57
  return relationship("Organization", back_populates="source_passages", lazy="selectin")
68
58
 
59
+ @declared_attr
60
+ def __table_args__(cls):
61
+ if settings.letta_pg_uri_no_default:
62
+ return (
63
+ Index("source_passages_org_idx", "organization_id"),
64
+ Index("source_passages_created_at_id_idx", "created_at", "id"),
65
+ {"extend_existing": True},
66
+ )
67
+ return (Index("source_passages_created_at_id_idx", "created_at", "id"), {"extend_existing": True})
68
+
69
69
  @declared_attr
70
70
  def source(cls) -> Mapped["Source"]:
71
71
  """Relationship to source"""
@@ -80,3 +80,18 @@ class AgentPassage(BasePassage, AgentMixin):
80
80
  @declared_attr
81
81
  def organization(cls) -> Mapped["Organization"]:
82
82
  return relationship("Organization", back_populates="agent_passages", lazy="selectin")
83
+
84
+ @declared_attr
85
+ def __table_args__(cls):
86
+ if settings.letta_pg_uri_no_default:
87
+ return (
88
+ Index("agent_passages_org_idx", "organization_id"),
89
+ Index("ix_agent_passages_org_agent", "organization_id", "agent_id"),
90
+ Index("agent_passages_created_at_id_idx", "created_at", "id"),
91
+ {"extend_existing": True},
92
+ )
93
+ return (
94
+ Index("ix_agent_passages_org_agent", "organization_id", "agent_id"),
95
+ Index("agent_passages_created_at_id_idx", "created_at", "id"),
96
+ {"extend_existing": True},
97
+ )
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional
3
3
 
4
4
  from sqlalchemy import JSON
5
5
  from sqlalchemy import Enum as SqlEnum
6
- from sqlalchemy import String, UniqueConstraint
6
+ from sqlalchemy import Index, String, UniqueConstraint
7
7
  from sqlalchemy.orm import Mapped, mapped_column, relationship
8
8
 
9
9
  from letta.orm.mixins import AgentMixin, OrganizationMixin, SandboxConfigMixin
@@ -61,7 +61,10 @@ class AgentEnvironmentVariable(SqlalchemyBase, OrganizationMixin, AgentMixin):
61
61
 
62
62
  __tablename__ = "agent_environment_variables"
63
63
  # We cannot have duplicate key names for the same agent, the env var would get overwritten
64
- __table_args__ = (UniqueConstraint("key", "agent_id", name="uix_key_agent"),)
64
+ __table_args__ = (
65
+ UniqueConstraint("key", "agent_id", name="uix_key_agent"),
66
+ Index("idx_agent_environment_variables_agent_id", "agent_id"),
67
+ )
65
68
 
66
69
  # agent_env_var generates its own id
67
70
  # TODO: We want to migrate all the ORM models to do this, so we will need to move this to the SqlalchemyBase
@@ -4,7 +4,7 @@ from functools import wraps
4
4
  from pprint import pformat
5
5
  from typing import TYPE_CHECKING, List, Literal, Optional, Tuple, Union
6
6
 
7
- from sqlalchemy import String, and_, func, or_, select
7
+ from sqlalchemy import String, and_, delete, func, or_, select, text
8
8
  from sqlalchemy.exc import DBAPIError, IntegrityError, TimeoutError
9
9
  from sqlalchemy.ext.asyncio import AsyncSession
10
10
  from sqlalchemy.orm import Mapped, Session, mapped_column
@@ -188,56 +188,55 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
188
188
 
189
189
  logger.debug(f"Listing {cls.__name__} with kwarg filters {kwargs}")
190
190
 
191
- async with db_session as session:
192
- # Get the reference objects for pagination
193
- before_obj = None
194
- after_obj = None
195
-
196
- if before:
197
- before_obj = await session.get(cls, before)
198
- if not before_obj:
199
- raise NoResultFound(f"No {cls.__name__} found with id {before}")
200
-
201
- if after:
202
- after_obj = await session.get(cls, after)
203
- if not after_obj:
204
- raise NoResultFound(f"No {cls.__name__} found with id {after}")
205
-
206
- # Validate that before comes after the after object if both are provided
207
- if before_obj and after_obj and before_obj.created_at < after_obj.created_at:
208
- raise ValueError("'before' reference must be later than 'after' reference")
209
-
210
- query = cls._list_preprocess(
211
- before_obj=before_obj,
212
- after_obj=after_obj,
213
- start_date=start_date,
214
- end_date=end_date,
215
- limit=limit,
216
- query_text=query_text,
217
- query_embedding=query_embedding,
218
- ascending=ascending,
219
- actor=actor,
220
- access=access,
221
- access_type=access_type,
222
- join_model=join_model,
223
- join_conditions=join_conditions,
224
- identifier_keys=identifier_keys,
225
- identity_id=identity_id,
226
- **kwargs,
227
- )
191
+ # Get the reference objects for pagination
192
+ before_obj = None
193
+ after_obj = None
194
+
195
+ if before:
196
+ before_obj = await db_session.get(cls, before)
197
+ if not before_obj:
198
+ raise NoResultFound(f"No {cls.__name__} found with id {before}")
199
+
200
+ if after:
201
+ after_obj = await db_session.get(cls, after)
202
+ if not after_obj:
203
+ raise NoResultFound(f"No {cls.__name__} found with id {after}")
204
+
205
+ # Validate that before comes after the after object if both are provided
206
+ if before_obj and after_obj and before_obj.created_at < after_obj.created_at:
207
+ raise ValueError("'before' reference must be later than 'after' reference")
208
+
209
+ query = cls._list_preprocess(
210
+ before_obj=before_obj,
211
+ after_obj=after_obj,
212
+ start_date=start_date,
213
+ end_date=end_date,
214
+ limit=limit,
215
+ query_text=query_text,
216
+ query_embedding=query_embedding,
217
+ ascending=ascending,
218
+ actor=actor,
219
+ access=access,
220
+ access_type=access_type,
221
+ join_model=join_model,
222
+ join_conditions=join_conditions,
223
+ identifier_keys=identifier_keys,
224
+ identity_id=identity_id,
225
+ **kwargs,
226
+ )
227
+
228
+ # Execute the query
229
+ results = await db_session.execute(query)
228
230
 
229
- # Execute the query
230
- results = await session.execute(query)
231
+ results = list(results.scalars())
232
+ results = cls._list_postprocess(
233
+ before=before,
234
+ after=after,
235
+ limit=limit,
236
+ results=results,
237
+ )
231
238
 
232
- results = list(results.scalars())
233
- results = cls._list_postprocess(
234
- before=before,
235
- after=after,
236
- limit=limit,
237
- results=results,
238
- )
239
-
240
- return results
239
+ return results
241
240
 
242
241
  @classmethod
243
242
  def _list_preprocess(
@@ -258,6 +257,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
258
257
  join_conditions: Optional[Union[Tuple, List]] = None,
259
258
  identifier_keys: Optional[List[str]] = None,
260
259
  identity_id: Optional[str] = None,
260
+ check_is_deleted: bool = False,
261
261
  **kwargs,
262
262
  ):
263
263
  """
@@ -280,15 +280,28 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
280
280
  query = query.join(cls.identities).filter(cls.identities.property.mapper.class_.id == identity_id)
281
281
 
282
282
  # Apply filtering logic from kwargs
283
+ # 1 part: <column> // 2 parts: <table>.<column> OR <column>.<json_key> // 3 parts: <table>.<column>.<json_key>
284
+ # TODO (cliandy): can make this more robust down the line
283
285
  for key, value in kwargs.items():
284
- if "." in key:
285
- # Handle joined table columns
286
- table_name, column_name = key.split(".")
286
+ parts = key.split(".")
287
+ if len(parts) == 1:
288
+ column = getattr(cls, key)
289
+ elif len(parts) == 2:
290
+ if locals().get(parts[0]) or globals().get(parts[0]):
291
+ # It's a joined table column
292
+ joined_table = locals().get(parts[0]) or globals().get(parts[0])
293
+ column = getattr(joined_table, parts[1])
294
+ else:
295
+ # It's a JSON field on the main table
296
+ column = getattr(cls, parts[0])
297
+ column = column.op("->>")(parts[1])
298
+ elif len(parts) == 3:
299
+ table_name, column_name, json_key = parts
287
300
  joined_table = locals().get(table_name) or globals().get(table_name)
288
301
  column = getattr(joined_table, column_name)
302
+ column = column.op("->>")(json_key)
289
303
  else:
290
- # Handle columns from main table
291
- column = getattr(cls, key)
304
+ raise ValueError(f"Unhandled column name {key}")
292
305
 
293
306
  if isinstance(value, (list, tuple, set)):
294
307
  query = query.where(column.in_(value))
@@ -361,7 +374,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
361
374
  is_ordered = True
362
375
 
363
376
  # Handle soft deletes
364
- if hasattr(cls, "is_deleted"):
377
+ if check_is_deleted and hasattr(cls, "is_deleted"):
365
378
  query = query.where(cls.is_deleted == False)
366
379
 
367
380
  # Apply ordering
@@ -405,6 +418,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
405
418
  actor: Optional["User"] = None,
406
419
  access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
407
420
  access_type: AccessType = AccessType.ORGANIZATION,
421
+ check_is_deleted: bool = False,
408
422
  **kwargs,
409
423
  ) -> "SqlalchemyBase":
410
424
  """The primary accessor for an ORM record.
@@ -421,7 +435,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
421
435
  """
422
436
  # this is ok because read_multiple will check if the
423
437
  identifiers = [] if identifier is None else [identifier]
424
- found = cls.read_multiple(db_session, identifiers, actor, access, access_type, **kwargs)
438
+ found = cls.read_multiple(db_session, identifiers, actor, access, access_type, check_is_deleted, **kwargs)
425
439
  if len(found) == 0:
426
440
  # for backwards compatibility.
427
441
  conditions = []
@@ -429,7 +443,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
429
443
  conditions.append(f"id={identifier}")
430
444
  if actor:
431
445
  conditions.append(f"access level in {access} for {actor}")
432
- if hasattr(cls, "is_deleted"):
446
+ if check_is_deleted and hasattr(cls, "is_deleted"):
433
447
  conditions.append("is_deleted=False")
434
448
  raise NoResultFound(f"{cls.__name__} not found with {', '.join(conditions if conditions else ['no conditions'])}")
435
449
  return found[0]
@@ -443,6 +457,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
443
457
  actor: Optional["User"] = None,
444
458
  access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
445
459
  access_type: AccessType = AccessType.ORGANIZATION,
460
+ check_is_deleted: bool = False,
446
461
  **kwargs,
447
462
  ) -> "SqlalchemyBase":
448
463
  """The primary accessor for an ORM record. Async version of read method.
@@ -457,20 +472,18 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
457
472
  Raises:
458
473
  NoResultFound: if the object is not found
459
474
  """
460
- # this is ok because read_multiple will check if the
461
475
  identifiers = [] if identifier is None else [identifier]
462
- found = await cls.read_multiple_async(db_session, identifiers, actor, access, access_type, **kwargs)
463
- if len(found) == 0:
464
- # for backwards compatibility.
465
- conditions = []
466
- if identifier:
467
- conditions.append(f"id={identifier}")
468
- if actor:
469
- conditions.append(f"access level in {access} for {actor}")
470
- if hasattr(cls, "is_deleted"):
471
- conditions.append("is_deleted=False")
472
- raise NoResultFound(f"{cls.__name__} not found with {', '.join(conditions if conditions else ['no conditions'])}")
473
- return found[0]
476
+ query, query_conditions = cls._read_multiple_preprocess(identifiers, actor, access, access_type, check_is_deleted, **kwargs)
477
+ await db_session.execute(text("SET LOCAL enable_seqscan = OFF"))
478
+ try:
479
+ result = await db_session.execute(query)
480
+ item = result.scalar_one_or_none()
481
+ finally:
482
+ await db_session.execute(text("SET LOCAL enable_seqscan = ON"))
483
+
484
+ if item is None:
485
+ raise NoResultFound(f"{cls.__name__} not found with {', '.join(query_conditions if query_conditions else ['no conditions'])}")
486
+ return item
474
487
 
475
488
  @classmethod
476
489
  @handle_db_timeout
@@ -481,6 +494,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
481
494
  actor: Optional["User"] = None,
482
495
  access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
483
496
  access_type: AccessType = AccessType.ORGANIZATION,
497
+ check_is_deleted: bool = False,
484
498
  **kwargs,
485
499
  ) -> List["SqlalchemyBase"]:
486
500
  """The primary accessor for ORM record(s)
@@ -495,7 +509,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
495
509
  Raises:
496
510
  NoResultFound: if the object is not found
497
511
  """
498
- query, query_conditions = cls._read_multiple_preprocess(identifiers, actor, access, access_type, **kwargs)
512
+ query, query_conditions = cls._read_multiple_preprocess(identifiers, actor, access, access_type, check_is_deleted, **kwargs)
499
513
  results = db_session.execute(query).scalars().all()
500
514
  return cls._read_multiple_postprocess(results, identifiers, query_conditions)
501
515
 
@@ -508,13 +522,14 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
508
522
  actor: Optional["User"] = None,
509
523
  access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
510
524
  access_type: AccessType = AccessType.ORGANIZATION,
525
+ check_is_deleted: bool = False,
511
526
  **kwargs,
512
527
  ) -> List["SqlalchemyBase"]:
513
528
  """
514
529
  Async version of read_multiple(...)
515
530
  The primary accessor for ORM record(s)
516
531
  """
517
- query, query_conditions = cls._read_multiple_preprocess(identifiers, actor, access, access_type, **kwargs)
532
+ query, query_conditions = cls._read_multiple_preprocess(identifiers, actor, access, access_type, check_is_deleted, **kwargs)
518
533
  results = await db_session.execute(query)
519
534
  return cls._read_multiple_postprocess(results.scalars().all(), identifiers, query_conditions)
520
535
 
@@ -525,6 +540,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
525
540
  actor: Optional["User"],
526
541
  access: Optional[List[Literal["read", "write", "admin"]]],
527
542
  access_type: AccessType,
543
+ check_is_deleted: bool,
528
544
  **kwargs,
529
545
  ):
530
546
  logger.debug(f"Reading {cls.__name__} with ID(s): {identifiers} with actor={actor}")
@@ -535,8 +551,11 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
535
551
  query_conditions = []
536
552
 
537
553
  # If an identifier is provided, add it to the query conditions
538
- if len(identifiers) > 0:
539
- query = query.where(cls.id.in_(identifiers))
554
+ if identifiers:
555
+ if len(identifiers) == 1:
556
+ query = query.where(cls.id == identifiers[0])
557
+ else:
558
+ query = query.where(cls.id.in_(identifiers))
540
559
  query_conditions.append(f"id='{identifiers}'")
541
560
  elif not kwargs:
542
561
  logger.debug(f"No identifiers provided for {cls.__name__}, returning empty list")
@@ -550,7 +569,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
550
569
  query = cls.apply_access_predicate(query, actor, access, access_type)
551
570
  query_conditions.append(f"access level in {access} for actor='{actor}'")
552
571
 
553
- if hasattr(cls, "is_deleted"):
572
+ if check_is_deleted and hasattr(cls, "is_deleted"):
554
573
  query = query.where(cls.is_deleted == False)
555
574
  query_conditions.append("is_deleted=False")
556
575
 
@@ -679,22 +698,21 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
679
698
  item._set_created_and_updated_by_fields(actor.id)
680
699
 
681
700
  try:
682
- async with db_session as session:
683
- session.add_all(items)
684
- await session.flush() # Flush to generate IDs but don't commit yet
701
+ db_session.add_all(items)
702
+ await db_session.flush() # Flush to generate IDs but don't commit yet
685
703
 
686
- # Collect IDs to fetch the complete objects after commit
687
- item_ids = [item.id for item in items]
704
+ # Collect IDs to fetch the complete objects after commit
705
+ item_ids = [item.id for item in items]
688
706
 
689
- await session.commit()
707
+ await db_session.commit()
690
708
 
691
- # Re-query the objects to get them with relationships loaded
692
- query = select(cls).where(cls.id.in_(item_ids))
693
- if hasattr(cls, "created_at"):
694
- query = query.order_by(cls.created_at)
709
+ # Re-query the objects to get them with relationships loaded
710
+ query = select(cls).where(cls.id.in_(item_ids))
711
+ if hasattr(cls, "created_at"):
712
+ query = query.order_by(cls.created_at)
695
713
 
696
- result = await session.execute(query)
697
- return list(result.scalars())
714
+ result = await db_session.execute(query)
715
+ return list(result.scalars())
698
716
 
699
717
  except (DBAPIError, IntegrityError) as e:
700
718
  cls._handle_dbapi_error(e)
@@ -741,14 +759,42 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
741
759
  """Permanently removes the record from the database asynchronously."""
742
760
  logger.debug(f"Hard deleting {self.__class__.__name__} with ID: {self.id} with actor={actor} (async)")
743
761
 
744
- async with db_session as session:
745
- try:
746
- await session.delete(self)
747
- await session.commit()
748
- except Exception as e:
749
- await session.rollback()
750
- logger.exception(f"Failed to hard delete {self.__class__.__name__} with ID {self.id}")
751
- raise ValueError(f"Failed to hard delete {self.__class__.__name__} with ID {self.id}: {e}")
762
+ try:
763
+ await db_session.delete(self)
764
+ await db_session.commit()
765
+ except Exception as e:
766
+ await db_session.rollback()
767
+ logger.exception(f"Failed to hard delete {self.__class__.__name__} with ID {self.id}")
768
+ raise ValueError(f"Failed to hard delete {self.__class__.__name__} with ID {self.id}: {e}")
769
+
770
+ @classmethod
771
+ @handle_db_timeout
772
+ async def bulk_hard_delete_async(
773
+ cls,
774
+ db_session: "AsyncSession",
775
+ identifiers: List[str],
776
+ actor: Optional["User"],
777
+ access: Optional[List[Literal["read", "write", "admin"]]] = ["write"],
778
+ access_type: AccessType = AccessType.ORGANIZATION,
779
+ ) -> None:
780
+ """Permanently removes the record from the database asynchronously."""
781
+ logger.debug(f"Hard deleting {cls.__name__} with IDs: {identifiers} with actor={actor} (async)")
782
+
783
+ if len(identifiers) == 0:
784
+ logger.debug(f"No identifiers provided for {cls.__name__}, nothing to delete")
785
+ return
786
+
787
+ query = delete(cls)
788
+ query = query.where(cls.id.in_(identifiers))
789
+ query = cls.apply_access_predicate(query, actor, access, access_type)
790
+ try:
791
+ result = await db_session.execute(query)
792
+ await db_session.commit()
793
+ logger.debug(f"Successfully deleted {result.rowcount} {cls.__name__} records")
794
+ except Exception as e:
795
+ await db_session.rollback()
796
+ logger.exception(f"Failed to hard delete {cls.__name__} with identifiers {identifiers}")
797
+ raise ValueError(f"Failed to hard delete {cls.__name__} with identifiers {identifiers}: {e}")
752
798
 
753
799
  @handle_db_timeout
754
800
  def update(self, db_session: Session, actor: Optional["User"] = None, no_commit: bool = False) -> "SqlalchemyBase":
@@ -790,10 +836,11 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
790
836
  actor: Optional["User"] = None,
791
837
  access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
792
838
  access_type: AccessType = AccessType.ORGANIZATION,
839
+ check_is_deleted: bool = False,
793
840
  **kwargs,
794
841
  ):
795
842
  logger.debug(f"Calculating size for {cls.__name__} with filters {kwargs}")
796
- query = select(func.count()).select_from(cls)
843
+ query = select(func.count(1)).select_from(cls)
797
844
 
798
845
  if actor:
799
846
  query = cls.apply_access_predicate(query, actor, access, access_type)
@@ -809,8 +856,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
809
856
  else: # Single value for equality filtering
810
857
  query = query.where(column == value)
811
858
 
812
- # Handle soft deletes if the class has the 'is_deleted' attribute
813
- if hasattr(cls, "is_deleted"):
859
+ if check_is_deleted and hasattr(cls, "is_deleted"):
814
860
  query = query.where(cls.is_deleted == False)
815
861
 
816
862
  return query
@@ -824,6 +870,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
824
870
  actor: Optional["User"] = None,
825
871
  access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
826
872
  access_type: AccessType = AccessType.ORGANIZATION,
873
+ check_is_deleted: bool = False,
827
874
  **kwargs,
828
875
  ) -> int:
829
876
  """
@@ -840,7 +887,14 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
840
887
  DBAPIError: If a database error occurs
841
888
  """
842
889
  with db_session as session:
843
- query = cls._size_preprocess(db_session=session, actor=actor, access=access, access_type=access_type, **kwargs)
890
+ query = cls._size_preprocess(
891
+ db_session=session,
892
+ actor=actor,
893
+ access=access,
894
+ access_type=access_type,
895
+ check_is_deleted=check_is_deleted,
896
+ **kwargs,
897
+ )
844
898
 
845
899
  try:
846
900
  count = session.execute(query).scalar()
@@ -858,6 +912,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
858
912
  actor: Optional["User"] = None,
859
913
  access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
860
914
  access_type: AccessType = AccessType.ORGANIZATION,
915
+ check_is_deleted: bool = False,
861
916
  **kwargs,
862
917
  ) -> int:
863
918
  """
@@ -870,16 +925,22 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
870
925
  Raises:
871
926
  DBAPIError: If a database error occurs
872
927
  """
873
- async with db_session as session:
874
- query = cls._size_preprocess(db_session=session, actor=actor, access=access, access_type=access_type, **kwargs)
928
+ query = cls._size_preprocess(
929
+ db_session=db_session,
930
+ actor=actor,
931
+ access=access,
932
+ access_type=access_type,
933
+ check_is_deleted=check_is_deleted,
934
+ **kwargs,
935
+ )
875
936
 
876
- try:
877
- result = await session.execute(query)
878
- count = result.scalar()
879
- return count if count else 0
880
- except DBAPIError as e:
881
- logger.exception(f"Failed to calculate size for {cls.__name__}")
882
- raise e
937
+ try:
938
+ result = await db_session.execute(query)
939
+ count = result.scalar()
940
+ return count if count else 0
941
+ except DBAPIError as e:
942
+ logger.exception(f"Failed to calculate size for {cls.__name__}")
943
+ raise e
883
944
 
884
945
  @classmethod
885
946
  def apply_access_predicate(
@@ -905,12 +966,12 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
905
966
  org_id = getattr(actor, "organization_id", None)
906
967
  if not org_id:
907
968
  raise ValueError(f"object {actor} has no organization accessor")
908
- return query.where(cls.organization_id == org_id, cls.is_deleted == False)
969
+ return query.where(cls.organization_id == org_id)
909
970
  elif access_type == AccessType.USER:
910
971
  user_id = getattr(actor, "id", None)
911
972
  if not user_id:
912
973
  raise ValueError(f"object {actor} has no user accessor")
913
- return query.where(cls.user_id == user_id, cls.is_deleted == False)
974
+ return query.where(cls.user_id == user_id)
914
975
  else:
915
976
  raise ValueError(f"unknown access_type: {access_type}")
916
977
 
@@ -41,9 +41,14 @@ You can edit your core memory using the 'core_memory_append' and 'core_memory_re
41
41
 
42
42
  Archival memory (infinite size):
43
43
  Your archival memory is infinite size, but is held outside of your immediate context, so you must explicitly run a retrieval/search operation to see data inside it.
44
- A more structured and deep storage space for your reflections, insights, or any other data that doesn't fit into the core memory but is essential enough not to be left only to the 'recall memory'.
44
+ A more structured and deep storage space for your reflections, insights, or any memories that arise from interacting with the user doesn't fit into the core memory but is essential enough not to be left only to the 'recall memory'.
45
45
  You can write to your archival memory using the 'archival_memory_insert' and 'archival_memory_search' functions.
46
46
  There is no function to search your core memory, because it is always visible in your context window (inside the initial system message).
47
47
 
48
+ Data sources:
49
+ You may be given access to external sources of data, relevant to the user's interaction. For example, code, style guides, and documentation relevant
50
+ to the current interaction with the user. Your core memory will contain information about the contents of these data sources. You will have access
51
+ to functions to open and close the files as a filesystem and maintain only the files that are relevant to the user's interaction.
52
+
48
53
  Base instructions finished.
49
54
  From now on, you are going to act as your persona.