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
@@ -37,7 +37,9 @@ class DataConnector:
37
37
  """
38
38
 
39
39
 
40
- def load_data(connector: DataConnector, source: Source, passage_manager: PassageManager, source_manager: SourceManager, actor: "User"):
40
+ async def load_data(
41
+ connector: DataConnector, source: Source, passage_manager: PassageManager, source_manager: SourceManager, actor: "User"
42
+ ):
41
43
  """Load data from a connector (generates file and passages) into a specified source_id, associated with a user_id."""
42
44
  embedding_config = source.embedding_config
43
45
 
@@ -51,7 +53,7 @@ def load_data(connector: DataConnector, source: Source, passage_manager: Passage
51
53
  file_count = 0
52
54
  for file_metadata in connector.find_files(source):
53
55
  file_count += 1
54
- source_manager.create_file(file_metadata, actor)
56
+ await source_manager.create_file(file_metadata, actor)
55
57
 
56
58
  # generate passages
57
59
  for passage_text, passage_metadata in connector.generate_passages(file_metadata, chunk_size=embedding_config.embedding_chunk_size):
@@ -1,5 +1,7 @@
1
1
  import ast
2
+ import builtins
2
3
  import json
4
+ import typing
3
5
  from typing import Dict, Optional, Tuple
4
6
 
5
7
  from letta.errors import LettaToolCreateError
@@ -22,7 +24,7 @@ def resolve_type(annotation: str):
22
24
  Resolve a type annotation string into a Python type.
23
25
 
24
26
  Args:
25
- annotation (str): The annotation string (e.g., 'int', 'list', etc.).
27
+ annotation (str): The annotation string (e.g., 'int', 'list[int]', 'dict[str, int]').
26
28
 
27
29
  Returns:
28
30
  type: The corresponding Python type.
@@ -34,24 +36,17 @@ def resolve_type(annotation: str):
34
36
  return BUILTIN_TYPES[annotation]
35
37
 
36
38
  try:
37
- if annotation.startswith("list["):
38
- inner_type = annotation[len("list[") : -1]
39
- resolve_type(inner_type)
40
- return list
41
- elif annotation.startswith("dict["):
42
- inner_types = annotation[len("dict[") : -1]
43
- key_type, value_type = inner_types.split(",")
44
- return dict
45
- elif annotation.startswith("tuple["):
46
- inner_types = annotation[len("tuple[") : -1]
47
- [resolve_type(t.strip()) for t in inner_types.split(",")]
48
- return tuple
49
-
50
- parsed = ast.literal_eval(annotation)
51
- if isinstance(parsed, type):
52
- return parsed
53
- raise ValueError(f"Annotation '{annotation}' is not a recognized type.")
54
- except (ValueError, SyntaxError):
39
+ # Allow use of typing and builtins in a safe eval context
40
+ namespace = {
41
+ **vars(typing),
42
+ **vars(builtins),
43
+ "list": list,
44
+ "dict": dict,
45
+ "tuple": tuple,
46
+ "set": set,
47
+ }
48
+ return eval(annotation, namespace)
49
+ except Exception:
55
50
  raise ValueError(f"Unsupported annotation: {annotation}")
56
51
 
57
52
 
@@ -82,41 +77,36 @@ def get_function_annotations_from_source(source_code: str, function_name: str) -
82
77
 
83
78
 
84
79
  def coerce_dict_args_by_annotations(function_args: dict, annotations: Dict[str, str]) -> dict:
85
- """
86
- Coerce arguments in a dictionary to their annotated types.
87
-
88
- Args:
89
- function_args (dict): The original function arguments.
90
- annotations (Dict[str, str]): Argument annotations as strings.
91
-
92
- Returns:
93
- dict: The updated dictionary with coerced argument types.
94
-
95
- Raises:
96
- ValueError: If type coercion fails for an argument.
97
- """
98
- coerced_args = dict(function_args) # Shallow copy for mutation safety
80
+ coerced_args = dict(function_args) # Shallow copy
99
81
 
100
82
  for arg_name, value in coerced_args.items():
101
83
  if arg_name in annotations:
102
84
  annotation_str = annotations[arg_name]
103
85
  try:
104
- # Resolve the type from the annotation
105
86
  arg_type = resolve_type(annotation_str)
106
87
 
107
- # Handle JSON-like inputs for dict and list types
108
- if arg_type in {dict, list} and isinstance(value, str):
88
+ # Always parse strings using literal_eval or json if possible
89
+ if isinstance(value, str):
109
90
  try:
110
- # First, try JSON parsing
111
91
  value = json.loads(value)
112
92
  except json.JSONDecodeError:
113
- # Fall back to literal_eval for Python-specific literals
114
- value = ast.literal_eval(value)
115
-
116
- # Coerce the value to the resolved type
117
- coerced_args[arg_name] = arg_type(value)
118
- except (TypeError, ValueError, json.JSONDecodeError, SyntaxError) as e:
93
+ try:
94
+ value = ast.literal_eval(value)
95
+ except (SyntaxError, ValueError) as e:
96
+ if arg_type is not str:
97
+ raise ValueError(f"Failed to coerce argument '{arg_name}' to {annotation_str}: {e}")
98
+
99
+ origin = typing.get_origin(arg_type)
100
+ if origin in (list, dict, tuple, set):
101
+ # Let the origin (e.g., list) handle coercion
102
+ coerced_args[arg_name] = origin(value)
103
+ else:
104
+ # Coerce simple types (e.g., int, float)
105
+ coerced_args[arg_name] = arg_type(value)
106
+
107
+ except Exception as e:
119
108
  raise ValueError(f"Failed to coerce argument '{arg_name}' to {annotation_str}: {e}")
109
+
120
110
  return coerced_args
121
111
 
122
112
 
@@ -19,6 +19,8 @@ from letta.services.group_manager import GroupManager
19
19
  from letta.services.job_manager import JobManager
20
20
  from letta.services.message_manager import MessageManager
21
21
  from letta.services.passage_manager import PassageManager
22
+ from letta.services.step_manager import NoopStepManager, StepManager
23
+ from letta.services.telemetry_manager import NoopTelemetryManager, TelemetryManager
22
24
 
23
25
 
24
26
  class SleeptimeMultiAgentV2(BaseAgent):
@@ -32,6 +34,8 @@ class SleeptimeMultiAgentV2(BaseAgent):
32
34
  group_manager: GroupManager,
33
35
  job_manager: JobManager,
34
36
  actor: User,
37
+ step_manager: StepManager = NoopStepManager(),
38
+ telemetry_manager: TelemetryManager = NoopTelemetryManager(),
35
39
  group: Optional[Group] = None,
36
40
  ):
37
41
  super().__init__(
@@ -45,11 +49,18 @@ class SleeptimeMultiAgentV2(BaseAgent):
45
49
  self.passage_manager = passage_manager
46
50
  self.group_manager = group_manager
47
51
  self.job_manager = job_manager
52
+ self.step_manager = step_manager
53
+ self.telemetry_manager = telemetry_manager
48
54
  # Group settings
49
55
  assert group.manager_type == ManagerType.sleeptime, f"Expected group manager type to be 'sleeptime', got {group.manager_type}"
50
56
  self.group = group
51
57
 
52
- async def step(self, input_messages: List[MessageCreate], max_steps: int = 10) -> LettaResponse:
58
+ async def step(
59
+ self,
60
+ input_messages: List[MessageCreate],
61
+ max_steps: int = 10,
62
+ use_assistant_message: bool = True,
63
+ ) -> LettaResponse:
53
64
  run_ids = []
54
65
 
55
66
  # Prepare new messages
@@ -68,22 +79,26 @@ class SleeptimeMultiAgentV2(BaseAgent):
68
79
  block_manager=self.block_manager,
69
80
  passage_manager=self.passage_manager,
70
81
  actor=self.actor,
82
+ step_manager=self.step_manager,
83
+ telemetry_manager=self.telemetry_manager,
71
84
  )
72
85
  # Perform foreground agent step
73
- response = await foreground_agent.step(input_messages=new_messages, max_steps=max_steps)
86
+ response = await foreground_agent.step(
87
+ input_messages=new_messages, max_steps=max_steps, use_assistant_message=use_assistant_message
88
+ )
74
89
 
75
90
  # Get last response messages
76
91
  last_response_messages = foreground_agent.response_messages
77
92
 
78
93
  # Update turns counter
79
94
  if self.group.sleeptime_agent_frequency is not None and self.group.sleeptime_agent_frequency > 0:
80
- turns_counter = self.group_manager.bump_turns_counter(group_id=self.group.id, actor=self.actor)
95
+ turns_counter = await self.group_manager.bump_turns_counter_async(group_id=self.group.id, actor=self.actor)
81
96
 
82
97
  # Perform participant steps
83
98
  if self.group.sleeptime_agent_frequency is None or (
84
99
  turns_counter is not None and turns_counter % self.group.sleeptime_agent_frequency == 0
85
100
  ):
86
- last_processed_message_id = self.group_manager.get_last_processed_message_id_and_update(
101
+ last_processed_message_id = await self.group_manager.get_last_processed_message_id_and_update_async(
87
102
  group_id=self.group.id, last_processed_message_id=last_response_messages[-1].id, actor=self.actor
88
103
  )
89
104
  for participant_agent_id in self.group.agent_ids:
@@ -92,6 +107,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
92
107
  participant_agent_id,
93
108
  last_response_messages,
94
109
  last_processed_message_id,
110
+ use_assistant_message,
95
111
  )
96
112
  run_ids.append(run_id)
97
113
 
@@ -103,7 +119,13 @@ class SleeptimeMultiAgentV2(BaseAgent):
103
119
  response.usage.run_ids = run_ids
104
120
  return response
105
121
 
106
- async def step_stream(self, input_messages: List[MessageCreate], max_steps: int = 10) -> AsyncGenerator[str, None]:
122
+ async def step_stream(
123
+ self,
124
+ input_messages: List[MessageCreate],
125
+ max_steps: int = 10,
126
+ use_assistant_message: bool = True,
127
+ request_start_timestamp_ns: Optional[int] = None,
128
+ ) -> AsyncGenerator[str, None]:
107
129
  # Prepare new messages
108
130
  new_messages = []
109
131
  for message in input_messages:
@@ -120,9 +142,16 @@ class SleeptimeMultiAgentV2(BaseAgent):
120
142
  block_manager=self.block_manager,
121
143
  passage_manager=self.passage_manager,
122
144
  actor=self.actor,
145
+ step_manager=self.step_manager,
146
+ telemetry_manager=self.telemetry_manager,
123
147
  )
124
148
  # Perform foreground agent step
125
- async for chunk in foreground_agent.step_stream(input_messages=new_messages, max_steps=max_steps):
149
+ async for chunk in foreground_agent.step_stream(
150
+ input_messages=new_messages,
151
+ max_steps=max_steps,
152
+ use_assistant_message=use_assistant_message,
153
+ request_start_timestamp_ns=request_start_timestamp_ns,
154
+ ):
126
155
  yield chunk
127
156
 
128
157
  # Get response messages
@@ -130,20 +159,21 @@ class SleeptimeMultiAgentV2(BaseAgent):
130
159
 
131
160
  # Update turns counter
132
161
  if self.group.sleeptime_agent_frequency is not None and self.group.sleeptime_agent_frequency > 0:
133
- turns_counter = self.group_manager.bump_turns_counter(group_id=self.group.id, actor=self.actor)
162
+ turns_counter = await self.group_manager.bump_turns_counter_async(group_id=self.group.id, actor=self.actor)
134
163
 
135
164
  # Perform participant steps
136
165
  if self.group.sleeptime_agent_frequency is None or (
137
166
  turns_counter is not None and turns_counter % self.group.sleeptime_agent_frequency == 0
138
167
  ):
139
- last_processed_message_id = self.group_manager.get_last_processed_message_id_and_update(
168
+ last_processed_message_id = await self.group_manager.get_last_processed_message_id_and_update_async(
140
169
  group_id=self.group.id, last_processed_message_id=last_response_messages[-1].id, actor=self.actor
141
170
  )
142
171
  for sleeptime_agent_id in self.group.agent_ids:
143
- self._issue_background_task(
172
+ run_id = await self._issue_background_task(
144
173
  sleeptime_agent_id,
145
174
  last_response_messages,
146
175
  last_processed_message_id,
176
+ use_assistant_message,
147
177
  )
148
178
 
149
179
  async def _issue_background_task(
@@ -151,6 +181,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
151
181
  sleeptime_agent_id: str,
152
182
  response_messages: List[Message],
153
183
  last_processed_message_id: str,
184
+ use_assistant_message: bool = True,
154
185
  ) -> str:
155
186
  run = Run(
156
187
  user_id=self.actor.id,
@@ -160,7 +191,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
160
191
  "agent_id": sleeptime_agent_id,
161
192
  },
162
193
  )
163
- run = self.job_manager.create_job(pydantic_job=run, actor=self.actor)
194
+ run = await self.job_manager.create_job_async(pydantic_job=run, actor=self.actor)
164
195
 
165
196
  asyncio.create_task(
166
197
  self._participant_agent_step(
@@ -169,6 +200,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
169
200
  response_messages=response_messages,
170
201
  last_processed_message_id=last_processed_message_id,
171
202
  run_id=run.id,
203
+ use_assistant_message=True,
172
204
  )
173
205
  )
174
206
  return run.id
@@ -180,11 +212,12 @@ class SleeptimeMultiAgentV2(BaseAgent):
180
212
  response_messages: List[Message],
181
213
  last_processed_message_id: str,
182
214
  run_id: str,
215
+ use_assistant_message: bool = True,
183
216
  ) -> str:
184
217
  try:
185
218
  # Update job status
186
219
  job_update = JobUpdate(status=JobStatus.running)
187
- self.job_manager.update_job_by_id(job_id=run_id, job_update=job_update, actor=self.actor)
220
+ await self.job_manager.update_job_by_id_async(job_id=run_id, job_update=job_update, actor=self.actor)
188
221
 
189
222
  # Create conversation transcript
190
223
  prior_messages = []
@@ -221,11 +254,14 @@ class SleeptimeMultiAgentV2(BaseAgent):
221
254
  block_manager=self.block_manager,
222
255
  passage_manager=self.passage_manager,
223
256
  actor=self.actor,
257
+ step_manager=self.step_manager,
258
+ telemetry_manager=self.telemetry_manager,
224
259
  )
225
260
 
226
261
  # Perform sleeptime agent step
227
262
  result = await sleeptime_agent.step(
228
263
  input_messages=sleeptime_agent_messages,
264
+ use_assistant_message=use_assistant_message,
229
265
  )
230
266
 
231
267
  # Update job status
@@ -237,7 +273,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
237
273
  "agent_id": sleeptime_agent_id,
238
274
  },
239
275
  )
240
- self.job_manager.update_job_by_id(job_id=run_id, job_update=job_update, actor=self.actor)
276
+ await self.job_manager.update_job_by_id_async(job_id=run_id, job_update=job_update, actor=self.actor)
241
277
  return result
242
278
  except Exception as e:
243
279
  job_update = JobUpdate(
@@ -245,5 +281,5 @@ class SleeptimeMultiAgentV2(BaseAgent):
245
281
  completed_at=datetime.now(timezone.utc).replace(tzinfo=None),
246
282
  metadata={"error": str(e)},
247
283
  )
248
- self.job_manager.update_job_by_id(job_id=run_id, job_update=job_update, actor=self.actor)
284
+ await self.job_manager.update_job_by_id_async(job_id=run_id, job_update=job_update, actor=self.actor)
249
285
  raise
@@ -106,7 +106,7 @@ async def poll_batch_updates(server: SyncServer, batch_jobs: List[LLMBatchJob],
106
106
  results: List[BatchPollingResult] = await asyncio.gather(*coros)
107
107
 
108
108
  # Update the server with batch status changes
109
- server.batch_manager.bulk_update_llm_batch_statuses(updates=results)
109
+ await server.batch_manager.bulk_update_llm_batch_statuses_async(updates=results)
110
110
  logger.info(f"[Poll BatchJob] Bulk-updated {len(results)} LLM batch(es) in the DB at job level.")
111
111
 
112
112
  return results
@@ -197,13 +197,13 @@ async def poll_running_llm_batches(server: "SyncServer") -> List[LettaBatchRespo
197
197
  # 6. Bulk update all items for newly completed batch(es)
198
198
  if item_updates:
199
199
  metrics.updated_items_count = len(item_updates)
200
- server.batch_manager.bulk_update_batch_llm_items_results_by_agent(item_updates)
200
+ await server.batch_manager.bulk_update_batch_llm_items_results_by_agent_async(item_updates)
201
201
 
202
202
  # ─── Kick off post‑processing for each batch that just completed ───
203
203
  completed = [r for r in batch_results if r.request_status == JobStatus.completed]
204
204
 
205
205
  async def _resume(batch_row: LLMBatchJob) -> LettaBatchResponse:
206
- actor: User = server.user_manager.get_user_by_id(batch_row.created_by_id)
206
+ actor: User = await server.user_manager.get_actor_by_id_async(batch_row.created_by_id)
207
207
  runner = LettaAgentBatch(
208
208
  message_manager=server.message_manager,
209
209
  agent_manager=server.agent_manager,
letta/jobs/scheduler.py CHANGED
@@ -4,10 +4,11 @@ from typing import Optional
4
4
 
5
5
  from apscheduler.schedulers.asyncio import AsyncIOScheduler
6
6
  from apscheduler.triggers.interval import IntervalTrigger
7
+ from sqlalchemy import text
7
8
 
8
9
  from letta.jobs.llm_batch_job_polling import poll_running_llm_batches
9
10
  from letta.log import get_logger
10
- from letta.server.db import db_context
11
+ from letta.server.db import db_registry
11
12
  from letta.server.server import SyncServer
12
13
  from letta.settings import settings
13
14
 
@@ -34,18 +35,16 @@ async def _try_acquire_lock_and_start_scheduler(server: SyncServer) -> bool:
34
35
  acquired_lock = False
35
36
  try:
36
37
  # Use a temporary connection context for the attempt initially
37
- with db_context() as session:
38
- engine = session.get_bind()
39
- # Get raw connection - MUST be kept open if lock is acquired
40
- raw_conn = engine.raw_connection()
41
- cur = raw_conn.cursor()
38
+ async with db_registry.async_session() as session:
39
+ raw_conn = await session.connection()
42
40
 
43
- cur.execute("SELECT pg_try_advisory_lock(CAST(%s AS bigint))", (ADVISORY_LOCK_KEY,))
44
- acquired_lock = cur.fetchone()[0]
41
+ # Try to acquire the advisory lock
42
+ sql = text("SELECT pg_try_advisory_lock(CAST(:lock_key AS bigint))")
43
+ result = await session.execute(sql, {"lock_key": ADVISORY_LOCK_KEY})
44
+ acquired_lock = result.scalar_one()
45
45
 
46
46
  if not acquired_lock:
47
- cur.close()
48
- raw_conn.close()
47
+ await raw_conn.close()
49
48
  logger.info("Scheduler lock held by another instance.")
50
49
  return False
51
50
 
@@ -106,14 +105,14 @@ async def _try_acquire_lock_and_start_scheduler(server: SyncServer) -> bool:
106
105
  # Clean up temporary resources if lock wasn't acquired or error occurred before storing
107
106
  if cur:
108
107
  try:
109
- cur.close()
110
- except:
111
- pass
108
+ await cur.close()
109
+ except Exception as e:
110
+ logger.warning(f"Error closing cursor: {e}")
112
111
  if raw_conn:
113
112
  try:
114
- raw_conn.close()
115
- except:
116
- pass
113
+ await raw_conn.close()
114
+ except Exception as e:
115
+ logger.warning(f"Error closing connection: {e}")
117
116
 
118
117
 
119
118
  async def _background_lock_retry_loop(server: SyncServer):
@@ -161,7 +160,9 @@ async def _release_advisory_lock():
161
160
  try:
162
161
  if not lock_conn.closed:
163
162
  if not lock_cur.closed:
164
- lock_cur.execute("SELECT pg_advisory_unlock(CAST(%s AS bigint))", (ADVISORY_LOCK_KEY,))
163
+ # Use SQLAlchemy text() for raw SQL
164
+ unlock_sql = text("SELECT pg_advisory_unlock(CAST(:lock_key AS bigint))")
165
+ lock_cur.execute(unlock_sql, {"lock_key": ADVISORY_LOCK_KEY})
165
166
  lock_cur.fetchone() # Consume result
166
167
  lock_conn.commit()
167
168
  logger.info(f"Executed pg_advisory_unlock for lock {ADVISORY_LOCK_KEY}")
@@ -175,12 +176,12 @@ async def _release_advisory_lock():
175
176
  # Ensure resources are closed regardless of unlock success
176
177
  try:
177
178
  if lock_cur and not lock_cur.closed:
178
- lock_cur.close()
179
+ await lock_cur.close()
179
180
  except Exception as e:
180
181
  logger.error(f"Error closing advisory lock cursor: {e}", exc_info=True)
181
182
  try:
182
183
  if lock_conn and not lock_conn.closed:
183
- lock_conn.close()
184
+ await lock_conn.close()
184
185
  logger.info("Closed database connection that held advisory lock.")
185
186
  except Exception as e:
186
187
  logger.error(f"Error closing advisory lock connection: {e}", exc_info=True)
@@ -45,11 +45,13 @@ logger = get_logger(__name__)
45
45
 
46
46
  class AnthropicClient(LLMClientBase):
47
47
 
48
+ @trace_method
48
49
  def request(self, request_data: dict, llm_config: LLMConfig) -> dict:
49
50
  client = self._get_anthropic_client(llm_config, async_client=False)
50
51
  response = client.beta.messages.create(**request_data, betas=["tools-2024-04-04"])
51
52
  return response.model_dump()
52
53
 
54
+ @trace_method
53
55
  async def request_async(self, request_data: dict, llm_config: LLMConfig) -> dict:
54
56
  client = self._get_anthropic_client(llm_config, async_client=True)
55
57
  response = await client.beta.messages.create(**request_data, betas=["tools-2024-04-04"])
@@ -339,6 +341,7 @@ class AnthropicClient(LLMClientBase):
339
341
 
340
342
  # TODO: Input messages doesn't get used here
341
343
  # TODO: Clean up this interface
344
+ @trace_method
342
345
  def convert_response_to_chat_completion(
343
346
  self,
344
347
  response_data: dict,
@@ -17,6 +17,7 @@ from letta.schemas.message import Message as PydanticMessage
17
17
  from letta.schemas.openai.chat_completion_request import Tool
18
18
  from letta.schemas.openai.chat_completion_response import ChatCompletionResponse, Choice, FunctionCall, Message, ToolCall, UsageStatistics
19
19
  from letta.settings import model_settings, settings
20
+ from letta.tracing import trace_method
20
21
  from letta.utils import get_tool_call_id
21
22
 
22
23
  logger = get_logger(__name__)
@@ -32,6 +33,7 @@ class GoogleVertexClient(LLMClientBase):
32
33
  http_options={"api_version": "v1"},
33
34
  )
34
35
 
36
+ @trace_method
35
37
  def request(self, request_data: dict, llm_config: LLMConfig) -> dict:
36
38
  """
