letta-nightly 0.7.20.dev20250521104258__py3-none-any.whl → 0.7.21.dev20250521233415__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- letta/__init__.py +1 -1
- letta/agent.py +290 -3
- letta/agents/base_agent.py +0 -55
- letta/agents/helpers.py +5 -0
- letta/agents/letta_agent.py +314 -64
- letta/agents/letta_agent_batch.py +102 -55
- letta/agents/voice_agent.py +5 -5
- letta/client/client.py +9 -18
- letta/constants.py +55 -1
- letta/functions/function_sets/builtin.py +27 -0
- letta/groups/sleeptime_multi_agent_v2.py +1 -1
- letta/interfaces/anthropic_streaming_interface.py +10 -1
- letta/interfaces/openai_streaming_interface.py +9 -2
- letta/llm_api/anthropic.py +21 -2
- letta/llm_api/anthropic_client.py +33 -6
- letta/llm_api/google_ai_client.py +136 -423
- letta/llm_api/google_vertex_client.py +173 -22
- letta/llm_api/llm_api_tools.py +27 -0
- letta/llm_api/llm_client.py +1 -1
- letta/llm_api/llm_client_base.py +32 -21
- letta/llm_api/openai.py +57 -0
- letta/llm_api/openai_client.py +7 -11
- letta/memory.py +0 -1
- letta/orm/__init__.py +1 -0
- letta/orm/enums.py +1 -0
- letta/orm/provider_trace.py +26 -0
- letta/orm/step.py +1 -0
- letta/schemas/provider_trace.py +43 -0
- letta/schemas/providers.py +210 -65
- letta/schemas/step.py +1 -0
- letta/schemas/tool.py +4 -0
- letta/server/db.py +37 -19
- letta/server/rest_api/routers/v1/__init__.py +2 -0
- letta/server/rest_api/routers/v1/agents.py +57 -34
- letta/server/rest_api/routers/v1/blocks.py +3 -3
- letta/server/rest_api/routers/v1/identities.py +24 -26
- letta/server/rest_api/routers/v1/jobs.py +3 -3
- letta/server/rest_api/routers/v1/llms.py +13 -8
- letta/server/rest_api/routers/v1/sandbox_configs.py +6 -6
- letta/server/rest_api/routers/v1/tags.py +3 -3
- letta/server/rest_api/routers/v1/telemetry.py +18 -0
- letta/server/rest_api/routers/v1/tools.py +6 -6
- letta/server/rest_api/streaming_response.py +105 -0
- letta/server/rest_api/utils.py +4 -0
- letta/server/server.py +140 -1
- letta/services/agent_manager.py +251 -18
- letta/services/block_manager.py +52 -37
- letta/services/helpers/noop_helper.py +10 -0
- letta/services/identity_manager.py +43 -38
- letta/services/job_manager.py +29 -0
- letta/services/message_manager.py +111 -0
- letta/services/sandbox_config_manager.py +36 -0
- letta/services/step_manager.py +146 -0
- letta/services/telemetry_manager.py +58 -0
- letta/services/tool_executor/tool_execution_manager.py +49 -5
- letta/services/tool_executor/tool_execution_sandbox.py +47 -0
- letta/services/tool_executor/tool_executor.py +236 -7
- letta/services/tool_manager.py +160 -1
- letta/services/tool_sandbox/e2b_sandbox.py +65 -3
- letta/settings.py +10 -2
- letta/tracing.py +5 -5
- {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250521233415.dist-info}/METADATA +3 -2
- {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250521233415.dist-info}/RECORD +66 -59
- {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250521233415.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250521233415.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250521233415.dist-info}/entry_points.txt +0 -0
letta/services/block_manager.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import asyncio
|
1
2
|
from typing import Dict, List, Optional
|
2
3
|
|
3
4
|
from sqlalchemy import select
|
@@ -82,39 +83,64 @@ class BlockManager:
|
|
82
83
|
return block.to_pydantic()
|
83
84
|
|
84
85
|
@enforce_types
|
85
|
-
def
|
86
|
+
async def get_blocks_async(
|
86
87
|
self,
|
87
88
|
actor: PydanticUser,
|
88
89
|
label: Optional[str] = None,
|
89
90
|
is_template: Optional[bool] = None,
|
90
91
|
template_name: Optional[str] = None,
|
91
|
-
identifier_keys: Optional[List[str]] = None,
|
92
92
|
identity_id: Optional[str] = None,
|
93
|
-
|
94
|
-
after: Optional[str] = None,
|
93
|
+
identifier_keys: Optional[List[str]] = None,
|
95
94
|
limit: Optional[int] = 50,
|
96
95
|
) -> List[PydanticBlock]:
|
97
|
-
"""Retrieve blocks based on various optional filters."""
|
98
|
-
|
99
|
-
|
100
|
-
|
96
|
+
"""Async version of get_blocks method. Retrieve blocks based on various optional filters."""
|
97
|
+
from sqlalchemy import select
|
98
|
+
from sqlalchemy.orm import noload
|
99
|
+
|
100
|
+
from letta.orm.sqlalchemy_base import AccessType
|
101
|
+
|
102
|
+
async with db_registry.async_session() as session:
|
103
|
+
# Start with a basic query
|
104
|
+
query = select(BlockModel)
|
105
|
+
|
106
|
+
# Explicitly avoid loading relationships
|
107
|
+
query = query.options(noload(BlockModel.agents), noload(BlockModel.identities), noload(BlockModel.groups))
|
108
|
+
|
109
|
+
# Apply access control
|
110
|
+
query = BlockModel.apply_access_predicate(query, actor, ["read"], AccessType.ORGANIZATION)
|
111
|
+
|
112
|
+
# Add filters
|
113
|
+
query = query.where(BlockModel.organization_id == actor.organization_id)
|
101
114
|
if label:
|
102
|
-
|
115
|
+
query = query.where(BlockModel.label == label)
|
116
|
+
|
103
117
|
if is_template is not None:
|
104
|
-
|
118
|
+
query = query.where(BlockModel.is_template == is_template)
|
119
|
+
|
105
120
|
if template_name:
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
121
|
+
query = query.where(BlockModel.template_name == template_name)
|
122
|
+
|
123
|
+
if identifier_keys:
|
124
|
+
query = (
|
125
|
+
query.join(BlockModel.identities)
|
126
|
+
.filter(BlockModel.identities.property.mapper.class_.identifier_key.in_(identifier_keys))
|
127
|
+
.distinct(BlockModel.id)
|
128
|
+
)
|
129
|
+
|
130
|
+
if identity_id:
|
131
|
+
query = (
|
132
|
+
query.join(BlockModel.identities)
|
133
|
+
.filter(BlockModel.identities.property.mapper.class_.id == identity_id)
|
134
|
+
.distinct(BlockModel.id)
|
135
|
+
)
|
136
|
+
|
137
|
+
# Add limit
|
138
|
+
if limit:
|
139
|
+
query = query.limit(limit)
|
140
|
+
|
141
|
+
# Execute the query
|
142
|
+
result = await session.execute(query)
|
143
|
+
blocks = result.scalars().all()
|
118
144
|
|
119
145
|
return [block.to_pydantic() for block in blocks]
|
120
146
|
|
@@ -190,15 +216,6 @@ class BlockManager:
|
|
190
216
|
except NoResultFound:
|
191
217
|
return None
|
192
218
|
|
193
|
-
@enforce_types
|
194
|
-
def get_all_blocks_by_ids(self, block_ids: List[str], actor: Optional[PydanticUser] = None) -> List[PydanticBlock]:
|
195
|
-
"""Retrieve blocks by their ids."""
|
196
|
-
with db_registry.session() as session:
|
197
|
-
blocks = [block.to_pydantic() for block in BlockModel.read_multiple(db_session=session, identifiers=block_ids, actor=actor)]
|
198
|
-
# backwards compatibility. previous implementation added None for every block not found.
|
199
|
-
blocks.extend([None for _ in range(len(block_ids) - len(blocks))])
|
200
|
-
return blocks
|
201
|
-
|
202
219
|
@enforce_types
|
203
220
|
async def get_all_blocks_by_ids_async(self, block_ids: List[str], actor: Optional[PydanticUser] = None) -> List[PydanticBlock]:
|
204
221
|
"""Retrieve blocks by their ids without loading unnecessary relationships. Async implementation."""
|
@@ -247,16 +264,14 @@ class BlockManager:
|
|
247
264
|
return pydantic_blocks
|
248
265
|
|
249
266
|
@enforce_types
|
250
|
-
def
|
267
|
+
async def get_agents_for_block_async(self, block_id: str, actor: PydanticUser) -> List[PydanticAgentState]:
|
251
268
|
"""
|
252
269
|
Retrieve all agents associated with a given block.
|
253
270
|
"""
|
254
|
-
with db_registry.
|
255
|
-
block = BlockModel.
|
271
|
+
async with db_registry.async_session() as session:
|
272
|
+
block = await BlockModel.read_async(db_session=session, identifier=block_id, actor=actor)
|
256
273
|
agents_orm = block.agents
|
257
|
-
|
258
|
-
|
259
|
-
return agents_pydantic
|
274
|
+
return await asyncio.gather(*[agent.to_pydantic_async() for agent in agents_orm])
|
260
275
|
|
261
276
|
@enforce_types
|
262
277
|
def size(
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from typing import List, Optional
|
2
2
|
|
3
3
|
from fastapi import HTTPException
|
4
|
+
from sqlalchemy import select
|
4
5
|
from sqlalchemy.exc import NoResultFound
|
5
6
|
from sqlalchemy.orm import Session
|
6
7
|
|
@@ -17,7 +18,7 @@ from letta.utils import enforce_types
|
|
17
18
|
class IdentityManager:
|
18
19
|
|
19
20
|
@enforce_types
|
20
|
-
def
|
21
|
+
async def list_identities_async(
|
21
22
|
self,
|
22
23
|
name: Optional[str] = None,
|
23
24
|
project_id: Optional[str] = None,
|
@@ -28,7 +29,7 @@ class IdentityManager:
|
|
28
29
|
limit: Optional[int] = 50,
|
29
30
|
actor: PydanticUser = None,
|
30
31
|
) -> list[PydanticIdentity]:
|
31
|
-
with db_registry.
|
32
|
+
async with db_registry.async_session() as session:
|
32
33
|
filters = {"organization_id": actor.organization_id}
|
33
34
|
if project_id:
|
34
35
|
filters["project_id"] = project_id
|
@@ -36,7 +37,7 @@ class IdentityManager:
|
|
36
37
|
filters["identifier_key"] = identifier_key
|
37
38
|
if identity_type:
|
38
39
|
filters["identity_type"] = identity_type
|
39
|
-
identities = IdentityModel.
|
40
|
+
identities = await IdentityModel.list_async(
|
40
41
|
db_session=session,
|
41
42
|
query_text=name,
|
42
43
|
before=before,
|
@@ -47,17 +48,17 @@ class IdentityManager:
|
|
47
48
|
return [identity.to_pydantic() for identity in identities]
|
48
49
|
|
49
50
|
@enforce_types
|
50
|
-
def
|
51
|
-
with db_registry.
|
52
|
-
identity = IdentityModel.
|
51
|
+
async def get_identity_async(self, identity_id: str, actor: PydanticUser) -> PydanticIdentity:
|
52
|
+
async with db_registry.async_session() as session:
|
53
|
+
identity = await IdentityModel.read_async(db_session=session, identifier=identity_id, actor=actor)
|
53
54
|
return identity.to_pydantic()
|
54
55
|
|
55
56
|
@enforce_types
|
56
|
-
def
|
57
|
-
with db_registry.
|
57
|
+
async def create_identity_async(self, identity: IdentityCreate, actor: PydanticUser) -> PydanticIdentity:
|
58
|
+
async with db_registry.async_session() as session:
|
58
59
|
new_identity = IdentityModel(**identity.model_dump(exclude={"agent_ids", "block_ids"}, exclude_unset=True))
|
59
60
|
new_identity.organization_id = actor.organization_id
|
60
|
-
self.
|
61
|
+
await self._process_relationship_async(
|
61
62
|
session=session,
|
62
63
|
identity=new_identity,
|
63
64
|
relationship_name="agents",
|
@@ -65,7 +66,7 @@ class IdentityManager:
|
|
65
66
|
item_ids=identity.agent_ids,
|
66
67
|
allow_partial=False,
|
67
68
|
)
|
68
|
-
self.
|
69
|
+
await self._process_relationship_async(
|
69
70
|
session=session,
|
70
71
|
identity=new_identity,
|
71
72
|
relationship_name="blocks",
|
@@ -73,13 +74,13 @@ class IdentityManager:
|
|
73
74
|
item_ids=identity.block_ids,
|
74
75
|
allow_partial=False,
|
75
76
|
)
|
76
|
-
new_identity.
|
77
|
+
await new_identity.create_async(session, actor=actor)
|
77
78
|
return new_identity.to_pydantic()
|
78
79
|
|
79
80
|
@enforce_types
|
80
|
-
def
|
81
|
-
with db_registry.
|
82
|
-
existing_identity = IdentityModel.
|
81
|
+
async def upsert_identity_async(self, identity: IdentityUpsert, actor: PydanticUser) -> PydanticIdentity:
|
82
|
+
async with db_registry.async_session() as session:
|
83
|
+
existing_identity = await IdentityModel.read_async(
|
83
84
|
db_session=session,
|
84
85
|
identifier_key=identity.identifier_key,
|
85
86
|
project_id=identity.project_id,
|
@@ -88,7 +89,7 @@ class IdentityManager:
|
|
88
89
|
)
|
89
90
|
|
90
91
|
if existing_identity is None:
|
91
|
-
return self.
|
92
|
+
return await self.create_identity_async(identity=IdentityCreate(**identity.model_dump()), actor=actor)
|
92
93
|
else:
|
93
94
|
identity_update = IdentityUpdate(
|
94
95
|
name=identity.name,
|
@@ -97,25 +98,27 @@ class IdentityManager:
|
|
97
98
|
agent_ids=identity.agent_ids,
|
98
99
|
properties=identity.properties,
|
99
100
|
)
|
100
|
-
return self.
|
101
|
+
return await self._update_identity_async(
|
101
102
|
session=session, existing_identity=existing_identity, identity=identity_update, actor=actor, replace=True
|
102
103
|
)
|
103
104
|
|
104
105
|
@enforce_types
|
105
|
-
def
|
106
|
-
|
106
|
+
async def update_identity_async(
|
107
|
+
self, identity_id: str, identity: IdentityUpdate, actor: PydanticUser, replace: bool = False
|
108
|
+
) -> PydanticIdentity:
|
109
|
+
async with db_registry.async_session() as session:
|
107
110
|
try:
|
108
|
-
existing_identity = IdentityModel.
|
111
|
+
existing_identity = await IdentityModel.read_async(db_session=session, identifier=identity_id, actor=actor)
|
109
112
|
except NoResultFound:
|
110
113
|
raise HTTPException(status_code=404, detail="Identity not found")
|
111
114
|
if existing_identity.organization_id != actor.organization_id:
|
112
115
|
raise HTTPException(status_code=403, detail="Forbidden")
|
113
116
|
|
114
|
-
return self.
|
117
|
+
return await self._update_identity_async(
|
115
118
|
session=session, existing_identity=existing_identity, identity=identity, actor=actor, replace=replace
|
116
119
|
)
|
117
120
|
|
118
|
-
def
|
121
|
+
async def _update_identity_async(
|
119
122
|
self,
|
120
123
|
session: Session,
|
121
124
|
existing_identity: IdentityModel,
|
@@ -139,7 +142,7 @@ class IdentityManager:
|
|
139
142
|
existing_identity.properties = list(new_properties.values())
|
140
143
|
|
141
144
|
if identity.agent_ids is not None:
|
142
|
-
self.
|
145
|
+
await self._process_relationship_async(
|
143
146
|
session=session,
|
144
147
|
identity=existing_identity,
|
145
148
|
relationship_name="agents",
|
@@ -149,7 +152,7 @@ class IdentityManager:
|
|
149
152
|
replace=replace,
|
150
153
|
)
|
151
154
|
if identity.block_ids is not None:
|
152
|
-
self.
|
155
|
+
await self._process_relationship_async(
|
153
156
|
session=session,
|
154
157
|
identity=existing_identity,
|
155
158
|
relationship_name="blocks",
|
@@ -158,16 +161,18 @@ class IdentityManager:
|
|
158
161
|
allow_partial=False,
|
159
162
|
replace=replace,
|
160
163
|
)
|
161
|
-
existing_identity.
|
164
|
+
await existing_identity.update_async(session, actor=actor)
|
162
165
|
return existing_identity.to_pydantic()
|
163
166
|
|
164
167
|
@enforce_types
|
165
|
-
def
|
166
|
-
|
167
|
-
|
168
|
+
async def upsert_identity_properties_async(
|
169
|
+
self, identity_id: str, properties: List[IdentityProperty], actor: PydanticUser
|
170
|
+
) -> PydanticIdentity:
|
171
|
+
async with db_registry.async_session() as session:
|
172
|
+
existing_identity = await IdentityModel.read_async(db_session=session, identifier=identity_id, actor=actor)
|
168
173
|
if existing_identity is None:
|
169
174
|
raise HTTPException(status_code=404, detail="Identity not found")
|
170
|
-
return self.
|
175
|
+
return await self._update_identity_async(
|
171
176
|
session=session,
|
172
177
|
existing_identity=existing_identity,
|
173
178
|
identity=IdentityUpdate(properties=properties),
|
@@ -176,28 +181,28 @@ class IdentityManager:
|
|
176
181
|
)
|
177
182
|
|
178
183
|
@enforce_types
|
179
|
-
def
|
180
|
-
with db_registry.
|
181
|
-
identity = IdentityModel.
|
184
|
+
async def delete_identity_async(self, identity_id: str, actor: PydanticUser) -> None:
|
185
|
+
async with db_registry.async_session() as session:
|
186
|
+
identity = await IdentityModel.read_async(db_session=session, identifier=identity_id, actor=actor)
|
182
187
|
if identity is None:
|
183
188
|
raise HTTPException(status_code=404, detail="Identity not found")
|
184
189
|
if identity.organization_id != actor.organization_id:
|
185
190
|
raise HTTPException(status_code=403, detail="Forbidden")
|
186
|
-
session.delete(identity)
|
187
|
-
session.commit()
|
191
|
+
await session.delete(identity)
|
192
|
+
await session.commit()
|
188
193
|
|
189
194
|
@enforce_types
|
190
|
-
def
|
195
|
+
async def size_async(
|
191
196
|
self,
|
192
197
|
actor: PydanticUser,
|
193
198
|
) -> int:
|
194
199
|
"""
|
195
200
|
Get the total count of identities for the given user.
|
196
201
|
"""
|
197
|
-
with db_registry.
|
198
|
-
return IdentityModel.
|
202
|
+
async with db_registry.async_session() as session:
|
203
|
+
return await IdentityModel.size_async(db_session=session, actor=actor)
|
199
204
|
|
200
|
-
def
|
205
|
+
async def _process_relationship_async(
|
201
206
|
self,
|
202
207
|
session: Session,
|
203
208
|
identity: PydanticIdentity,
|
@@ -214,7 +219,7 @@ class IdentityManager:
|
|
214
219
|
return
|
215
220
|
|
216
221
|
# Retrieve models for the provided IDs
|
217
|
-
found_items = session.
|
222
|
+
found_items = (await session.execute(select(model_class).where(model_class.id.in_(item_ids)))).scalars().all()
|
218
223
|
|
219
224
|
# Validate all items are found if allow_partial is False
|
220
225
|
if not allow_partial and len(found_items) != len(item_ids):
|
letta/services/job_manager.py
CHANGED
@@ -150,6 +150,35 @@ class JobManager:
|
|
150
150
|
)
|
151
151
|
return [job.to_pydantic() for job in jobs]
|
152
152
|
|
153
|
+
@enforce_types
|
154
|
+
async def list_jobs_async(
|
155
|
+
self,
|
156
|
+
actor: PydanticUser,
|
157
|
+
before: Optional[str] = None,
|
158
|
+
after: Optional[str] = None,
|
159
|
+
limit: Optional[int] = 50,
|
160
|
+
statuses: Optional[List[JobStatus]] = None,
|
161
|
+
job_type: JobType = JobType.JOB,
|
162
|
+
ascending: bool = True,
|
163
|
+
) -> List[PydanticJob]:
|
164
|
+
"""List all jobs with optional pagination and status filter."""
|
165
|
+
async with db_registry.async_session() as session:
|
166
|
+
filter_kwargs = {"user_id": actor.id, "job_type": job_type}
|
167
|
+
|
168
|
+
# Add status filter if provided
|
169
|
+
if statuses:
|
170
|
+
filter_kwargs["status"] = statuses
|
171
|
+
|
172
|
+
jobs = await JobModel.list_async(
|
173
|
+
db_session=session,
|
174
|
+
before=before,
|
175
|
+
after=after,
|
176
|
+
limit=limit,
|
177
|
+
ascending=ascending,
|
178
|
+
**filter_kwargs,
|
179
|
+
)
|
180
|
+
return [job.to_pydantic() for job in jobs]
|
181
|
+
|
153
182
|
@enforce_types
|
154
183
|
def delete_job_by_id(self, job_id: str, actor: PydanticUser) -> PydanticJob:
|
155
184
|
"""Delete a job by its ID."""
|
@@ -31,6 +31,16 @@ class MessageManager:
|
|
31
31
|
except NoResultFound:
|
32
32
|
return None
|
33
33
|
|
34
|
+
@enforce_types
|
35
|
+
async def get_message_by_id_async(self, message_id: str, actor: PydanticUser) -> Optional[PydanticMessage]:
|
36
|
+
"""Fetch a message by ID."""
|
37
|
+
async with db_registry.async_session() as session:
|
38
|
+
try:
|
39
|
+
message = await MessageModel.read_async(db_session=session, identifier=message_id, actor=actor)
|
40
|
+
return message.to_pydantic()
|
41
|
+
except NoResultFound:
|
42
|
+
return None
|
43
|
+
|
34
44
|
@enforce_types
|
35
45
|
def get_messages_by_ids(self, message_ids: List[str], actor: PydanticUser) -> List[PydanticMessage]:
|
36
46
|
"""Fetch messages by ID and return them in the requested order."""
|
@@ -426,6 +436,107 @@ class MessageManager:
|
|
426
436
|
results = query.all()
|
427
437
|
return [msg.to_pydantic() for msg in results]
|
428
438
|
|
439
|
+
@enforce_types
|
440
|
+
async def list_messages_for_agent_async(
|
441
|
+
self,
|
442
|
+
agent_id: str,
|
443
|
+
actor: PydanticUser,
|
444
|
+
after: Optional[str] = None,
|
445
|
+
before: Optional[str] = None,
|
446
|
+
query_text: Optional[str] = None,
|
447
|
+
roles: Optional[Sequence[MessageRole]] = None,
|
448
|
+
limit: Optional[int] = 50,
|
449
|
+
ascending: bool = True,
|
450
|
+
group_id: Optional[str] = None,
|
451
|
+
) -> List[PydanticMessage]:
|
452
|
+
"""
|
453
|
+
Most performant query to list messages for an agent by directly querying the Message table.
|
454
|
+
|
455
|
+
This function filters by the agent_id (leveraging the index on messages.agent_id)
|
456
|
+
and applies pagination using sequence_id as the cursor.
|
457
|
+
If query_text is provided, it will filter messages whose text content partially matches the query.
|
458
|
+
If role is provided, it will filter messages by the specified role.
|
459
|
+
|
460
|
+
Args:
|
461
|
+
agent_id: The ID of the agent whose messages are queried.
|
462
|
+
actor: The user performing the action (used for permission checks).
|
463
|
+
after: A message ID; if provided, only messages *after* this message (by sequence_id) are returned.
|
464
|
+
before: A message ID; if provided, only messages *before* this message (by sequence_id) are returned.
|
465
|
+
query_text: Optional string to partially match the message text content.
|
466
|
+
roles: Optional MessageRole to filter messages by role.
|
467
|
+
limit: Maximum number of messages to return.
|
468
|
+
ascending: If True, sort by sequence_id ascending; if False, sort descending.
|
469
|
+
group_id: Optional group ID to filter messages by group_id.
|
470
|
+
|
471
|
+
Returns:
|
472
|
+
List[PydanticMessage]: A list of messages (converted via .to_pydantic()).
|
473
|
+
|
474
|
+
Raises:
|
475
|
+
NoResultFound: If the provided after/before message IDs do not exist.
|
476
|
+
"""
|
477
|
+
|
478
|
+
async with db_registry.async_session() as session:
|
479
|
+
# Permission check: raise if the agent doesn't exist or actor is not allowed.
|
480
|
+
await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
|
481
|
+
|
482
|
+
# Build a query that directly filters the Message table by agent_id.
|
483
|
+
query = select(MessageModel).where(MessageModel.agent_id == agent_id)
|
484
|
+
|
485
|
+
# If group_id is provided, filter messages by group_id.
|
486
|
+
if group_id:
|
487
|
+
query = query.where(MessageModel.group_id == group_id)
|
488
|
+
|
489
|
+
# If query_text is provided, filter messages using subquery + json_array_elements.
|
490
|
+
if query_text:
|
491
|
+
content_element = func.json_array_elements(MessageModel.content).alias("content_element")
|
492
|
+
query = query.where(
|
493
|
+
exists(
|
494
|
+
select(1)
|
495
|
+
.select_from(content_element)
|
496
|
+
.where(text("content_element->>'type' = 'text' AND content_element->>'text' ILIKE :query_text"))
|
497
|
+
.params(query_text=f"%{query_text}%")
|
498
|
+
)
|
499
|
+
)
|
500
|
+
|
501
|
+
# If role(s) are provided, filter messages by those roles.
|
502
|
+
if roles:
|
503
|
+
role_values = [r.value for r in roles]
|
504
|
+
query = query.where(MessageModel.role.in_(role_values))
|
505
|
+
|
506
|
+
# Apply 'after' pagination if specified.
|
507
|
+
if after:
|
508
|
+
after_query = select(MessageModel.sequence_id).where(MessageModel.id == after)
|
509
|
+
after_result = await session.execute(after_query)
|
510
|
+
after_ref = after_result.one_or_none()
|
511
|
+
if not after_ref:
|
512
|
+
raise NoResultFound(f"No message found with id '{after}' for agent '{agent_id}'.")
|
513
|
+
# Filter out any messages with a sequence_id <= after_ref.sequence_id
|
514
|
+
query = query.where(MessageModel.sequence_id > after_ref.sequence_id)
|
515
|
+
|
516
|
+
# Apply 'before' pagination if specified.
|
517
|
+
if before:
|
518
|
+
before_query = select(MessageModel.sequence_id).where(MessageModel.id == before)
|
519
|
+
before_result = await session.execute(before_query)
|
520
|
+
before_ref = before_result.one_or_none()
|
521
|
+
if not before_ref:
|
522
|
+
raise NoResultFound(f"No message found with id '{before}' for agent '{agent_id}'.")
|
523
|
+
# Filter out any messages with a sequence_id >= before_ref.sequence_id
|
524
|
+
query = query.where(MessageModel.sequence_id < before_ref.sequence_id)
|
525
|
+
|
526
|
+
# Apply ordering based on the ascending flag.
|
527
|
+
if ascending:
|
528
|
+
query = query.order_by(MessageModel.sequence_id.asc())
|
529
|
+
else:
|
530
|
+
query = query.order_by(MessageModel.sequence_id.desc())
|
531
|
+
|
532
|
+
# Limit the number of results.
|
533
|
+
query = query.limit(limit)
|
534
|
+
|
535
|
+
# Execute and convert each Message to its Pydantic representation.
|
536
|
+
result = await session.execute(query)
|
537
|
+
results = result.scalars().all()
|
538
|
+
return [msg.to_pydantic() for msg in results]
|
539
|
+
|
429
540
|
@enforce_types
|
430
541
|
def delete_all_messages_for_agent(self, agent_id: str, actor: PydanticUser) -> int:
|
431
542
|
"""
|
@@ -122,6 +122,23 @@ class SandboxConfigManager:
|
|
122
122
|
sandboxes = SandboxConfigModel.list(db_session=session, after=after, limit=limit, **kwargs)
|
123
123
|
return [sandbox.to_pydantic() for sandbox in sandboxes]
|
124
124
|
|
125
|
+
@enforce_types
|
126
|
+
async def list_sandbox_configs_async(
|
127
|
+
self,
|
128
|
+
actor: PydanticUser,
|
129
|
+
after: Optional[str] = None,
|
130
|
+
limit: Optional[int] = 50,
|
131
|
+
sandbox_type: Optional[SandboxType] = None,
|
132
|
+
) -> List[PydanticSandboxConfig]:
|
133
|
+
"""List all sandbox configurations with optional pagination."""
|
134
|
+
kwargs = {"organization_id": actor.organization_id}
|
135
|
+
if sandbox_type:
|
136
|
+
kwargs.update({"type": sandbox_type})
|
137
|
+
|
138
|
+
async with db_registry.async_session() as session:
|
139
|
+
sandboxes = await SandboxConfigModel.list_async(db_session=session, after=after, limit=limit, **kwargs)
|
140
|
+
return [sandbox.to_pydantic() for sandbox in sandboxes]
|
141
|
+
|
125
142
|
@enforce_types
|
126
143
|
def get_sandbox_config_by_id(self, sandbox_config_id: str, actor: Optional[PydanticUser] = None) -> Optional[PydanticSandboxConfig]:
|
127
144
|
"""Retrieve a sandbox configuration by its ID."""
|
@@ -224,6 +241,25 @@ class SandboxConfigManager:
|
|
224
241
|
)
|
225
242
|
return [env_var.to_pydantic() for env_var in env_vars]
|
226
243
|
|
244
|
+
@enforce_types
|
245
|
+
async def list_sandbox_env_vars_async(
|
246
|
+
self,
|
247
|
+
sandbox_config_id: str,
|
248
|
+
actor: PydanticUser,
|
249
|
+
after: Optional[str] = None,
|
250
|
+
limit: Optional[int] = 50,
|
251
|
+
) -> List[PydanticEnvVar]:
|
252
|
+
"""List all sandbox environment variables with optional pagination."""
|
253
|
+
async with db_registry.async_session() as session:
|
254
|
+
env_vars = await SandboxEnvVarModel.list_async(
|
255
|
+
db_session=session,
|
256
|
+
after=after,
|
257
|
+
limit=limit,
|
258
|
+
organization_id=actor.organization_id,
|
259
|
+
sandbox_config_id=sandbox_config_id,
|
260
|
+
)
|
261
|
+
return [env_var.to_pydantic() for env_var in env_vars]
|
262
|
+
|
227
263
|
@enforce_types
|
228
264
|
def list_sandbox_env_vars_by_key(
|
229
265
|
self, key: str, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50
|