letta-nightly 0.11.7.dev20251008104128__py3-none-any.whl → 0.12.0.dev20251009203644__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/agents/letta_agent_v3.py +33 -5
- letta/database_utils.py +161 -0
- letta/interfaces/anthropic_streaming_interface.py +21 -9
- letta/interfaces/gemini_streaming_interface.py +7 -5
- letta/interfaces/openai_streaming_interface.py +42 -30
- letta/llm_api/anthropic_client.py +36 -16
- letta/llm_api/google_vertex_client.py +1 -0
- letta/orm/__init__.py +1 -0
- letta/orm/run_metrics.py +82 -0
- letta/schemas/letta_message.py +29 -12
- letta/schemas/message.py +192 -51
- letta/schemas/run_metrics.py +21 -0
- letta/server/db.py +3 -10
- letta/server/rest_api/interface.py +85 -41
- letta/server/rest_api/routers/v1/providers.py +34 -0
- letta/server/rest_api/routers/v1/runs.py +27 -18
- letta/server/server.py +22 -0
- letta/services/context_window_calculator/token_counter.py +1 -1
- letta/services/helpers/run_manager_helper.py +5 -21
- letta/services/run_manager.py +63 -0
- letta/system.py +5 -1
- {letta_nightly-0.11.7.dev20251008104128.dist-info → letta_nightly-0.12.0.dev20251009203644.dist-info}/METADATA +1 -1
- {letta_nightly-0.11.7.dev20251008104128.dist-info → letta_nightly-0.12.0.dev20251009203644.dist-info}/RECORD +27 -24
- {letta_nightly-0.11.7.dev20251008104128.dist-info → letta_nightly-0.12.0.dev20251009203644.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20251008104128.dist-info → letta_nightly-0.12.0.dev20251009203644.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20251008104128.dist-info → letta_nightly-0.12.0.dev20251009203644.dist-info}/licenses/LICENSE +0 -0
letta/schemas/letta_message.py
CHANGED
@@ -190,7 +190,8 @@ class ToolCallMessage(LettaMessage):
|
|
190
190
|
message_type: Literal[MessageType.tool_call_message] = Field(
|
191
191
|
default=MessageType.tool_call_message, description="The type of the message."
|
192
192
|
)
|
193
|
-
tool_call: Union[ToolCall, ToolCallDelta]
|
193
|
+
tool_call: Union[ToolCall, ToolCallDelta] = Field(..., deprecated=True)
|
194
|
+
tool_calls: Optional[Union[List[ToolCall], ToolCallDelta]] = None
|
194
195
|
|
195
196
|
def model_dump(self, *args, **kwargs):
|
196
197
|
"""
|
@@ -198,8 +199,14 @@ class ToolCallMessage(LettaMessage):
|
|
198
199
|
"""
|
199
200
|
kwargs["exclude_none"] = True
|
200
201
|
data = super().model_dump(*args, **kwargs)
|
201
|
-
if isinstance(data
|
202
|
+
if isinstance(data.get("tool_call"), dict):
|
202
203
|
data["tool_call"] = {k: v for k, v in data["tool_call"].items() if v is not None}
|
204
|
+
if isinstance(data.get("tool_calls"), dict):
|
205
|
+
data["tool_calls"] = {k: v for k, v in data["tool_calls"].items() if v is not None}
|
206
|
+
elif isinstance(data.get("tool_calls"), list):
|
207
|
+
data["tool_calls"] = [
|
208
|
+
{k: v for k, v in item.items() if v is not None} if isinstance(item, dict) else item for item in data["tool_calls"]
|
209
|
+
]
|
203
210
|
return data
|
204
211
|
|
205
212
|
class Config:
|
@@ -226,6 +233,14 @@ class ToolCallMessage(LettaMessage):
|
|
226
233
|
return v
|
227
234
|
|
228
235
|
|
236
|
+
class ToolReturn(BaseModel):
|
237
|
+
tool_return: str
|
238
|
+
status: Literal["success", "error"]
|
239
|
+
tool_call_id: str
|
240
|
+
stdout: Optional[List[str]] = None
|
241
|
+
stderr: Optional[List[str]] = None
|
242
|
+
|
243
|
+
|
229
244
|
class ToolReturnMessage(LettaMessage):
|
230
245
|
"""
|
231
246
|
A message representing the return value of a tool call (generated by Letta executing the requested tool).
|
@@ -234,21 +249,23 @@ class ToolReturnMessage(LettaMessage):
|
|
234
249
|
id (str): The ID of the message
|
235
250
|
date (datetime): The date the message was created in ISO format
|
236
251
|
name (Optional[str]): The name of the sender of the message
|
237
|
-
tool_return (str): The return value of the tool
|
238
|
-
status (Literal["success", "error"]): The status of the tool call
|
239
|
-
tool_call_id (str): A unique identifier for the tool call that generated this message
|
240
|
-
stdout (Optional[List(str)]): Captured stdout (e.g. prints, logs) from the tool invocation
|
241
|
-
stderr (Optional[List(str)]): Captured stderr from the tool invocation
|
252
|
+
tool_return (str): The return value of the tool (deprecated, use tool_returns)
|
253
|
+
status (Literal["success", "error"]): The status of the tool call (deprecated, use tool_returns)
|
254
|
+
tool_call_id (str): A unique identifier for the tool call that generated this message (deprecated, use tool_returns)
|
255
|
+
stdout (Optional[List(str)]): Captured stdout (e.g. prints, logs) from the tool invocation (deprecated, use tool_returns)
|
256
|
+
stderr (Optional[List(str)]): Captured stderr from the tool invocation (deprecated, use tool_returns)
|
257
|
+
tool_returns (Optional[List[ToolReturn]]): List of tool returns for multi-tool support
|
242
258
|
"""
|
243
259
|
|
244
260
|
message_type: Literal[MessageType.tool_return_message] = Field(
|
245
261
|
default=MessageType.tool_return_message, description="The type of the message."
|
246
262
|
)
|
247
|
-
tool_return: str
|
248
|
-
status: Literal["success", "error"]
|
249
|
-
tool_call_id: str
|
250
|
-
stdout: Optional[List[str]] = None
|
251
|
-
stderr: Optional[List[str]] = None
|
263
|
+
tool_return: str = Field(..., deprecated=True)
|
264
|
+
status: Literal["success", "error"] = Field(..., deprecated=True)
|
265
|
+
tool_call_id: str = Field(..., deprecated=True)
|
266
|
+
stdout: Optional[List[str]] = Field(None, deprecated=True)
|
267
|
+
stderr: Optional[List[str]] = Field(None, deprecated=True)
|
268
|
+
tool_returns: Optional[List[ToolReturn]] = None
|
252
269
|
|
253
270
|
|
254
271
|
class ApprovalRequestMessage(LettaMessage):
|
letta/schemas/message.py
CHANGED
@@ -492,23 +492,27 @@ class Message(BaseMessage):
|
|
492
492
|
assistant_message_tool_kwarg: str = DEFAULT_MESSAGE_TOOL_KWARG,
|
493
493
|
) -> List[LettaMessage]:
|
494
494
|
messages = []
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
495
|
+
|
496
|
+
# If assistant mode is off, just create one ToolCallMessage with all tool calls
|
497
|
+
if not use_assistant_message:
|
498
|
+
all_tool_call_objs = [
|
499
|
+
ToolCall(
|
500
|
+
name=tool_call.function.name,
|
501
|
+
arguments=tool_call.function.arguments,
|
502
|
+
tool_call_id=tool_call.id,
|
503
|
+
)
|
504
|
+
for tool_call in self.tool_calls
|
505
|
+
]
|
506
|
+
|
507
|
+
if all_tool_call_objs:
|
508
|
+
otid = Message.generate_otid_from_id(self.id, current_message_count)
|
507
509
|
messages.append(
|
508
|
-
|
510
|
+
ToolCallMessage(
|
509
511
|
id=self.id,
|
510
512
|
date=self.created_at,
|
511
|
-
|
513
|
+
# use first tool call for the deprecated field
|
514
|
+
tool_call=all_tool_call_objs[0],
|
515
|
+
tool_calls=all_tool_call_objs,
|
512
516
|
name=self.name,
|
513
517
|
otid=otid,
|
514
518
|
sender_id=self.sender_id,
|
@@ -517,16 +521,41 @@ class Message(BaseMessage):
|
|
517
521
|
run_id=self.run_id,
|
518
522
|
)
|
519
523
|
)
|
520
|
-
|
524
|
+
return messages
|
525
|
+
|
526
|
+
collected_tool_calls = []
|
527
|
+
|
528
|
+
for tool_call in self.tool_calls:
|
529
|
+
otid = Message.generate_otid_from_id(self.id, current_message_count + len(messages))
|
530
|
+
|
531
|
+
if tool_call.function.name == assistant_message_tool_name:
|
532
|
+
if collected_tool_calls:
|
533
|
+
tool_call_message = ToolCallMessage(
|
534
|
+
id=self.id,
|
535
|
+
date=self.created_at,
|
536
|
+
# use first tool call for the deprecated field
|
537
|
+
tool_call=collected_tool_calls[0],
|
538
|
+
tool_calls=collected_tool_calls.copy(),
|
539
|
+
name=self.name,
|
540
|
+
otid=Message.generate_otid_from_id(self.id, current_message_count + len(messages)),
|
541
|
+
sender_id=self.sender_id,
|
542
|
+
step_id=self.step_id,
|
543
|
+
is_err=self.is_err,
|
544
|
+
run_id=self.run_id,
|
545
|
+
)
|
546
|
+
messages.append(tool_call_message)
|
547
|
+
collected_tool_calls = [] # reset the collection
|
548
|
+
|
549
|
+
try:
|
550
|
+
func_args = parse_json(tool_call.function.arguments)
|
551
|
+
message_string = validate_function_response(func_args[assistant_message_tool_kwarg], 0, truncate=False)
|
552
|
+
except KeyError:
|
553
|
+
raise ValueError(f"Function call {tool_call.function.name} missing {assistant_message_tool_kwarg} argument")
|
521
554
|
messages.append(
|
522
|
-
|
555
|
+
AssistantMessage(
|
523
556
|
id=self.id,
|
524
557
|
date=self.created_at,
|
525
|
-
|
526
|
-
name=tool_call.function.name,
|
527
|
-
arguments=tool_call.function.arguments,
|
528
|
-
tool_call_id=tool_call.id,
|
529
|
-
),
|
558
|
+
content=message_string,
|
530
559
|
name=self.name,
|
531
560
|
otid=otid,
|
532
561
|
sender_id=self.sender_id,
|
@@ -535,6 +564,32 @@ class Message(BaseMessage):
|
|
535
564
|
run_id=self.run_id,
|
536
565
|
)
|
537
566
|
)
|
567
|
+
else:
|
568
|
+
# non-assistant tool call, collect it
|
569
|
+
tool_call_obj = ToolCall(
|
570
|
+
name=tool_call.function.name,
|
571
|
+
arguments=tool_call.function.arguments,
|
572
|
+
tool_call_id=tool_call.id,
|
573
|
+
)
|
574
|
+
collected_tool_calls.append(tool_call_obj)
|
575
|
+
|
576
|
+
# flush any remaining collected tool calls
|
577
|
+
if collected_tool_calls:
|
578
|
+
tool_call_message = ToolCallMessage(
|
579
|
+
id=self.id,
|
580
|
+
date=self.created_at,
|
581
|
+
# use first tool call for the deprecated field
|
582
|
+
tool_call=collected_tool_calls[0],
|
583
|
+
tool_calls=collected_tool_calls,
|
584
|
+
name=self.name,
|
585
|
+
otid=Message.generate_otid_from_id(self.id, current_message_count + len(messages)),
|
586
|
+
sender_id=self.sender_id,
|
587
|
+
step_id=self.step_id,
|
588
|
+
is_err=self.is_err,
|
589
|
+
run_id=self.run_id,
|
590
|
+
)
|
591
|
+
messages.append(tool_call_message)
|
592
|
+
|
538
593
|
return messages
|
539
594
|
|
540
595
|
def _convert_tool_return_message(self) -> List[ToolReturnMessage]:
|
@@ -556,6 +611,13 @@ class Message(BaseMessage):
|
|
556
611
|
if self.role != MessageRole.tool:
|
557
612
|
raise ValueError(f"Cannot convert message of type {self.role} to ToolReturnMessage")
|
558
613
|
|
614
|
+
# This is a very special buggy case during the double writing period
|
615
|
+
# where there is no tool call id on the tool return object, but it exists top level
|
616
|
+
# This is meant to be a short term patch - this can happen when people are using old agent files that were exported
|
617
|
+
# during a specific migration state
|
618
|
+
if len(self.tool_returns) == 1 and self.tool_call_id and not self.tool_returns[0].tool_call_id:
|
619
|
+
self.tool_returns[0].tool_call_id = self.tool_call_id
|
620
|
+
|
559
621
|
if self.tool_returns:
|
560
622
|
return self._convert_explicit_tool_returns()
|
561
623
|
|
@@ -647,6 +709,16 @@ class Message(BaseMessage):
|
|
647
709
|
Returns:
|
648
710
|
Configured ToolReturnMessage instance
|
649
711
|
"""
|
712
|
+
from letta.schemas.letta_message import ToolReturn as ToolReturnSchema
|
713
|
+
|
714
|
+
tool_return_obj = ToolReturnSchema(
|
715
|
+
tool_return=message_text,
|
716
|
+
status=status,
|
717
|
+
tool_call_id=tool_call_id,
|
718
|
+
stdout=stdout,
|
719
|
+
stderr=stderr,
|
720
|
+
)
|
721
|
+
|
650
722
|
return ToolReturnMessage(
|
651
723
|
id=self.id,
|
652
724
|
date=self.created_at,
|
@@ -655,6 +727,7 @@ class Message(BaseMessage):
|
|
655
727
|
tool_call_id=tool_call_id,
|
656
728
|
stdout=stdout,
|
657
729
|
stderr=stderr,
|
730
|
+
tool_returns=[tool_return_obj],
|
658
731
|
name=self.name,
|
659
732
|
otid=Message.generate_otid_from_id(self.id, otid_index),
|
660
733
|
sender_id=self.sender_id,
|
@@ -965,7 +1038,13 @@ class Message(BaseMessage):
|
|
965
1038
|
}
|
966
1039
|
|
967
1040
|
elif self.role == "assistant" or self.role == "approval":
|
968
|
-
|
1041
|
+
try:
|
1042
|
+
assert self.tool_calls is not None or text_content is not None, vars(self)
|
1043
|
+
except AssertionError as e:
|
1044
|
+
# relax check if this message only contains reasoning content
|
1045
|
+
if self.content is not None and len(self.content) > 0 and isinstance(self.content[0], ReasoningContent):
|
1046
|
+
return None
|
1047
|
+
raise e
|
969
1048
|
|
970
1049
|
# if native content, then put it directly inside the content
|
971
1050
|
if native_content:
|
@@ -1040,6 +1119,7 @@ class Message(BaseMessage):
|
|
1040
1119
|
put_inner_thoughts_in_kwargs: bool = False,
|
1041
1120
|
use_developer_message: bool = False,
|
1042
1121
|
) -> List[dict]:
|
1122
|
+
messages = Message.filter_messages_for_llm_api(messages)
|
1043
1123
|
result = [
|
1044
1124
|
m.to_openai_dict(
|
1045
1125
|
max_tool_id_length=max_tool_id_length,
|
@@ -1149,6 +1229,7 @@ class Message(BaseMessage):
|
|
1149
1229
|
messages: List[Message],
|
1150
1230
|
max_tool_id_length: int = TOOL_CALL_ID_MAX_LEN,
|
1151
1231
|
) -> List[dict]:
|
1232
|
+
messages = Message.filter_messages_for_llm_api(messages)
|
1152
1233
|
result = []
|
1153
1234
|
for message in messages:
|
1154
1235
|
result.extend(message.to_openai_responses_dicts(max_tool_id_length=max_tool_id_length))
|
@@ -1156,6 +1237,7 @@ class Message(BaseMessage):
|
|
1156
1237
|
|
1157
1238
|
def to_anthropic_dict(
|
1158
1239
|
self,
|
1240
|
+
current_model: str,
|
1159
1241
|
inner_thoughts_xml_tag="thinking",
|
1160
1242
|
put_inner_thoughts_in_kwargs: bool = False,
|
1161
1243
|
# if true, then treat the content field as AssistantMessage
|
@@ -1242,20 +1324,22 @@ class Message(BaseMessage):
|
|
1242
1324
|
for content_part in self.content:
|
1243
1325
|
# TextContent, ImageContent, ToolCallContent, ToolReturnContent, ReasoningContent, RedactedReasoningContent, OmittedReasoningContent
|
1244
1326
|
if isinstance(content_part, ReasoningContent):
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1248
|
-
|
1249
|
-
|
1250
|
-
|
1251
|
-
|
1327
|
+
if current_model == self.model:
|
1328
|
+
content.append(
|
1329
|
+
{
|
1330
|
+
"type": "thinking",
|
1331
|
+
"thinking": content_part.reasoning,
|
1332
|
+
"signature": content_part.signature,
|
1333
|
+
}
|
1334
|
+
)
|
1252
1335
|
elif isinstance(content_part, RedactedReasoningContent):
|
1253
|
-
|
1254
|
-
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1258
|
-
|
1336
|
+
if current_model == self.model:
|
1337
|
+
content.append(
|
1338
|
+
{
|
1339
|
+
"type": "redacted_thinking",
|
1340
|
+
"data": content_part.data,
|
1341
|
+
}
|
1342
|
+
)
|
1259
1343
|
elif isinstance(content_part, TextContent):
|
1260
1344
|
content.append(
|
1261
1345
|
{
|
@@ -1272,20 +1356,22 @@ class Message(BaseMessage):
|
|
1272
1356
|
if self.content is not None and len(self.content) >= 1:
|
1273
1357
|
for content_part in self.content:
|
1274
1358
|
if isinstance(content_part, ReasoningContent):
|
1275
|
-
|
1276
|
-
|
1277
|
-
|
1278
|
-
|
1279
|
-
|
1280
|
-
|
1281
|
-
|
1359
|
+
if current_model == self.model:
|
1360
|
+
content.append(
|
1361
|
+
{
|
1362
|
+
"type": "thinking",
|
1363
|
+
"thinking": content_part.reasoning,
|
1364
|
+
"signature": content_part.signature,
|
1365
|
+
}
|
1366
|
+
)
|
1282
1367
|
if isinstance(content_part, RedactedReasoningContent):
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
|
1287
|
-
|
1288
|
-
|
1368
|
+
if current_model == self.model:
|
1369
|
+
content.append(
|
1370
|
+
{
|
1371
|
+
"type": "redacted_thinking",
|
1372
|
+
"data": content_part.data,
|
1373
|
+
}
|
1374
|
+
)
|
1289
1375
|
if isinstance(content_part, TextContent):
|
1290
1376
|
content.append(
|
1291
1377
|
{
|
@@ -1349,14 +1435,17 @@ class Message(BaseMessage):
|
|
1349
1435
|
@staticmethod
|
1350
1436
|
def to_anthropic_dicts_from_list(
|
1351
1437
|
messages: List[Message],
|
1438
|
+
current_model: str,
|
1352
1439
|
inner_thoughts_xml_tag: str = "thinking",
|
1353
1440
|
put_inner_thoughts_in_kwargs: bool = False,
|
1354
1441
|
# if true, then treat the content field as AssistantMessage
|
1355
1442
|
native_content: bool = False,
|
1356
1443
|
strip_request_heartbeat: bool = False,
|
1357
1444
|
) -> List[dict]:
|
1445
|
+
messages = Message.filter_messages_for_llm_api(messages)
|
1358
1446
|
result = [
|
1359
1447
|
m.to_anthropic_dict(
|
1448
|
+
current_model=current_model,
|
1360
1449
|
inner_thoughts_xml_tag=inner_thoughts_xml_tag,
|
1361
1450
|
put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
|
1362
1451
|
native_content=native_content,
|
@@ -1369,6 +1458,7 @@ class Message(BaseMessage):
|
|
1369
1458
|
|
1370
1459
|
def to_google_dict(
|
1371
1460
|
self,
|
1461
|
+
current_model: str,
|
1372
1462
|
put_inner_thoughts_in_kwargs: bool = True,
|
1373
1463
|
# if true, then treat the content field as AssistantMessage
|
1374
1464
|
native_content: bool = False,
|
@@ -1484,11 +1574,12 @@ class Message(BaseMessage):
|
|
1484
1574
|
for content in self.content:
|
1485
1575
|
if isinstance(content, TextContent):
|
1486
1576
|
native_part = {"text": content.text}
|
1487
|
-
if content.signature:
|
1577
|
+
if content.signature and current_model == self.model:
|
1488
1578
|
native_part["thought_signature"] = content.signature
|
1489
1579
|
native_google_content_parts.append(native_part)
|
1490
1580
|
elif isinstance(content, ReasoningContent):
|
1491
|
-
|
1581
|
+
if current_model == self.model:
|
1582
|
+
native_google_content_parts.append({"text": content.reasoning, "thought": True})
|
1492
1583
|
elif isinstance(content, ToolCallContent):
|
1493
1584
|
native_part = {
|
1494
1585
|
"function_call": {
|
@@ -1496,7 +1587,7 @@ class Message(BaseMessage):
|
|
1496
1587
|
"args": content.input,
|
1497
1588
|
},
|
1498
1589
|
}
|
1499
|
-
if content.signature:
|
1590
|
+
if content.signature and current_model == self.model:
|
1500
1591
|
native_part["thought_signature"] = content.signature
|
1501
1592
|
native_google_content_parts.append(native_part)
|
1502
1593
|
else:
|
@@ -1554,11 +1645,14 @@ class Message(BaseMessage):
|
|
1554
1645
|
@staticmethod
|
1555
1646
|
def to_google_dicts_from_list(
|
1556
1647
|
messages: List[Message],
|
1648
|
+
current_model: str,
|
1557
1649
|
put_inner_thoughts_in_kwargs: bool = True,
|
1558
1650
|
native_content: bool = False,
|
1559
1651
|
):
|
1652
|
+
messages = Message.filter_messages_for_llm_api(messages)
|
1560
1653
|
result = [
|
1561
1654
|
m.to_google_dict(
|
1655
|
+
current_model=current_model,
|
1562
1656
|
put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
|
1563
1657
|
native_content=native_content,
|
1564
1658
|
)
|
@@ -1567,6 +1661,53 @@ class Message(BaseMessage):
|
|
1567
1661
|
result = [m for m in result if m is not None]
|
1568
1662
|
return result
|
1569
1663
|
|
1664
|
+
def is_approval_request(self) -> bool:
|
1665
|
+
return self.role == "approval" and self.tool_calls is not None and len(self.tool_calls) > 0
|
1666
|
+
|
1667
|
+
def is_approval_response(self) -> bool:
|
1668
|
+
return self.role == "approval" and self.tool_calls is None and self.approve is not None
|
1669
|
+
|
1670
|
+
def is_summarization_message(self) -> bool:
|
1671
|
+
return (
|
1672
|
+
self.role == "user"
|
1673
|
+
and self.content is not None
|
1674
|
+
and len(self.content) == 1
|
1675
|
+
and isinstance(self.content[0], TextContent)
|
1676
|
+
and "system_alert" in self.content[0].text
|
1677
|
+
)
|
1678
|
+
|
1679
|
+
@staticmethod
|
1680
|
+
def filter_messages_for_llm_api(
|
1681
|
+
messages: List[Message],
|
1682
|
+
) -> List[Message]:
|
1683
|
+
messages = [m for m in messages if m is not None]
|
1684
|
+
if len(messages) == 0:
|
1685
|
+
return []
|
1686
|
+
# Add special handling for legacy bug where summarization triggers in the middle of hitl
|
1687
|
+
messages_to_filter = []
|
1688
|
+
for i in range(len(messages) - 1):
|
1689
|
+
first_message_is_approval = messages[i].is_approval_request()
|
1690
|
+
second_message_is_summary = messages[i + 1].is_summarization_message()
|
1691
|
+
third_message_is_optional_approval = i + 2 >= len(messages) or messages[i + 2].is_approval_response()
|
1692
|
+
if first_message_is_approval and second_message_is_summary and third_message_is_optional_approval:
|
1693
|
+
messages_to_filter.append(messages[i])
|
1694
|
+
for idx in reversed(messages_to_filter): # reverse to avoid index shift
|
1695
|
+
messages.remove(idx)
|
1696
|
+
|
1697
|
+
# Filter last message if it is a lone approval request without a response - this only occurs for token counting
|
1698
|
+
if messages[-1].role == "approval" and messages[-1].tool_calls is not None and len(messages[-1].tool_calls) > 0:
|
1699
|
+
messages.remove(messages[-1])
|
1700
|
+
|
1701
|
+
# Filter last message if it is a lone reasoning message without assistant message or tool call
|
1702
|
+
if (
|
1703
|
+
messages[-1].role == "assistant"
|
1704
|
+
and messages[-1].tool_calls is None
|
1705
|
+
and (not messages[-1].content or all(not isinstance(content_part, TextContent) for content_part in messages[-1].content))
|
1706
|
+
):
|
1707
|
+
messages.remove(messages[-1])
|
1708
|
+
|
1709
|
+
return messages
|
1710
|
+
|
1570
1711
|
@staticmethod
|
1571
1712
|
def generate_otid_from_id(message_id: str, index: int) -> str:
|
1572
1713
|
"""
|
@@ -0,0 +1,21 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
|
3
|
+
from pydantic import Field
|
4
|
+
|
5
|
+
from letta.schemas.letta_base import LettaBase
|
6
|
+
|
7
|
+
|
8
|
+
class RunMetricsBase(LettaBase):
|
9
|
+
__id_prefix__ = "run"
|
10
|
+
|
11
|
+
|
12
|
+
class RunMetrics(RunMetricsBase):
|
13
|
+
id: str = Field(..., description="The id of the run this metric belongs to (matches runs.id).")
|
14
|
+
organization_id: Optional[str] = Field(None, description="The unique identifier of the organization.")
|
15
|
+
agent_id: Optional[str] = Field(None, description="The unique identifier of the agent.")
|
16
|
+
project_id: Optional[str] = Field(None, description="The project that the run belongs to (cloud only).")
|
17
|
+
run_start_ns: Optional[int] = Field(None, description="The timestamp of the start of the run in nanoseconds.")
|
18
|
+
run_ns: Optional[int] = Field(None, description="Total time for the run in nanoseconds.")
|
19
|
+
num_steps: Optional[int] = Field(None, description="The number of steps in the run.")
|
20
|
+
template_id: Optional[str] = Field(None, description="The template ID that the run belongs to (cloud only).")
|
21
|
+
base_template_id: Optional[str] = Field(None, description="The base template ID that the run belongs to (cloud only).")
|
letta/server/db.py
CHANGED
@@ -10,18 +10,11 @@ from sqlalchemy.ext.asyncio import (
|
|
10
10
|
create_async_engine,
|
11
11
|
)
|
12
12
|
|
13
|
+
from letta.database_utils import get_database_uri_for_context
|
13
14
|
from letta.settings import settings
|
14
15
|
|
15
|
-
# Convert PostgreSQL URI to async format
|
16
|
-
|
17
|
-
if pg_uri.startswith("postgresql://"):
|
18
|
-
async_pg_uri = pg_uri.replace("postgresql://", "postgresql+asyncpg://")
|
19
|
-
else:
|
20
|
-
# Handle other URI formats (e.g., postgresql+pg8000://)
|
21
|
-
async_pg_uri = f"postgresql+asyncpg://{pg_uri.split('://', 1)[1]}" if "://" in pg_uri else pg_uri
|
22
|
-
|
23
|
-
# Replace sslmode with ssl for asyncpg
|
24
|
-
async_pg_uri = async_pg_uri.replace("sslmode=", "ssl=")
|
16
|
+
# Convert PostgreSQL URI to async format using common utility
|
17
|
+
async_pg_uri = get_database_uri_for_context(settings.letta_pg_uri, "async")
|
25
18
|
|
26
19
|
# Build engine configuration based on settings
|
27
20
|
engine_args = {
|