letta-nightly 0.7.21.dev20250521233415__py3-none-any.whl → 0.7.22.dev20250523081403__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 (50) hide show
  1. letta/__init__.py +2 -2
  2. letta/agents/base_agent.py +4 -2
  3. letta/agents/letta_agent.py +3 -10
  4. letta/agents/letta_agent_batch.py +6 -6
  5. letta/cli/cli.py +0 -316
  6. letta/cli/cli_load.py +0 -52
  7. letta/client/client.py +2 -1554
  8. letta/data_sources/connectors.py +4 -2
  9. letta/functions/ast_parsers.py +33 -43
  10. letta/groups/sleeptime_multi_agent_v2.py +49 -13
  11. letta/jobs/llm_batch_job_polling.py +3 -3
  12. letta/jobs/scheduler.py +20 -19
  13. letta/llm_api/anthropic_client.py +3 -0
  14. letta/llm_api/google_vertex_client.py +5 -0
  15. letta/llm_api/openai_client.py +5 -0
  16. letta/main.py +2 -362
  17. letta/server/db.py +5 -0
  18. letta/server/rest_api/routers/v1/agents.py +72 -43
  19. letta/server/rest_api/routers/v1/llms.py +2 -2
  20. letta/server/rest_api/routers/v1/messages.py +5 -3
  21. letta/server/rest_api/routers/v1/sandbox_configs.py +18 -18
  22. letta/server/rest_api/routers/v1/sources.py +49 -36
  23. letta/server/server.py +53 -22
  24. letta/services/agent_manager.py +797 -124
  25. letta/services/block_manager.py +14 -62
  26. letta/services/group_manager.py +37 -0
  27. letta/services/identity_manager.py +9 -0
  28. letta/services/job_manager.py +17 -0
  29. letta/services/llm_batch_manager.py +88 -64
  30. letta/services/message_manager.py +19 -0
  31. letta/services/organization_manager.py +10 -0
  32. letta/services/passage_manager.py +13 -0
  33. letta/services/per_agent_lock_manager.py +4 -0
  34. letta/services/provider_manager.py +34 -0
  35. letta/services/sandbox_config_manager.py +130 -0
  36. letta/services/source_manager.py +59 -44
  37. letta/services/step_manager.py +8 -1
  38. letta/services/tool_manager.py +21 -0
  39. letta/services/tool_sandbox/e2b_sandbox.py +4 -2
  40. letta/services/tool_sandbox/local_sandbox.py +7 -3
  41. letta/services/user_manager.py +16 -0
  42. {letta_nightly-0.7.21.dev20250521233415.dist-info → letta_nightly-0.7.22.dev20250523081403.dist-info}/METADATA +1 -1
  43. {letta_nightly-0.7.21.dev20250521233415.dist-info → letta_nightly-0.7.22.dev20250523081403.dist-info}/RECORD +46 -50
  44. letta/__main__.py +0 -3
  45. letta/benchmark/benchmark.py +0 -98
  46. letta/benchmark/constants.py +0 -14
  47. letta/cli/cli_config.py +0 -227
  48. {letta_nightly-0.7.21.dev20250521233415.dist-info → letta_nightly-0.7.22.dev20250523081403.dist-info}/LICENSE +0 -0
  49. {letta_nightly-0.7.21.dev20250521233415.dist-info → letta_nightly-0.7.22.dev20250523081403.dist-info}/WHEEL +0 -0
  50. {letta_nightly-0.7.21.dev20250521233415.dist-info → letta_nightly-0.7.22.dev20250523081403.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,8 @@
1
1
  import threading
2
2
  from collections import defaultdict
3
3
 
4
+ from letta.tracing import trace_method
5
+
4
6
 
5
7
  class PerAgentLockManager:
6
8
  """Manages per-agent locks."""
@@ -8,10 +10,12 @@ class PerAgentLockManager:
8
10
  def __init__(self):
9
11
  self.locks = defaultdict(threading.Lock)
10
12
 
13
+ @trace_method
11
14
  def get_lock(self, agent_id: str) -> threading.Lock:
12
15
  """Retrieve the lock for a specific agent_id."""
13
16
  return self.locks[agent_id]
14
17
 
18
+ @trace_method
15
19
  def clear_lock(self, agent_id: str):
16
20
  """Optionally remove a lock if no longer needed (to prevent unbounded growth)."""
17
21
  if agent_id in self.locks:
@@ -6,12 +6,14 @@ from letta.schemas.providers import Provider as PydanticProvider
6
6
  from letta.schemas.providers import ProviderCheck, ProviderCreate, ProviderUpdate
7
7
  from letta.schemas.user import User as PydanticUser
8
8
  from letta.server.db import db_registry
9
+ from letta.tracing import trace_method
9
10
  from letta.utils import enforce_types
10
11
 
11
12
 
12
13
  class ProviderManager:
13
14
 
14
15
  @enforce_types
16
+ @trace_method
15
17
  def create_provider(self, request: ProviderCreate, actor: PydanticUser) -> PydanticProvider:
16
18
  """Create a new provider if it doesn't already exist."""
17
19
  with db_registry.session() as session:
@@ -32,6 +34,7 @@ class ProviderManager:
32
34
  return new_provider.to_pydantic()
33
35
 
34
36
  @enforce_types
37
+ @trace_method
35
38
  def update_provider(self, provider_id: str, provider_update: ProviderUpdate, actor: PydanticUser) -> PydanticProvider:
36
39
  """Update provider details."""
37
40
  with db_registry.session() as session:
@@ -48,6 +51,7 @@ class ProviderManager:
48
51
  return existing_provider.to_pydantic()
49
52
 
50
53
  @enforce_types
54
+ @trace_method
51
55
  def delete_provider_by_id(self, provider_id: str, actor: PydanticUser):
52
56
  """Delete a provider."""
53
57
  with db_registry.session() as session:
@@ -62,6 +66,7 @@ class ProviderManager:
62
66
  session.commit()
63
67
 
64
68
  @enforce_types
69
+ @trace_method
65
70
  def list_providers(
66
71
  self,
67
72
  actor: PydanticUser,
@@ -87,16 +92,45 @@ class ProviderManager:
87
92
  return [provider.to_pydantic() for provider in providers]
88
93
 
89
94
  @enforce_types
95
+ @trace_method
96
+ async def list_providers_async(
97
+ self,
98
+ actor: PydanticUser,
99
+ name: Optional[str] = None,
100
+ provider_type: Optional[ProviderType] = None,
101
+ after: Optional[str] = None,
102
+ limit: Optional[int] = 50,
103
+ ) -> List[PydanticProvider]:
104
+ """List all providers with optional pagination."""
105
+ filter_kwargs = {}
106
+ if name:
107
+ filter_kwargs["name"] = name
108
+ if provider_type:
109
+ filter_kwargs["provider_type"] = provider_type
110
+ async with db_registry.async_session() as session:
111
+ providers = await ProviderModel.list_async(
112
+ db_session=session,
113
+ after=after,
114
+ limit=limit,
115
+ actor=actor,
116
+ **filter_kwargs,
117
+ )
118
+ return [provider.to_pydantic() for provider in providers]
119
+
120
+ @enforce_types
121
+ @trace_method
90
122
  def get_provider_id_from_name(self, provider_name: Union[str, None], actor: PydanticUser) -> Optional[str]:
91
123
  providers = self.list_providers(name=provider_name, actor=actor)
92
124
  return providers[0].id if providers else None
93
125
 
94
126
  @enforce_types
127
+ @trace_method
95
128
  def get_override_key(self, provider_name: Union[str, None], actor: PydanticUser) -> Optional[str]:
96
129
  providers = self.list_providers(name=provider_name, actor=actor)
97
130
  return providers[0].api_key if providers else None
98
131
 
99
132
  @enforce_types
133
+ @trace_method
100
134
  def check_provider_api_key(self, provider_check: ProviderCheck) -> None:
101
135
  provider = PydanticProvider(
102
136
  name=provider_check.provider_type.value,
@@ -12,6 +12,7 @@ from letta.schemas.sandbox_config import SandboxConfig as PydanticSandboxConfig
12
12
  from letta.schemas.sandbox_config import SandboxConfigCreate, SandboxConfigUpdate, SandboxType
13
13
  from letta.schemas.user import User as PydanticUser
14
14
  from letta.server.db import db_registry
15
+ from letta.tracing import trace_method
15
16
  from letta.utils import enforce_types, printd
16
17
 
17
18
  logger = get_logger(__name__)
@@ -21,6 +22,7 @@ class SandboxConfigManager:
21
22
  """Manager class to handle business logic related to SandboxConfig and SandboxEnvironmentVariable."""
22
23
 
23
24
  @enforce_types
25
+ @trace_method
24
26
  def get_or_create_default_sandbox_config(self, sandbox_type: SandboxType, actor: PydanticUser) -> PydanticSandboxConfig:
25
27
  sandbox_config = self.get_sandbox_config_by_type(sandbox_type, actor=actor)
26
28
  if not sandbox_config:
@@ -38,6 +40,7 @@ class SandboxConfigManager:
38
40
  return sandbox_config
39
41
 
40
42
  @enforce_types
43
+ @trace_method
41
44
  def create_or_update_sandbox_config(self, sandbox_config_create: SandboxConfigCreate, actor: PydanticUser) -> PydanticSandboxConfig:
42
45
  """Create or update a sandbox configuration based on the PydanticSandboxConfig schema."""
43
46
  config = sandbox_config_create.config
@@ -71,6 +74,61 @@ class SandboxConfigManager:
71
74
  return db_sandbox.to_pydantic()
72
75
 
73
76
  @enforce_types
77
+ @trace_method
78
+ async def get_or_create_default_sandbox_config_async(self, sandbox_type: SandboxType, actor: PydanticUser) -> PydanticSandboxConfig:
79
+ sandbox_config = await self.get_sandbox_config_by_type_async(sandbox_type, actor=actor)
80
+ if not sandbox_config:
81
+ logger.debug(f"Creating new sandbox config of type {sandbox_type}, none found for organization {actor.organization_id}.")
82
+
83
+ # TODO: Add more sandbox types later
84
+ if sandbox_type == SandboxType.E2B:
85
+ default_config = {} # Empty
86
+ else:
87
+ # TODO: May want to move this to environment variables v.s. persisting in database
88
+ default_local_sandbox_path = LETTA_TOOL_EXECUTION_DIR
89
+ default_config = LocalSandboxConfig(sandbox_dir=default_local_sandbox_path).model_dump(exclude_none=True)
90
+
91
+ sandbox_config = await self.create_or_update_sandbox_config_async(SandboxConfigCreate(config=default_config), actor=actor)
92
+ return sandbox_config
93
+
94
+ @enforce_types
95
+ @trace_method
96
+ async def create_or_update_sandbox_config_async(
97
+ self, sandbox_config_create: SandboxConfigCreate, actor: PydanticUser
98
+ ) -> PydanticSandboxConfig:
99
+ """Create or update a sandbox configuration based on the PydanticSandboxConfig schema."""
100
+ config = sandbox_config_create.config
101
+ sandbox_type = config.type
102
+ sandbox_config = PydanticSandboxConfig(
103
+ type=sandbox_type, config=config.model_dump(exclude_none=True), organization_id=actor.organization_id
104
+ )
105
+
106
+ # Attempt to retrieve the existing sandbox configuration by type within the organization
107
+ db_sandbox = await self.get_sandbox_config_by_type_async(sandbox_config.type, actor=actor)
108
+ if db_sandbox:
109
+ # Prepare the update data, excluding fields that should not be reset
110
+ update_data = sandbox_config.model_dump(exclude_unset=True, exclude_none=True)
111
+ update_data = {key: value for key, value in update_data.items() if getattr(db_sandbox, key) != value}
112
+
113
+ # If there are changes, update the sandbox configuration
114
+ if update_data:
115
+ db_sandbox = await self.update_sandbox_config_async(db_sandbox.id, SandboxConfigUpdate(**update_data), actor)
116
+ else:
117
+ printd(
118
+ f"`create_or_update_sandbox_config` was called with user_id={actor.id}, organization_id={actor.organization_id}, "
119
+ f"type={sandbox_config.type}, but found existing configuration with nothing to update."
120
+ )
121
+
122
+ return db_sandbox
123
+ else:
124
+ # If the sandbox configuration doesn't exist, create a new one
125
+ async with db_registry.async_session() as session:
126
+ db_sandbox = SandboxConfigModel(**sandbox_config.model_dump(exclude_none=True))
127
+ await db_sandbox.create_async(session, actor=actor)
128
+ return db_sandbox.to_pydantic()
129
+
130
+ @enforce_types
131
+ @trace_method
74
132
  def update_sandbox_config(
75
133
  self, sandbox_config_id: str, sandbox_update: SandboxConfigUpdate, actor: PydanticUser
76
134
  ) -> PydanticSandboxConfig:
@@ -98,6 +156,35 @@ class SandboxConfigManager:
98
156
  return sandbox.to_pydantic()
99
157
 
100
158
  @enforce_types
159
+ @trace_method
160
+ async def update_sandbox_config_async(
161
+ self, sandbox_config_id: str, sandbox_update: SandboxConfigUpdate, actor: PydanticUser
162
+ ) -> PydanticSandboxConfig:
163
+ """Update an existing sandbox configuration."""
164
+ async with db_registry.async_session() as session:
165
+ sandbox = await SandboxConfigModel.read_async(db_session=session, identifier=sandbox_config_id, actor=actor)
166
+ # We need to check that the sandbox_update provided is the same type as the original sandbox
167
+ if sandbox.type != sandbox_update.config.type:
168
+ raise ValueError(
169
+ f"Mismatched type for sandbox config update: tried to update sandbox_config of type {sandbox.type} with config of type {sandbox_update.config.type}"
170
+ )
171
+
172
+ update_data = sandbox_update.model_dump(exclude_unset=True, exclude_none=True)
173
+ update_data = {key: value for key, value in update_data.items() if getattr(sandbox, key) != value}
174
+
175
+ if update_data:
176
+ for key, value in update_data.items():
177
+ setattr(sandbox, key, value)
178
+ await sandbox.update_async(db_session=session, actor=actor)
179
+ else:
180
+ printd(
181
+ f"`update_sandbox_config` called with user_id={actor.id}, organization_id={actor.organization_id}, "
182
+ f"name={sandbox.type}, but nothing to update."
183
+ )
184
+ return sandbox.to_pydantic()
185
+
186
+ @enforce_types
187
+ @trace_method
101
188
  def delete_sandbox_config(self, sandbox_config_id: str, actor: PydanticUser) -> PydanticSandboxConfig:
102
189
  """Delete a sandbox configuration by its ID."""
103
190
  with db_registry.session() as session:
@@ -106,6 +193,7 @@ class SandboxConfigManager:
106
193
  return sandbox.to_pydantic()
107
194
 
108
195
  @enforce_types
196
+ @trace_method
109
197
  def list_sandbox_configs(
110
198
  self,
111
199
  actor: PydanticUser,
@@ -123,6 +211,7 @@ class SandboxConfigManager:
123
211
  return [sandbox.to_pydantic() for sandbox in sandboxes]
124
212
 
125
213
  @enforce_types
214
+ @trace_method
126
215
  async def list_sandbox_configs_async(
127
216
  self,
128
217
  actor: PydanticUser,
@@ -140,6 +229,7 @@ class SandboxConfigManager:
140
229
  return [sandbox.to_pydantic() for sandbox in sandboxes]
141
230
 
142
231
  @enforce_types
232
+ @trace_method
143
233
  def get_sandbox_config_by_id(self, sandbox_config_id: str, actor: Optional[PydanticUser] = None) -> Optional[PydanticSandboxConfig]:
144
234
  """Retrieve a sandbox configuration by its ID."""
145
235
  with db_registry.session() as session:
@@ -150,6 +240,7 @@ class SandboxConfigManager:
150
240
  return None
151
241
 
152
242
  @enforce_types
243
+ @trace_method
153
244
  def get_sandbox_config_by_type(self, type: SandboxType, actor: Optional[PydanticUser] = None) -> Optional[PydanticSandboxConfig]:
154
245
  """Retrieve a sandbox config by its type."""
155
246
  with db_registry.session() as session:
@@ -167,6 +258,27 @@ class SandboxConfigManager:
167
258
  return None
168
259
 
169
260
  @enforce_types
261
+ @trace_method
262
+ async def get_sandbox_config_by_type_async(
263
+ self, type: SandboxType, actor: Optional[PydanticUser] = None
264
+ ) -> Optional[PydanticSandboxConfig]:
265
+ """Retrieve a sandbox config by its type."""
266
+ async with db_registry.async_session() as session:
267
+ try:
268
+ sandboxes = await SandboxConfigModel.list_async(
269
+ db_session=session,
270
+ type=type,
271
+ organization_id=actor.organization_id,
272
+ limit=1,
273
+ )
274
+ if sandboxes:
275
+ return sandboxes[0].to_pydantic()
276
+ return None
277
+ except NoResultFound:
278
+ return None
279
+
280
+ @enforce_types
281
+ @trace_method
170
282
  def create_sandbox_env_var(
171
283
  self, env_var_create: SandboxEnvironmentVariableCreate, sandbox_config_id: str, actor: PydanticUser
172
284
  ) -> PydanticEnvVar:
@@ -194,6 +306,7 @@ class SandboxConfigManager:
194
306
  return env_var.to_pydantic()
195
307
 
196
308
  @enforce_types
309
+ @trace_method
197
310
  def update_sandbox_env_var(
198
311
  self, env_var_id: str, env_var_update: SandboxEnvironmentVariableUpdate, actor: PydanticUser
199
312
  ) -> PydanticEnvVar:
@@ -215,6 +328,7 @@ class SandboxConfigManager:
215
328
  return env_var.to_pydantic()
216
329
 
217
330
  @enforce_types
331
+ @trace_method
218
332
  def delete_sandbox_env_var(self, env_var_id: str, actor: PydanticUser) -> PydanticEnvVar:
219
333
  """Delete a sandbox environment variable by its ID."""
220
334
  with db_registry.session() as session:
@@ -223,6 +337,7 @@ class SandboxConfigManager:
223
337
  return env_var.to_pydantic()
224
338
 
225
339
  @enforce_types
340
+ @trace_method
226
341
  def list_sandbox_env_vars(
227
342
  self,
228
343
  sandbox_config_id: str,
@@ -242,6 +357,7 @@ class SandboxConfigManager:
242
357
  return [env_var.to_pydantic() for env_var in env_vars]
243
358
 
244
359
  @enforce_types
360
+ @trace_method
245
361
  async def list_sandbox_env_vars_async(
246
362
  self,
247
363
  sandbox_config_id: str,
@@ -261,6 +377,7 @@ class SandboxConfigManager:
261
377
  return [env_var.to_pydantic() for env_var in env_vars]
262
378
 
263
379
  @enforce_types
380
+ @trace_method
264
381
  def list_sandbox_env_vars_by_key(
265
382
  self, key: str, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50
266
383
  ) -> List[PydanticEnvVar]:
@@ -276,6 +393,7 @@ class SandboxConfigManager:
276
393
  return [env_var.to_pydantic() for env_var in env_vars]
277
394
 
278
395
  @enforce_types
396
+ @trace_method
279
397
  def get_sandbox_env_vars_as_dict(
280
398
  self, sandbox_config_id: str, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50
281
399
  ) -> Dict[str, str]:
@@ -286,6 +404,18 @@ class SandboxConfigManager:
286
404
  return result
287
405
 
288
406
  @enforce_types
407
+ @trace_method
408
+ async def get_sandbox_env_vars_as_dict_async(
409
+ self, sandbox_config_id: str, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50
410
+ ) -> Dict[str, str]:
411
+ env_vars = await self.list_sandbox_env_vars_async(sandbox_config_id, actor, after, limit)
412
+ result = {}
413
+ for env_var in env_vars:
414
+ result[env_var.key] = env_var.value
415
+ return result
416
+
417
+ @enforce_types
418
+ @trace_method
289
419
  def get_sandbox_env_var_by_key_and_sandbox_config_id(
290
420
  self, key: str, sandbox_config_id: str, actor: Optional[PydanticUser] = None
291
421
  ) -> Optional[PydanticEnvVar]:
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  from typing import List, Optional
2
3
 
3
4
  from letta.orm.errors import NoResultFound
@@ -9,6 +10,7 @@ from letta.schemas.source import Source as PydanticSource
9
10
  from letta.schemas.source import SourceUpdate
10
11
  from letta.schemas.user import User as PydanticUser
11
12
  from letta.server.db import db_registry
13
+ from letta.tracing import trace_method
12
14
  from letta.utils import enforce_types, printd
13
15
 
14
16
 
@@ -16,25 +18,27 @@ class SourceManager:
16
18
  """Manager class to handle business logic related to Sources."""
17
19
 
18
20
  @enforce_types
19
- def create_source(self, source: PydanticSource, actor: PydanticUser) -> PydanticSource:
21
+ @trace_method
22
+ async def create_source(self, source: PydanticSource, actor: PydanticUser) -> PydanticSource:
20
23
  """Create a new source based on the PydanticSource schema."""
21
24
  # Try getting the source first by id
22
- db_source = self.get_source_by_id(source.id, actor=actor)
25
+ db_source = await self.get_source_by_id(source.id, actor=actor)
23
26
  if db_source:
24
27
  return db_source
25
28
  else:
26
- with db_registry.session() as session:
29
+ async with db_registry.async_session() as session:
27
30
  # Provide default embedding config if not given
28
31
  source.organization_id = actor.organization_id
29
32
  source = SourceModel(**source.model_dump(to_orm=True, exclude_none=True))
30
- source.create(session, actor=actor)
33
+ await source.create_async(session, actor=actor)
31
34
  return source.to_pydantic()
32
35
 
33
36
  @enforce_types
34
- def update_source(self, source_id: str, source_update: SourceUpdate, actor: PydanticUser) -> PydanticSource:
37
+ @trace_method
38
+ async def update_source(self, source_id: str, source_update: SourceUpdate, actor: PydanticUser) -> PydanticSource:
35
39
  """Update a source by its ID with the given SourceUpdate object."""
36
- with db_registry.session() as session:
37
- source = SourceModel.read(db_session=session, identifier=source_id, actor=actor)
40
+ async with db_registry.async_session() as session:
41
+ source = await SourceModel.read_async(db_session=session, identifier=source_id, actor=actor)
38
42
 
39
43
  # get update dictionary
40
44
  update_data = source_update.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
@@ -53,18 +57,22 @@ class SourceManager:
53
57
  return source.to_pydantic()
54
58
 
55
59
  @enforce_types
56
- def delete_source(self, source_id: str, actor: PydanticUser) -> PydanticSource:
60
+ @trace_method
61
+ async def delete_source(self, source_id: str, actor: PydanticUser) -> PydanticSource:
57
62
  """Delete a source by its ID."""
58
- with db_registry.session() as session:
59
- source = SourceModel.read(db_session=session, identifier=source_id)
60
- source.hard_delete(db_session=session, actor=actor)
63
+ async with db_registry.async_session() as session:
64
+ source = await SourceModel.read_async(db_session=session, identifier=source_id)
65
+ await source.hard_delete_async(db_session=session, actor=actor)
61
66
  return source.to_pydantic()
62
67
 
63
68
  @enforce_types
64
- def list_sources(self, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50, **kwargs) -> List[PydanticSource]:
69
+ @trace_method
70
+ async def list_sources(
71
+ self, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50, **kwargs
72
+ ) -> List[PydanticSource]:
65
73
  """List all sources with optional pagination."""
66
- with db_registry.session() as session:
67
- sources = SourceModel.list(
74
+ async with db_registry.async_session() as session:
75
+ sources = await SourceModel.list_async(
68
76
  db_session=session,
69
77
  after=after,
70
78
  limit=limit,
@@ -74,18 +82,17 @@ class SourceManager:
74
82
  return [source.to_pydantic() for source in sources]
75
83
 
76
84
  @enforce_types
77
- def size(
78
- self,
79
- actor: PydanticUser,
80
- ) -> int:
85
+ @trace_method
86
+ async def size(self, actor: PydanticUser) -> int:
81
87
  """
82
88
  Get the total count of sources for the given user.
83
89
  """
84
- with db_registry.session() as session:
85
- return SourceModel.size(db_session=session, actor=actor)
90
+ async with db_registry.async_session() as session:
91
+ return await SourceModel.size_async(db_session=session, actor=actor)
86
92
 
87
93
  @enforce_types
88
- def list_attached_agents(self, source_id: str, actor: Optional[PydanticUser] = None) -> List[PydanticAgentState]:
94
+ @trace_method
95
+ async def list_attached_agents(self, source_id: str, actor: Optional[PydanticUser] = None) -> List[PydanticAgentState]:
89
96
  """
90
97
  Lists all agents that have the specified source attached.
91
98
 
@@ -96,30 +103,33 @@ class SourceManager:
96
103
  Returns:
97
104
  List[PydanticAgentState]: List of agents that have this source attached
98
105
  """
99
- with db_registry.session() as session:
106
+ async with db_registry.async_session() as session:
100
107
  # Verify source exists and user has permission to access it
101
- source = SourceModel.read(db_session=session, identifier=source_id, actor=actor)
108
+ source = await SourceModel.read_async(db_session=session, identifier=source_id, actor=actor)
102
109
 
103
110
  # The agents relationship is already loaded due to lazy="selectin" in the Source model
104
111
  # and will be properly filtered by organization_id due to the OrganizationMixin
105
- return [agent.to_pydantic() for agent in source.agents]
112
+ agents_orm = source.agents
113
+ return await asyncio.gather(*[agent.to_pydantic_async() for agent in agents_orm])
106
114
 
107
115
  # TODO: We make actor optional for now, but should most likely be enforced due to security reasons
108
116
  @enforce_types
109
- def get_source_by_id(self, source_id: str, actor: Optional[PydanticUser] = None) -> Optional[PydanticSource]:
117
+ @trace_method
118
+ async def get_source_by_id(self, source_id: str, actor: Optional[PydanticUser] = None) -> Optional[PydanticSource]:
110
119
  """Retrieve a source by its ID."""
111
- with db_registry.session() as session:
120
+ async with db_registry.async_session() as session:
112
121
  try:
113
- source = SourceModel.read(db_session=session, identifier=source_id, actor=actor)
122
+ source = await SourceModel.read_async(db_session=session, identifier=source_id, actor=actor)
114
123
  return source.to_pydantic()
115
124
  except NoResultFound:
116
125
  return None
117
126
 
118
127
  @enforce_types
119
- def get_source_by_name(self, source_name: str, actor: PydanticUser) -> Optional[PydanticSource]:
128
+ @trace_method
129
+ async def get_source_by_name(self, source_name: str, actor: PydanticUser) -> Optional[PydanticSource]:
120
130
  """Retrieve a source by its name."""
121
- with db_registry.session() as session:
122
- sources = SourceModel.list(
131
+ async with db_registry.async_session() as session:
132
+ sources = await SourceModel.list_async(
123
133
  db_session=session,
124
134
  name=source_name,
125
135
  organization_id=actor.organization_id,
@@ -131,44 +141,49 @@ class SourceManager:
131
141
  return sources[0].to_pydantic()
132
142
 
133
143
  @enforce_types
134
- def create_file(self, file_metadata: PydanticFileMetadata, actor: PydanticUser) -> PydanticFileMetadata:
144
+ @trace_method
145
+ async def create_file(self, file_metadata: PydanticFileMetadata, actor: PydanticUser) -> PydanticFileMetadata:
135
146
  """Create a new file based on the PydanticFileMetadata schema."""
136
- db_file = self.get_file_by_id(file_metadata.id, actor=actor)
147
+ db_file = await self.get_file_by_id(file_metadata.id, actor=actor)
137
148
  if db_file:
138
149
  return db_file
139
150
  else:
140
- with db_registry.session() as session:
151
+ async with db_registry.async_session() as session:
141
152
  file_metadata.organization_id = actor.organization_id
142
153
  file_metadata = FileMetadataModel(**file_metadata.model_dump(to_orm=True, exclude_none=True))
143
- file_metadata.create(session, actor=actor)
154
+ await file_metadata.create_async(session, actor=actor)
144
155
  return file_metadata.to_pydantic()
145
156
 
146
157
  # TODO: We make actor optional for now, but should most likely be enforced due to security reasons
147
158
  @enforce_types
148
- def get_file_by_id(self, file_id: str, actor: Optional[PydanticUser] = None) -> Optional[PydanticFileMetadata]:
159
+ @trace_method
160
+ async def get_file_by_id(self, file_id: str, actor: Optional[PydanticUser] = None) -> Optional[PydanticFileMetadata]:
149
161
  """Retrieve a file by its ID."""
150
- with db_registry.session() as session:
162
+ async with db_registry.async_session() as session:
151
163
  try:
152
- file = FileMetadataModel.read(db_session=session, identifier=file_id, actor=actor)
164
+ file = await FileMetadataModel.read_async(db_session=session, identifier=file_id, actor=actor)
153
165
  return file.to_pydantic()
154
166
  except NoResultFound:
155
167
  return None
156
168
 
157
169
  @enforce_types
158
- def list_files(
170
+ @trace_method
171
+ async def list_files(
159
172
  self, source_id: str, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50
160
173
  ) -> List[PydanticFileMetadata]:
161
174
  """List all files with optional pagination."""
162
- with db_registry.session() as session:
163
- files = FileMetadataModel.list(
175
+ async with db_registry.async_session() as session:
176
+ files_all = await FileMetadataModel.list_async(db_session=session, organization_id=actor.organization_id, source_id=source_id)
177
+ files = await FileMetadataModel.list_async(
164
178
  db_session=session, after=after, limit=limit, organization_id=actor.organization_id, source_id=source_id
165
179
  )
166
180
  return [file.to_pydantic() for file in files]
167
181
 
168
182
  @enforce_types
169
- def delete_file(self, file_id: str, actor: PydanticUser) -> PydanticFileMetadata:
183
+ @trace_method
184
+ async def delete_file(self, file_id: str, actor: PydanticUser) -> PydanticFileMetadata:
170
185
  """Delete a file by its ID."""
171
- with db_registry.session() as session:
172
- file = FileMetadataModel.read(db_session=session, identifier=file_id)
173
- file.hard_delete(db_session=session, actor=actor)
186
+ async with db_registry.async_session() as session:
187
+ file = await FileMetadataModel.read_async(db_session=session, identifier=file_id)
188
+ await file.hard_delete_async(db_session=session, actor=actor)
174
189
  return file.to_pydantic()
@@ -14,13 +14,14 @@ from letta.schemas.step import Step as PydanticStep
14
14
  from letta.schemas.user import User as PydanticUser
15
15
  from letta.server.db import db_registry
16
16
  from letta.services.helpers.noop_helper import singleton
17
- from letta.tracing import get_trace_id
17
+ from letta.tracing import get_trace_id, trace_method
18
18
  from letta.utils import enforce_types
19
19
 
20
20
 
21
21
  class StepManager:
22
22
 
23
23
  @enforce_types
24
+ @trace_method
24
25
  def list_steps(
25
26
  self,
26
27
  actor: PydanticUser,
@@ -54,6 +55,7 @@ class StepManager:
54
55
  return [step.to_pydantic() for step in steps]
55
56
 
56
57
  @enforce_types
58
+ @trace_method
57
59
  def log_step(
58
60
  self,
59
61
  actor: PydanticUser,
@@ -96,6 +98,7 @@ class StepManager:
96
98
  return new_step.to_pydantic()
97
99
 
98
100
  @enforce_types
101
+ @trace_method
99
102
  async def log_step_async(
100
103
  self,
101
104
  actor: PydanticUser,
@@ -138,12 +141,14 @@ class StepManager:
138
141
  return new_step.to_pydantic()
139
142
 
140
143
  @enforce_types
144
+ @trace_method
141
145
  def get_step(self, step_id: str, actor: PydanticUser) -> PydanticStep:
142
146
  with db_registry.session() as session:
143
147
  step = StepModel.read(db_session=session, identifier=step_id, actor=actor)
144
148
  return step.to_pydantic()
145
149
 
146
150
  @enforce_types
151
+ @trace_method
147
152
  def update_step_transaction_id(self, actor: PydanticUser, step_id: str, transaction_id: str) -> PydanticStep:
148
153
  """Update the transaction ID for a step.
149
154
 
@@ -236,6 +241,7 @@ class NoopStepManager(StepManager):
236
241
  """
237
242
 
238
243
  @enforce_types
244
+ @trace_method
239
245
  def log_step(
240
246
  self,
241
247
  actor: PydanticUser,
@@ -253,6 +259,7 @@ class NoopStepManager(StepManager):
253
259
  return
254
260
 
255
261
  @enforce_types
262
+ @trace_method
256
263
  async def log_step_async(
257
264
  self,
258
265
  actor: PydanticUser,