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.
- letta/__init__.py +7 -1
- letta/agent.py +14 -7
- letta/agents/base_agent.py +1 -0
- letta/agents/ephemeral_summary_agent.py +104 -0
- letta/agents/helpers.py +35 -3
- letta/agents/letta_agent.py +492 -176
- letta/agents/letta_agent_batch.py +22 -16
- letta/agents/prompts/summary_system_prompt.txt +62 -0
- letta/agents/voice_agent.py +22 -7
- letta/agents/voice_sleeptime_agent.py +13 -8
- letta/constants.py +33 -1
- letta/data_sources/connectors.py +52 -36
- letta/errors.py +4 -0
- letta/functions/ast_parsers.py +13 -30
- letta/functions/function_sets/base.py +3 -1
- letta/functions/functions.py +2 -0
- letta/functions/mcp_client/base_client.py +151 -97
- letta/functions/mcp_client/sse_client.py +49 -31
- letta/functions/mcp_client/stdio_client.py +107 -106
- letta/functions/schema_generator.py +22 -22
- letta/groups/helpers.py +3 -4
- letta/groups/sleeptime_multi_agent.py +4 -4
- letta/groups/sleeptime_multi_agent_v2.py +22 -0
- letta/helpers/composio_helpers.py +16 -0
- letta/helpers/converters.py +20 -0
- letta/helpers/datetime_helpers.py +1 -6
- letta/helpers/tool_rule_solver.py +2 -1
- letta/interfaces/anthropic_streaming_interface.py +17 -2
- letta/interfaces/openai_chat_completions_streaming_interface.py +1 -0
- letta/interfaces/openai_streaming_interface.py +18 -2
- letta/llm_api/anthropic_client.py +24 -3
- letta/llm_api/google_ai_client.py +0 -15
- letta/llm_api/google_vertex_client.py +6 -5
- letta/llm_api/llm_client_base.py +15 -0
- letta/llm_api/openai.py +2 -2
- letta/llm_api/openai_client.py +60 -8
- letta/orm/__init__.py +2 -0
- letta/orm/agent.py +45 -43
- letta/orm/base.py +0 -2
- letta/orm/block.py +1 -0
- letta/orm/custom_columns.py +13 -0
- letta/orm/enums.py +5 -0
- letta/orm/file.py +3 -1
- letta/orm/files_agents.py +68 -0
- letta/orm/mcp_server.py +48 -0
- letta/orm/message.py +1 -0
- letta/orm/organization.py +11 -2
- letta/orm/passage.py +25 -10
- letta/orm/sandbox_config.py +5 -2
- letta/orm/sqlalchemy_base.py +171 -110
- letta/prompts/system/memgpt_base.txt +6 -1
- letta/prompts/system/memgpt_v2_chat.txt +57 -0
- letta/prompts/system/sleeptime.txt +2 -0
- letta/prompts/system/sleeptime_v2.txt +28 -0
- letta/schemas/agent.py +87 -20
- letta/schemas/block.py +7 -1
- letta/schemas/file.py +57 -0
- letta/schemas/mcp.py +74 -0
- letta/schemas/memory.py +5 -2
- letta/schemas/message.py +9 -0
- letta/schemas/openai/openai.py +0 -6
- letta/schemas/providers.py +33 -4
- letta/schemas/tool.py +26 -21
- letta/schemas/tool_execution_result.py +5 -0
- letta/server/db.py +23 -8
- letta/server/rest_api/app.py +73 -56
- letta/server/rest_api/interface.py +4 -4
- letta/server/rest_api/routers/v1/agents.py +132 -47
- letta/server/rest_api/routers/v1/blocks.py +3 -2
- letta/server/rest_api/routers/v1/embeddings.py +3 -3
- letta/server/rest_api/routers/v1/groups.py +3 -3
- letta/server/rest_api/routers/v1/jobs.py +14 -17
- letta/server/rest_api/routers/v1/organizations.py +10 -10
- letta/server/rest_api/routers/v1/providers.py +12 -10
- letta/server/rest_api/routers/v1/runs.py +3 -3
- letta/server/rest_api/routers/v1/sandbox_configs.py +12 -12
- letta/server/rest_api/routers/v1/sources.py +108 -43
- letta/server/rest_api/routers/v1/steps.py +8 -6
- letta/server/rest_api/routers/v1/tools.py +134 -95
- letta/server/rest_api/utils.py +12 -1
- letta/server/server.py +272 -73
- letta/services/agent_manager.py +246 -313
- letta/services/block_manager.py +30 -9
- letta/services/context_window_calculator/__init__.py +0 -0
- letta/services/context_window_calculator/context_window_calculator.py +150 -0
- letta/services/context_window_calculator/token_counter.py +82 -0
- letta/services/file_processor/__init__.py +0 -0
- letta/services/file_processor/chunker/__init__.py +0 -0
- letta/services/file_processor/chunker/llama_index_chunker.py +29 -0
- letta/services/file_processor/embedder/__init__.py +0 -0
- letta/services/file_processor/embedder/openai_embedder.py +84 -0
- letta/services/file_processor/file_processor.py +123 -0
- letta/services/file_processor/parser/__init__.py +0 -0
- letta/services/file_processor/parser/base_parser.py +9 -0
- letta/services/file_processor/parser/mistral_parser.py +54 -0
- letta/services/file_processor/types.py +0 -0
- letta/services/files_agents_manager.py +184 -0
- letta/services/group_manager.py +118 -0
- letta/services/helpers/agent_manager_helper.py +76 -21
- letta/services/helpers/tool_execution_helper.py +3 -0
- letta/services/helpers/tool_parser_helper.py +100 -0
- letta/services/identity_manager.py +44 -42
- letta/services/job_manager.py +21 -10
- letta/services/mcp/base_client.py +5 -2
- letta/services/mcp/sse_client.py +3 -5
- letta/services/mcp/stdio_client.py +3 -5
- letta/services/mcp_manager.py +281 -0
- letta/services/message_manager.py +40 -26
- letta/services/organization_manager.py +55 -19
- letta/services/passage_manager.py +211 -13
- letta/services/provider_manager.py +48 -2
- letta/services/sandbox_config_manager.py +105 -0
- letta/services/source_manager.py +4 -5
- letta/services/step_manager.py +9 -6
- letta/services/summarizer/summarizer.py +50 -23
- letta/services/telemetry_manager.py +7 -0
- letta/services/tool_executor/tool_execution_manager.py +11 -52
- letta/services/tool_executor/tool_execution_sandbox.py +4 -34
- letta/services/tool_executor/tool_executor.py +107 -105
- letta/services/tool_manager.py +56 -17
- letta/services/tool_sandbox/base.py +39 -92
- letta/services/tool_sandbox/e2b_sandbox.py +16 -11
- letta/services/tool_sandbox/local_sandbox.py +51 -23
- letta/services/user_manager.py +36 -3
- letta/settings.py +10 -3
- letta/templates/__init__.py +0 -0
- letta/templates/sandbox_code_file.py.j2 +47 -0
- letta/templates/template_helper.py +16 -0
- letta/tracing.py +30 -1
- letta/types/__init__.py +7 -0
- letta/utils.py +25 -1
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/METADATA +7 -2
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/RECORD +136 -110
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/WHEEL +0 -0
- {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
|
+
)
|
letta/orm/sandbox_config.py
CHANGED
@@ -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__ = (
|
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
|
letta/orm/sqlalchemy_base.py
CHANGED
@@ -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
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
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
|
-
|
230
|
-
|
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
|
-
|
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
|
-
|
285
|
-
|
286
|
-
|
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
|
-
|
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
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
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
|
539
|
-
|
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
|
-
|
683
|
-
|
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
|
-
|
687
|
-
|
704
|
+
# Collect IDs to fetch the complete objects after commit
|
705
|
+
item_ids = [item.id for item in items]
|
688
706
|
|
689
|
-
|
707
|
+
await db_session.commit()
|
690
708
|
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
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
|
-
|
697
|
-
|
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
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
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
|
-
|
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(
|
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
|
-
|
874
|
-
|
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
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
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
|
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
|
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
|
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.
|