letta-nightly 0.6.43.dev20250324104208__py3-none-any.whl → 0.6.44.dev20250325050316__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.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/__init__.py +1 -1
- letta/agent.py +106 -104
- letta/agents/voice_agent.py +1 -1
- letta/client/streaming.py +3 -1
- letta/functions/function_sets/base.py +2 -1
- letta/functions/function_sets/multi_agent.py +51 -40
- letta/functions/helpers.py +26 -22
- letta/helpers/message_helper.py +41 -0
- letta/llm_api/anthropic.py +150 -44
- letta/llm_api/aws_bedrock.py +5 -3
- letta/llm_api/azure_openai.py +0 -1
- letta/llm_api/llm_api_tools.py +4 -0
- letta/orm/organization.py +1 -0
- letta/orm/sqlalchemy_base.py +2 -4
- letta/schemas/agent.py +8 -0
- letta/schemas/letta_message.py +8 -4
- letta/schemas/llm_config.py +6 -0
- letta/schemas/message.py +143 -24
- letta/schemas/openai/chat_completion_response.py +5 -0
- letta/schemas/organization.py +7 -0
- letta/schemas/providers.py +17 -0
- letta/schemas/tool.py +5 -1
- letta/schemas/usage.py +5 -1
- letta/serialize_schemas/pydantic_agent_schema.py +1 -1
- letta/server/rest_api/interface.py +44 -7
- letta/server/rest_api/routers/v1/agents.py +13 -2
- letta/server/rest_api/routers/v1/organizations.py +19 -1
- letta/server/rest_api/utils.py +1 -1
- letta/server/server.py +49 -70
- letta/services/agent_manager.py +6 -2
- letta/services/helpers/agent_manager_helper.py +24 -38
- letta/services/message_manager.py +7 -6
- letta/services/organization_manager.py +13 -0
- letta/services/tool_execution_sandbox.py +5 -1
- letta/services/tool_executor/__init__.py +0 -0
- letta/services/tool_executor/tool_execution_manager.py +74 -0
- letta/services/tool_executor/tool_executor.py +380 -0
- {letta_nightly-0.6.43.dev20250324104208.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/METADATA +2 -3
- {letta_nightly-0.6.43.dev20250324104208.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/RECORD +42 -38
- {letta_nightly-0.6.43.dev20250324104208.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.43.dev20250324104208.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.43.dev20250324104208.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/entry_points.txt +0 -0
letta/schemas/message.py
CHANGED
|
@@ -19,6 +19,7 @@ from letta.schemas.enums import MessageRole
|
|
|
19
19
|
from letta.schemas.letta_base import OrmMetadataBase
|
|
20
20
|
from letta.schemas.letta_message import (
|
|
21
21
|
AssistantMessage,
|
|
22
|
+
HiddenReasoningMessage,
|
|
22
23
|
LettaMessage,
|
|
23
24
|
ReasoningMessage,
|
|
24
25
|
SystemMessage,
|
|
@@ -27,7 +28,13 @@ from letta.schemas.letta_message import (
|
|
|
27
28
|
ToolReturnMessage,
|
|
28
29
|
UserMessage,
|
|
29
30
|
)
|
|
30
|
-
from letta.schemas.letta_message_content import
|
|
31
|
+
from letta.schemas.letta_message_content import (
|
|
32
|
+
LettaMessageContentUnion,
|
|
33
|
+
ReasoningContent,
|
|
34
|
+
RedactedReasoningContent,
|
|
35
|
+
TextContent,
|
|
36
|
+
get_letta_message_content_union_str_json_schema,
|
|
37
|
+
)
|
|
31
38
|
from letta.system import unpack_message
|
|
32
39
|
|
|
33
40
|
|
|
@@ -206,23 +213,58 @@ class Message(BaseMessage):
|
|
|
206
213
|
assistant_message_tool_kwarg: str = DEFAULT_MESSAGE_TOOL_KWARG,
|
|
207
214
|
) -> List[LettaMessage]:
|
|
208
215
|
"""Convert message object (in DB format) to the style used by the original Letta API"""
|
|
209
|
-
if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
|
|
210
|
-
text_content = self.content[0].text
|
|
211
|
-
else:
|
|
212
|
-
text_content = None
|
|
213
|
-
|
|
214
216
|
messages = []
|
|
215
217
|
|
|
216
218
|
if self.role == MessageRole.assistant:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
|
|
220
|
+
# Handle reasoning
|
|
221
|
+
if self.content:
|
|
222
|
+
# Check for ReACT-style COT inside of TextContent
|
|
223
|
+
if len(self.content) == 1 and isinstance(self.content[0], TextContent):
|
|
224
|
+
messages.append(
|
|
225
|
+
ReasoningMessage(
|
|
226
|
+
id=self.id,
|
|
227
|
+
date=self.created_at,
|
|
228
|
+
reasoning=self.content[0].text,
|
|
229
|
+
)
|
|
224
230
|
)
|
|
225
|
-
|
|
231
|
+
# Otherwise, we may have a list of multiple types
|
|
232
|
+
else:
|
|
233
|
+
# TODO we can probably collapse these two cases into a single loop
|
|
234
|
+
for content_part in self.content:
|
|
235
|
+
if isinstance(content_part, TextContent):
|
|
236
|
+
# COT
|
|
237
|
+
messages.append(
|
|
238
|
+
ReasoningMessage(
|
|
239
|
+
id=self.id,
|
|
240
|
+
date=self.created_at,
|
|
241
|
+
reasoning=content_part.text,
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
elif isinstance(content_part, ReasoningContent):
|
|
245
|
+
# "native" COT
|
|
246
|
+
messages.append(
|
|
247
|
+
ReasoningMessage(
|
|
248
|
+
id=self.id,
|
|
249
|
+
date=self.created_at,
|
|
250
|
+
reasoning=content_part.reasoning,
|
|
251
|
+
source="reasoner_model", # TODO do we want to tag like this?
|
|
252
|
+
signature=content_part.signature,
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
elif isinstance(content_part, RedactedReasoningContent):
|
|
256
|
+
# "native" redacted/hidden COT
|
|
257
|
+
messages.append(
|
|
258
|
+
HiddenReasoningMessage(
|
|
259
|
+
id=self.id,
|
|
260
|
+
date=self.created_at,
|
|
261
|
+
state="redacted",
|
|
262
|
+
hidden_reasoning=content_part.data,
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
warnings.warn(f"Unrecognized content part in assistant message: {content_part}")
|
|
267
|
+
|
|
226
268
|
if self.tool_calls is not None:
|
|
227
269
|
# This is type FunctionCall
|
|
228
270
|
for tool_call in self.tool_calls:
|
|
@@ -264,7 +306,11 @@ class Message(BaseMessage):
|
|
|
264
306
|
# "message": response_string,
|
|
265
307
|
# "time": formatted_time,
|
|
266
308
|
# }
|
|
267
|
-
|
|
309
|
+
if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
|
|
310
|
+
text_content = self.content[0].text
|
|
311
|
+
else:
|
|
312
|
+
raise ValueError(f"Invalid tool return (no text object on message): {self.content}")
|
|
313
|
+
|
|
268
314
|
try:
|
|
269
315
|
function_return = json.loads(text_content)
|
|
270
316
|
status = function_return["status"]
|
|
@@ -292,7 +338,11 @@ class Message(BaseMessage):
|
|
|
292
338
|
)
|
|
293
339
|
elif self.role == MessageRole.user:
|
|
294
340
|
# This is type UserMessage
|
|
295
|
-
|
|
341
|
+
if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
|
|
342
|
+
text_content = self.content[0].text
|
|
343
|
+
else:
|
|
344
|
+
raise ValueError(f"Invalid user message (no text object on message): {self.content}")
|
|
345
|
+
|
|
296
346
|
message_str = unpack_message(text_content)
|
|
297
347
|
messages.append(
|
|
298
348
|
UserMessage(
|
|
@@ -303,7 +353,11 @@ class Message(BaseMessage):
|
|
|
303
353
|
)
|
|
304
354
|
elif self.role == MessageRole.system:
|
|
305
355
|
# This is type SystemMessage
|
|
306
|
-
|
|
356
|
+
if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
|
|
357
|
+
text_content = self.content[0].text
|
|
358
|
+
else:
|
|
359
|
+
raise ValueError(f"Invalid system message (no text object on system): {self.content}")
|
|
360
|
+
|
|
307
361
|
messages.append(
|
|
308
362
|
SystemMessage(
|
|
309
363
|
id=self.id,
|
|
@@ -335,6 +389,29 @@ class Message(BaseMessage):
|
|
|
335
389
|
assert "role" in openai_message_dict, openai_message_dict
|
|
336
390
|
assert "content" in openai_message_dict, openai_message_dict
|
|
337
391
|
|
|
392
|
+
# TODO(caren) implicit support for only non-parts/list content types
|
|
393
|
+
if openai_message_dict["content"] is not None and type(openai_message_dict["content"]) is not str:
|
|
394
|
+
raise ValueError(f"Invalid content type: {type(openai_message_dict['content'])}")
|
|
395
|
+
content = [TextContent(text=openai_message_dict["content"])] if openai_message_dict["content"] else []
|
|
396
|
+
|
|
397
|
+
# TODO(caren) bad assumption here that "reasoning_content" always comes before "redacted_reasoning_content"
|
|
398
|
+
if "reasoning_content" in openai_message_dict and openai_message_dict["reasoning_content"]:
|
|
399
|
+
content.append(
|
|
400
|
+
ReasoningContent(
|
|
401
|
+
reasoning=openai_message_dict["reasoning_content"],
|
|
402
|
+
is_native=True,
|
|
403
|
+
signature=(
|
|
404
|
+
openai_message_dict["reasoning_content_signature"] if openai_message_dict["reasoning_content_signature"] else None
|
|
405
|
+
),
|
|
406
|
+
),
|
|
407
|
+
)
|
|
408
|
+
if "redacted_reasoning_content" in openai_message_dict and openai_message_dict["redacted_reasoning_content"]:
|
|
409
|
+
content.append(
|
|
410
|
+
RedactedReasoningContent(
|
|
411
|
+
data=openai_message_dict["redacted_reasoning_content"] if "redacted_reasoning_content" in openai_message_dict else None,
|
|
412
|
+
),
|
|
413
|
+
)
|
|
414
|
+
|
|
338
415
|
# If we're going from deprecated function form
|
|
339
416
|
if openai_message_dict["role"] == "function":
|
|
340
417
|
if not allow_functions_style:
|
|
@@ -348,7 +425,7 @@ class Message(BaseMessage):
|
|
|
348
425
|
model=model,
|
|
349
426
|
# standard fields expected in an OpenAI ChatCompletion message object
|
|
350
427
|
role=MessageRole.tool, # NOTE
|
|
351
|
-
content=
|
|
428
|
+
content=content,
|
|
352
429
|
name=openai_message_dict["name"] if "name" in openai_message_dict else None,
|
|
353
430
|
tool_calls=openai_message_dict["tool_calls"] if "tool_calls" in openai_message_dict else None,
|
|
354
431
|
tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
|
|
@@ -362,7 +439,7 @@ class Message(BaseMessage):
|
|
|
362
439
|
model=model,
|
|
363
440
|
# standard fields expected in an OpenAI ChatCompletion message object
|
|
364
441
|
role=MessageRole.tool, # NOTE
|
|
365
|
-
content=
|
|
442
|
+
content=content,
|
|
366
443
|
name=openai_message_dict["name"] if "name" in openai_message_dict else None,
|
|
367
444
|
tool_calls=openai_message_dict["tool_calls"] if "tool_calls" in openai_message_dict else None,
|
|
368
445
|
tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
|
|
@@ -395,7 +472,7 @@ class Message(BaseMessage):
|
|
|
395
472
|
model=model,
|
|
396
473
|
# standard fields expected in an OpenAI ChatCompletion message object
|
|
397
474
|
role=MessageRole(openai_message_dict["role"]),
|
|
398
|
-
content=
|
|
475
|
+
content=content,
|
|
399
476
|
name=openai_message_dict["name"] if "name" in openai_message_dict else None,
|
|
400
477
|
tool_calls=tool_calls,
|
|
401
478
|
tool_call_id=None, # NOTE: None, since this field is only non-null for role=='tool'
|
|
@@ -409,7 +486,7 @@ class Message(BaseMessage):
|
|
|
409
486
|
model=model,
|
|
410
487
|
# standard fields expected in an OpenAI ChatCompletion message object
|
|
411
488
|
role=MessageRole(openai_message_dict["role"]),
|
|
412
|
-
content=
|
|
489
|
+
content=content,
|
|
413
490
|
name=openai_message_dict["name"] if "name" in openai_message_dict else None,
|
|
414
491
|
tool_calls=tool_calls,
|
|
415
492
|
tool_call_id=None, # NOTE: None, since this field is only non-null for role=='tool'
|
|
@@ -442,7 +519,7 @@ class Message(BaseMessage):
|
|
|
442
519
|
model=model,
|
|
443
520
|
# standard fields expected in an OpenAI ChatCompletion message object
|
|
444
521
|
role=MessageRole(openai_message_dict["role"]),
|
|
445
|
-
content=
|
|
522
|
+
content=content,
|
|
446
523
|
name=openai_message_dict["name"] if "name" in openai_message_dict else None,
|
|
447
524
|
tool_calls=tool_calls,
|
|
448
525
|
tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
|
|
@@ -456,7 +533,7 @@ class Message(BaseMessage):
|
|
|
456
533
|
model=model,
|
|
457
534
|
# standard fields expected in an OpenAI ChatCompletion message object
|
|
458
535
|
role=MessageRole(openai_message_dict["role"]),
|
|
459
|
-
content=
|
|
536
|
+
content=content,
|
|
460
537
|
name=openai_message_dict["name"] if "name" in openai_message_dict else None,
|
|
461
538
|
tool_calls=tool_calls,
|
|
462
539
|
tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
|
|
@@ -477,11 +554,25 @@ class Message(BaseMessage):
|
|
|
477
554
|
"""Go from Message class to ChatCompletion message object"""
|
|
478
555
|
|
|
479
556
|
# TODO change to pydantic casting, eg `return SystemMessageModel(self)`
|
|
557
|
+
# If we only have one content part and it's text, treat it as COT
|
|
558
|
+
parse_content_parts = False
|
|
480
559
|
if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
|
|
481
560
|
text_content = self.content[0].text
|
|
561
|
+
# Otherwise, check if we have TextContent and multiple other parts
|
|
562
|
+
elif self.content and len(self.content) > 1:
|
|
563
|
+
text = [content for content in self.content if isinstance(self.content[0], TextContent)]
|
|
564
|
+
if len(text) > 1:
|
|
565
|
+
assert len(text) == 1, f"multiple text content parts found in a single message: {self.content}"
|
|
566
|
+
text_content = text[0].text
|
|
567
|
+
parse_content_parts = True
|
|
482
568
|
else:
|
|
483
569
|
text_content = None
|
|
484
570
|
|
|
571
|
+
# TODO(caren) we should eventually support multiple content parts here?
|
|
572
|
+
# ie, actually make dict['content'] type list
|
|
573
|
+
# But for now, it's OK until we support multi-modal,
|
|
574
|
+
# since the only "parts" we have are for supporting various COT
|
|
575
|
+
|
|
485
576
|
if self.role == "system":
|
|
486
577
|
assert all([v is not None for v in [self.role]]), vars(self)
|
|
487
578
|
openai_message = {
|
|
@@ -539,6 +630,15 @@ class Message(BaseMessage):
|
|
|
539
630
|
else:
|
|
540
631
|
raise ValueError(self.role)
|
|
541
632
|
|
|
633
|
+
if parse_content_parts:
|
|
634
|
+
for content in self.content:
|
|
635
|
+
if isinstance(content, ReasoningContent):
|
|
636
|
+
openai_message["reasoning_content"] = content.reasoning
|
|
637
|
+
if content.signature:
|
|
638
|
+
openai_message["reasoning_content_signature"] = content.signature
|
|
639
|
+
if isinstance(content, RedactedReasoningContent):
|
|
640
|
+
openai_message["redacted_reasoning_content"] = content.data
|
|
641
|
+
|
|
542
642
|
return openai_message
|
|
543
643
|
|
|
544
644
|
def to_anthropic_dict(
|
|
@@ -552,6 +652,8 @@ class Message(BaseMessage):
|
|
|
552
652
|
Args:
|
|
553
653
|
inner_thoughts_xml_tag (str): The XML tag to wrap around inner thoughts
|
|
554
654
|
"""
|
|
655
|
+
|
|
656
|
+
# Check for COT
|
|
555
657
|
if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
|
|
556
658
|
text_content = self.content[0].text
|
|
557
659
|
else:
|
|
@@ -587,7 +689,24 @@ class Message(BaseMessage):
|
|
|
587
689
|
}
|
|
588
690
|
content = []
|
|
589
691
|
# COT / reasoning / thinking
|
|
590
|
-
if
|
|
692
|
+
if len(self.content) > 1:
|
|
693
|
+
for content_part in self.content:
|
|
694
|
+
if isinstance(content_part, ReasoningContent):
|
|
695
|
+
content.append(
|
|
696
|
+
{
|
|
697
|
+
"type": "thinking",
|
|
698
|
+
"thinking": content_part.reasoning,
|
|
699
|
+
"signature": content_part.signature,
|
|
700
|
+
}
|
|
701
|
+
)
|
|
702
|
+
if isinstance(content_part, RedactedReasoningContent):
|
|
703
|
+
content.append(
|
|
704
|
+
{
|
|
705
|
+
"type": "redacted_thinking",
|
|
706
|
+
"data": content_part.data,
|
|
707
|
+
}
|
|
708
|
+
)
|
|
709
|
+
elif text_content is not None:
|
|
591
710
|
content.append(
|
|
592
711
|
{
|
|
593
712
|
"type": "text",
|
|
@@ -40,6 +40,8 @@ class Message(BaseModel):
|
|
|
40
40
|
role: str
|
|
41
41
|
function_call: Optional[FunctionCall] = None # Deprecated
|
|
42
42
|
reasoning_content: Optional[str] = None # Used in newer reasoning APIs
|
|
43
|
+
reasoning_content_signature: Optional[str] = None # NOTE: for Anthropic
|
|
44
|
+
redacted_reasoning_content: Optional[str] = None # NOTE: for Anthropic
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
class Choice(BaseModel):
|
|
@@ -117,6 +119,8 @@ class MessageDelta(BaseModel):
|
|
|
117
119
|
|
|
118
120
|
content: Optional[str] = None
|
|
119
121
|
reasoning_content: Optional[str] = None
|
|
122
|
+
reasoning_content_signature: Optional[str] = None # NOTE: for Anthropic
|
|
123
|
+
redacted_reasoning_content: Optional[str] = None # NOTE: for Anthropic
|
|
120
124
|
tool_calls: Optional[List[ToolCallDelta]] = None
|
|
121
125
|
role: Optional[str] = None
|
|
122
126
|
function_call: Optional[FunctionCallDelta] = None # Deprecated
|
|
@@ -140,3 +144,4 @@ class ChatCompletionChunkResponse(BaseModel):
|
|
|
140
144
|
system_fingerprint: Optional[str] = None
|
|
141
145
|
# object: str = Field(default="chat.completion")
|
|
142
146
|
object: Literal["chat.completion.chunk"] = "chat.completion.chunk"
|
|
147
|
+
output_tokens: int = 0
|
letta/schemas/organization.py
CHANGED
|
@@ -16,7 +16,14 @@ class Organization(OrganizationBase):
|
|
|
16
16
|
id: str = OrganizationBase.generate_id_field()
|
|
17
17
|
name: str = Field(create_random_username(), description="The name of the organization.", json_schema_extra={"default": "SincereYogurt"})
|
|
18
18
|
created_at: Optional[datetime] = Field(default_factory=get_utc_time, description="The creation date of the organization.")
|
|
19
|
+
privileged_tools: bool = Field(False, description="Whether the organization has access to privileged tools.")
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class OrganizationCreate(OrganizationBase):
|
|
22
23
|
name: Optional[str] = Field(None, description="The name of the organization.")
|
|
24
|
+
privileged_tools: Optional[bool] = Field(False, description="Whether the organization has access to privileged tools.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OrganizationUpdate(OrganizationBase):
|
|
28
|
+
name: Optional[str] = Field(None, description="The name of the organization.")
|
|
29
|
+
privileged_tools: Optional[bool] = Field(False, description="Whether the organization has access to privileged tools.")
|
letta/schemas/providers.py
CHANGED
|
@@ -157,6 +157,23 @@ class OpenAIProvider(Provider):
|
|
|
157
157
|
# if "config" in data and "chat_template" in data["config"] and "tools" not in data["config"]["chat_template"]:
|
|
158
158
|
# continue
|
|
159
159
|
|
|
160
|
+
# for openai, filter models
|
|
161
|
+
if self.base_url == "https://api.openai.com/v1":
|
|
162
|
+
allowed_types = ["gpt-4", "o1", "o3"]
|
|
163
|
+
disallowed_types = ["transcribe", "search", "realtime", "tts", "audio", "computer"]
|
|
164
|
+
skip = True
|
|
165
|
+
for model_type in allowed_types:
|
|
166
|
+
if model_name.startswith(model_type):
|
|
167
|
+
skip = False
|
|
168
|
+
break
|
|
169
|
+
for keyword in disallowed_types:
|
|
170
|
+
if keyword in model_name:
|
|
171
|
+
skip = True
|
|
172
|
+
break
|
|
173
|
+
# ignore this model
|
|
174
|
+
if skip:
|
|
175
|
+
continue
|
|
176
|
+
|
|
160
177
|
configs.append(
|
|
161
178
|
LLMConfig(
|
|
162
179
|
model=model_name,
|
letta/schemas/tool.py
CHANGED
|
@@ -93,7 +93,11 @@ class Tool(BaseTool):
|
|
|
93
93
|
description=description,
|
|
94
94
|
)
|
|
95
95
|
else:
|
|
96
|
-
|
|
96
|
+
try:
|
|
97
|
+
self.json_schema = derive_openai_json_schema(source_code=self.source_code)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
error_msg = f"Failed to derive json schema for tool with id={self.id} name={self.name}. Error: {str(e)}"
|
|
100
|
+
logger.error(error_msg)
|
|
97
101
|
elif self.tool_type in {ToolType.LETTA_CORE, ToolType.LETTA_MEMORY_CORE}:
|
|
98
102
|
# If it's letta core tool, we generate the json_schema on the fly here
|
|
99
103
|
self.json_schema = get_json_schema_from_module(module_name=LETTA_CORE_TOOL_MODULE_NAME, function_name=self.name)
|
letta/schemas/usage.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
from typing import Literal
|
|
1
|
+
from typing import List, Literal, Optional
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
|
+
from letta.schemas.message import Message
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
class LettaUsageStatistics(BaseModel):
|
|
7
9
|
"""
|
|
@@ -19,3 +21,5 @@ class LettaUsageStatistics(BaseModel):
|
|
|
19
21
|
prompt_tokens: int = Field(0, description="The number of tokens in the prompt.")
|
|
20
22
|
total_tokens: int = Field(0, description="The total number of tokens processed by the agent.")
|
|
21
23
|
step_count: int = Field(0, description="The number of steps taken by the agent.")
|
|
24
|
+
# TODO: Optional for now. This field makes everyone's lives easier
|
|
25
|
+
steps_messages: Optional[List[List[Message]]] = Field(None, description="The messages generated per step")
|
|
@@ -13,6 +13,7 @@ from letta.local_llm.constants import INNER_THOUGHTS_KWARG
|
|
|
13
13
|
from letta.schemas.enums import MessageStreamStatus
|
|
14
14
|
from letta.schemas.letta_message import (
|
|
15
15
|
AssistantMessage,
|
|
16
|
+
HiddenReasoningMessage,
|
|
16
17
|
LegacyFunctionCallMessage,
|
|
17
18
|
LegacyLettaMessage,
|
|
18
19
|
LettaMessage,
|
|
@@ -22,6 +23,7 @@ from letta.schemas.letta_message import (
|
|
|
22
23
|
ToolCallMessage,
|
|
23
24
|
ToolReturnMessage,
|
|
24
25
|
)
|
|
26
|
+
from letta.schemas.letta_message_content import ReasoningContent, RedactedReasoningContent, TextContent
|
|
25
27
|
from letta.schemas.message import Message
|
|
26
28
|
from letta.schemas.openai.chat_completion_response import ChatCompletionChunkResponse
|
|
27
29
|
from letta.server.rest_api.optimistic_json_parser import OptimisticJSONParser
|
|
@@ -478,7 +480,7 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
|
478
480
|
|
|
479
481
|
if (
|
|
480
482
|
message_delta.content is None
|
|
481
|
-
and (expect_reasoning_content and message_delta.reasoning_content is None)
|
|
483
|
+
and (expect_reasoning_content and message_delta.reasoning_content is None and message_delta.redacted_reasoning_content is None)
|
|
482
484
|
and message_delta.tool_calls is None
|
|
483
485
|
and message_delta.function_call is None
|
|
484
486
|
and choice.finish_reason is None
|
|
@@ -493,6 +495,15 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
|
493
495
|
id=message_id,
|
|
494
496
|
date=message_date,
|
|
495
497
|
reasoning=message_delta.reasoning_content,
|
|
498
|
+
signature=message_delta.reasoning_content_signature,
|
|
499
|
+
source="reasoner_model" if message_delta.reasoning_content_signature else "non_reasoner_model",
|
|
500
|
+
)
|
|
501
|
+
elif expect_reasoning_content and message_delta.redacted_reasoning_content is not None:
|
|
502
|
+
processed_chunk = HiddenReasoningMessage(
|
|
503
|
+
id=message_id,
|
|
504
|
+
date=message_date,
|
|
505
|
+
hidden_reasoning=message_delta.redacted_reasoning_content,
|
|
506
|
+
state="redacted",
|
|
496
507
|
)
|
|
497
508
|
elif expect_reasoning_content and message_delta.content is not None:
|
|
498
509
|
# "ignore" content if we expect reasoning content
|
|
@@ -1071,13 +1082,39 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
|
1071
1082
|
# "id": str(msg_obj.id) if msg_obj is not None else None,
|
|
1072
1083
|
# }
|
|
1073
1084
|
assert msg_obj is not None, "Internal monologue requires msg_obj references for metadata"
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1085
|
+
if msg_obj.content and len(msg_obj.content) == 1 and isinstance(msg_obj.content[0], TextContent):
|
|
1086
|
+
processed_chunk = ReasoningMessage(
|
|
1087
|
+
id=msg_obj.id,
|
|
1088
|
+
date=msg_obj.created_at,
|
|
1089
|
+
reasoning=msg,
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
self._push_to_buffer(processed_chunk)
|
|
1093
|
+
else:
|
|
1094
|
+
for content in msg_obj.content:
|
|
1095
|
+
if isinstance(content, TextContent):
|
|
1096
|
+
processed_chunk = ReasoningMessage(
|
|
1097
|
+
id=msg_obj.id,
|
|
1098
|
+
date=msg_obj.created_at,
|
|
1099
|
+
reasoning=content.text,
|
|
1100
|
+
)
|
|
1101
|
+
elif isinstance(content, ReasoningContent):
|
|
1102
|
+
processed_chunk = ReasoningMessage(
|
|
1103
|
+
id=msg_obj.id,
|
|
1104
|
+
date=msg_obj.created_at,
|
|
1105
|
+
source="reasoner_model",
|
|
1106
|
+
reasoning=content.reasoning,
|
|
1107
|
+
signature=content.signature,
|
|
1108
|
+
)
|
|
1109
|
+
elif isinstance(content, RedactedReasoningContent):
|
|
1110
|
+
processed_chunk = HiddenReasoningMessage(
|
|
1111
|
+
id=msg_obj.id,
|
|
1112
|
+
date=msg_obj.created_at,
|
|
1113
|
+
state="redacted",
|
|
1114
|
+
hidden_reasoning=content.data,
|
|
1115
|
+
)
|
|
1079
1116
|
|
|
1080
|
-
|
|
1117
|
+
self._push_to_buffer(processed_chunk)
|
|
1081
1118
|
|
|
1082
1119
|
return
|
|
1083
1120
|
|
|
@@ -63,6 +63,10 @@ def list_agents(
|
|
|
63
63
|
"Using this can optimize performance by reducing unnecessary joins."
|
|
64
64
|
),
|
|
65
65
|
),
|
|
66
|
+
ascending: bool = Query(
|
|
67
|
+
False,
|
|
68
|
+
description="Whether to sort agents oldest to newest (True) or newest to oldest (False, default)",
|
|
69
|
+
),
|
|
66
70
|
):
|
|
67
71
|
"""
|
|
68
72
|
List all agents associated with a given user.
|
|
@@ -90,6 +94,7 @@ def list_agents(
|
|
|
90
94
|
identity_id=identity_id,
|
|
91
95
|
identifier_keys=identifier_keys,
|
|
92
96
|
include_relationships=include_relationships,
|
|
97
|
+
ascending=ascending,
|
|
93
98
|
)
|
|
94
99
|
|
|
95
100
|
|
|
@@ -432,9 +437,13 @@ def detach_block(
|
|
|
432
437
|
def list_passages(
|
|
433
438
|
agent_id: str,
|
|
434
439
|
server: "SyncServer" = Depends(get_letta_server),
|
|
435
|
-
after: Optional[
|
|
436
|
-
before: Optional[
|
|
440
|
+
after: Optional[str] = Query(None, description="Unique ID of the memory to start the query range at."),
|
|
441
|
+
before: Optional[str] = Query(None, description="Unique ID of the memory to end the query range at."),
|
|
437
442
|
limit: Optional[int] = Query(None, description="How many results to include in the response."),
|
|
443
|
+
search: Optional[str] = Query(None, description="Search passages by text"),
|
|
444
|
+
ascending: Optional[bool] = Query(
|
|
445
|
+
True, description="Whether to sort passages oldest to newest (True, default) or newest to oldest (False)"
|
|
446
|
+
),
|
|
438
447
|
actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
|
439
448
|
):
|
|
440
449
|
"""
|
|
@@ -447,7 +456,9 @@ def list_passages(
|
|
|
447
456
|
agent_id=agent_id,
|
|
448
457
|
after=after,
|
|
449
458
|
before=before,
|
|
459
|
+
query_text=search,
|
|
450
460
|
limit=limit,
|
|
461
|
+
ascending=ascending,
|
|
451
462
|
)
|
|
452
463
|
|
|
453
464
|
|
|
@@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, List, Optional
|
|
|
2
2
|
|
|
3
3
|
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
|
4
4
|
|
|
5
|
-
from letta.schemas.organization import Organization, OrganizationCreate
|
|
5
|
+
from letta.schemas.organization import Organization, OrganizationCreate, OrganizationUpdate
|
|
6
6
|
from letta.server.rest_api.utils import get_letta_server
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
@@ -59,3 +59,21 @@ def delete_org(
|
|
|
59
59
|
except Exception as e:
|
|
60
60
|
raise HTTPException(status_code=500, detail=f"{e}")
|
|
61
61
|
return org
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@router.patch("/", tags=["admin"], response_model=Organization, operation_id="update_organization")
|
|
65
|
+
def update_org(
|
|
66
|
+
org_id: str = Query(..., description="The org_id key to be updated."),
|
|
67
|
+
request: OrganizationUpdate = Body(...),
|
|
68
|
+
server: "SyncServer" = Depends(get_letta_server),
|
|
69
|
+
):
|
|
70
|
+
try:
|
|
71
|
+
org = server.organization_manager.get_organization_by_id(org_id=org_id)
|
|
72
|
+
if org is None:
|
|
73
|
+
raise HTTPException(status_code=404, detail=f"Organization does not exist")
|
|
74
|
+
org = server.organization_manager.update_organization(org_id=org_id, name=request.name)
|
|
75
|
+
except HTTPException:
|
|
76
|
+
raise
|
|
77
|
+
except Exception as e:
|
|
78
|
+
raise HTTPException(status_code=500, detail=f"{e}")
|
|
79
|
+
return org
|
letta/server/rest_api/utils.py
CHANGED
|
@@ -80,7 +80,7 @@ async def sse_async_generator(
|
|
|
80
80
|
err_msg = f"Expected LettaUsageStatistics, got {type(usage)}"
|
|
81
81
|
logger.error(err_msg)
|
|
82
82
|
raise ValueError(err_msg)
|
|
83
|
-
yield sse_formatter(usage.model_dump())
|
|
83
|
+
yield sse_formatter(usage.model_dump(exclude={"steps_messages"}))
|
|
84
84
|
|
|
85
85
|
except ContextWindowExceededError as e:
|
|
86
86
|
log_error_to_sentry(e)
|