letta-nightly 0.7.21.dev20250522104246__py3-none-any.whl → 0.7.22.dev20250523104244__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 +2 -2
- letta/agents/base_agent.py +4 -2
- letta/agents/letta_agent.py +3 -10
- letta/agents/letta_agent_batch.py +6 -6
- letta/cli/cli.py +0 -316
- letta/cli/cli_load.py +0 -52
- letta/client/client.py +2 -1554
- letta/data_sources/connectors.py +4 -2
- letta/functions/ast_parsers.py +33 -43
- letta/groups/sleeptime_multi_agent_v2.py +49 -13
- letta/jobs/llm_batch_job_polling.py +3 -3
- letta/jobs/scheduler.py +20 -19
- letta/llm_api/anthropic_client.py +3 -0
- letta/llm_api/google_vertex_client.py +5 -0
- letta/llm_api/openai_client.py +5 -0
- letta/main.py +2 -362
- letta/server/db.py +5 -0
- letta/server/rest_api/routers/v1/agents.py +72 -43
- letta/server/rest_api/routers/v1/llms.py +2 -2
- letta/server/rest_api/routers/v1/messages.py +5 -3
- letta/server/rest_api/routers/v1/sandbox_configs.py +18 -18
- letta/server/rest_api/routers/v1/sources.py +49 -36
- letta/server/server.py +53 -22
- letta/services/agent_manager.py +797 -124
- letta/services/block_manager.py +14 -62
- letta/services/group_manager.py +37 -0
- letta/services/identity_manager.py +9 -0
- letta/services/job_manager.py +17 -0
- letta/services/llm_batch_manager.py +88 -64
- letta/services/message_manager.py +19 -0
- letta/services/organization_manager.py +10 -0
- letta/services/passage_manager.py +13 -0
- letta/services/per_agent_lock_manager.py +4 -0
- letta/services/provider_manager.py +34 -0
- letta/services/sandbox_config_manager.py +130 -0
- letta/services/source_manager.py +59 -44
- letta/services/step_manager.py +8 -1
- letta/services/tool_manager.py +21 -0
- letta/services/tool_sandbox/e2b_sandbox.py +4 -2
- letta/services/tool_sandbox/local_sandbox.py +7 -3
- letta/services/user_manager.py +16 -0
- {letta_nightly-0.7.21.dev20250522104246.dist-info → letta_nightly-0.7.22.dev20250523104244.dist-info}/METADATA +1 -1
- {letta_nightly-0.7.21.dev20250522104246.dist-info → letta_nightly-0.7.22.dev20250523104244.dist-info}/RECORD +46 -50
- letta/__main__.py +0 -3
- letta/benchmark/benchmark.py +0 -98
- letta/benchmark/constants.py +0 -14
- letta/cli/cli_config.py +0 -227
- {letta_nightly-0.7.21.dev20250522104246.dist-info → letta_nightly-0.7.22.dev20250523104244.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.21.dev20250522104246.dist-info → letta_nightly-0.7.22.dev20250523104244.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.21.dev20250522104246.dist-info → letta_nightly-0.7.22.dev20250523104244.dist-info}/entry_points.txt +0 -0
letta/data_sources/connectors.py
CHANGED
@@ -37,7 +37,9 @@ class DataConnector:
|
|
37
37
|
"""
|
38
38
|
|
39
39
|
|
40
|
-
def load_data(
|
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):
|
letta/functions/ast_parsers.py
CHANGED
@@ -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',
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
#
|
108
|
-
if
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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(
|
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(
|
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.
|
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.
|
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(
|
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(
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
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
|
38
|
-
|
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
|
-
|
44
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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,
|
letta/llm_api/openai_client.py
CHANGED
@@ -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,
|