37
39
  Performs underlying request to llm and returns raw response.
@@ -44,6 +46,7 @@ class GoogleVertexClient(LLMClientBase):
44
46
  )
45
47
  return response.model_dump()
46
48
 
49
+ @trace_method
47
50
  async def request_async(self, request_data: dict, llm_config: LLMConfig) -> dict:
48
51
  """
49
52
  Performs underlying request to llm and returns raw response.
@@ -189,6 +192,7 @@ class GoogleVertexClient(LLMClientBase):
189
192
 
190
193
  return [{"functionDeclarations": function_list}]
191
194
 
195
+ @trace_method
192
196
  def build_request_data(
193
197
  self,
194
198
  messages: List[PydanticMessage],
@@ -248,6 +252,7 @@ class GoogleVertexClient(LLMClientBase):
248
252
 
249
253
  return request_data
250
254
 
255
+ @trace_method
251
256
  def convert_response_to_chat_completion(
252
257
  self,
253
258
  response_data: dict,
@@ -32,6 +32,7 @@ from letta.schemas.openai.chat_completion_request import Tool as OpenAITool
32
32
  from letta.schemas.openai.chat_completion_request import ToolFunctionChoice, cast_message_to_subtype
33
33
  from letta.schemas.openai.chat_completion_response import ChatCompletionResponse
34
34
  from letta.settings import model_settings
35
+ from letta.tracing import trace_method
35
36
 
36
37
  logger = get_logger(__name__)
37
38
 
@@ -124,6 +125,7 @@ class OpenAIClient(LLMClientBase):
124
125
 
125
126
  return kwargs
126
127
 
128
+ @trace_method
127
129
  def build_request_data(
128
130
  self,
129
131
  messages: List[PydanticMessage],
@@ -213,6 +215,7 @@ class OpenAIClient(LLMClientBase):
213
215
 
214
216
  return data.model_dump(exclude_unset=True)
215
217
 
218
+ @trace_method
216
219
  def request(self, request_data: dict, llm_config: LLMConfig) -> dict:
217
220
  """
218
221
  Performs underlying synchronous request to OpenAI API and returns raw response dict.
@@ -222,6 +225,7 @@ class OpenAIClient(LLMClientBase):
222
225
  response: ChatCompletion = client.chat.completions.create(**request_data)
223
226
  return response.model_dump()
224
227
 
228
+ @trace_method
225
229
  async def request_async(self, request_data: dict, llm_config: LLMConfig) -> dict:
226
230
  """
227
231
  Performs underlying asynchronous request to OpenAI API and returns raw response dict.
@@ -230,6 +234,7 @@ class OpenAIClient(LLMClientBase):
230
234
  response: ChatCompletion = await client.chat.completions.create(**request_data)
231
235
  return response.model_dump()
232
236
 
237
+ @trace_method
233
238
  def convert_response_to_chat_completion(
234
239
  self,
235
240
  response_data: dict,