letta-nightly 0.6.50.dev20250411104155__py3-none-any.whl → 0.6.52.dev20250412051016__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/agent.py +23 -32
- letta/agents/base_agent.py +17 -6
- letta/agents/ephemeral_agent.py +5 -6
- letta/agents/ephemeral_memory_agent.py +8 -10
- letta/agents/helpers.py +6 -6
- letta/agents/letta_agent.py +9 -10
- letta/agents/letta_agent_batch.py +164 -0
- letta/agents/voice_agent.py +8 -8
- letta/functions/function_sets/base.py +1 -1
- letta/helpers/converters.py +5 -2
- letta/helpers/tool_rule_solver.py +12 -2
- letta/jobs/scheduler.py +13 -11
- letta/llm_api/anthropic.py +0 -1
- letta/llm_api/anthropic_client.py +61 -23
- letta/llm_api/cohere.py +1 -1
- letta/llm_api/google_ai_client.py +48 -13
- letta/llm_api/google_vertex_client.py +19 -1
- letta/llm_api/llm_client_base.py +13 -5
- letta/llm_api/openai.py +4 -3
- letta/llm_api/openai_client.py +18 -10
- letta/orm/organization.py +4 -2
- letta/orm/sqlalchemy_base.py +3 -0
- letta/schemas/enums.py +1 -0
- letta/schemas/group.py +30 -1
- letta/schemas/identity.py +10 -0
- letta/schemas/letta_request.py +4 -0
- letta/schemas/letta_response.py +9 -1
- letta/schemas/llm_config.py +10 -0
- letta/schemas/message.py +21 -12
- letta/schemas/openai/chat_completion_request.py +1 -0
- letta/schemas/tool_rule.py +14 -1
- letta/server/rest_api/interface.py +5 -4
- letta/server/rest_api/routers/v1/agents.py +20 -13
- letta/server/rest_api/routers/v1/groups.py +1 -1
- letta/server/rest_api/routers/v1/identities.py +23 -2
- letta/server/rest_api/utils.py +20 -22
- letta/server/server.py +34 -21
- letta/services/agent_manager.py +13 -9
- letta/services/block_manager.py +2 -4
- letta/services/identity_manager.py +21 -5
- letta/services/llm_batch_manager.py +21 -1
- letta/services/summarizer/summarizer.py +11 -4
- letta/services/tool_manager.py +1 -1
- letta/settings.py +1 -0
- letta/utils.py +2 -2
- {letta_nightly-0.6.50.dev20250411104155.dist-info → letta_nightly-0.6.52.dev20250412051016.dist-info}/METADATA +3 -3
- {letta_nightly-0.6.50.dev20250411104155.dist-info → letta_nightly-0.6.52.dev20250412051016.dist-info}/RECORD +51 -50
- {letta_nightly-0.6.50.dev20250411104155.dist-info → letta_nightly-0.6.52.dev20250412051016.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.50.dev20250411104155.dist-info → letta_nightly-0.6.52.dev20250412051016.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.50.dev20250411104155.dist-info → letta_nightly-0.6.52.dev20250412051016.dist-info}/entry_points.txt +0 -0
letta/llm_api/openai_client.py
CHANGED
@@ -21,6 +21,7 @@ from letta.llm_api.helpers import add_inner_thoughts_to_functions, convert_to_st
|
|
21
21
|
from letta.llm_api.llm_client_base import LLMClientBase
|
22
22
|
from letta.local_llm.constants import INNER_THOUGHTS_KWARG, INNER_THOUGHTS_KWARG_DESCRIPTION, INNER_THOUGHTS_KWARG_DESCRIPTION_GO_FIRST
|
23
23
|
from letta.log import get_logger
|
24
|
+
from letta.schemas.llm_config import LLMConfig
|
24
25
|
from letta.schemas.message import Message as PydanticMessage
|
25
26
|
from letta.schemas.openai.chat_completion_request import ChatCompletionRequest
|
26
27
|
from letta.schemas.openai.chat_completion_request import FunctionCall as ToolFunctionChoiceFunctionCall
|
@@ -45,17 +46,18 @@ class OpenAIClient(LLMClientBase):
|
|
45
46
|
def build_request_data(
|
46
47
|
self,
|
47
48
|
messages: List[PydanticMessage],
|
49
|
+
llm_config: LLMConfig,
|
48
50
|
tools: Optional[List[dict]] = None, # Keep as dict for now as per base class
|
49
51
|
force_tool_call: Optional[str] = None,
|
50
52
|
) -> dict:
|
51
53
|
"""
|
52
54
|
Constructs a request object in the expected data format for the OpenAI API.
|
53
55
|
"""
|
54
|
-
if tools and
|
56
|
+
if tools and llm_config.put_inner_thoughts_in_kwargs:
|
55
57
|
# Special case for LM Studio backend since it needs extra guidance to force out the thoughts first
|
56
58
|
# TODO(fix)
|
57
59
|
inner_thoughts_desc = (
|
58
|
-
INNER_THOUGHTS_KWARG_DESCRIPTION_GO_FIRST if ":1234" in
|
60
|
+
INNER_THOUGHTS_KWARG_DESCRIPTION_GO_FIRST if ":1234" in llm_config.model_endpoint else INNER_THOUGHTS_KWARG_DESCRIPTION
|
59
61
|
)
|
60
62
|
tools = add_inner_thoughts_to_functions(
|
61
63
|
functions=tools,
|
@@ -64,22 +66,28 @@ class OpenAIClient(LLMClientBase):
|
|
64
66
|
put_inner_thoughts_first=True,
|
65
67
|
)
|
66
68
|
|
69
|
+
use_developer_message = llm_config.model.startswith("o1") or llm_config.model.startswith("o3") # o-series models
|
67
70
|
openai_message_list = [
|
68
|
-
cast_message_to_subtype(
|
71
|
+
cast_message_to_subtype(
|
72
|
+
m.to_openai_dict(
|
73
|
+
put_inner_thoughts_in_kwargs=llm_config.put_inner_thoughts_in_kwargs,
|
74
|
+
use_developer_message=use_developer_message,
|
75
|
+
)
|
76
|
+
)
|
69
77
|
for m in messages
|
70
78
|
]
|
71
79
|
|
72
|
-
if
|
73
|
-
model =
|
80
|
+
if llm_config.model:
|
81
|
+
model = llm_config.model
|
74
82
|
else:
|
75
|
-
logger.warning(f"Model type not set in llm_config: {
|
83
|
+
logger.warning(f"Model type not set in llm_config: {llm_config.model_dump_json(indent=4)}")
|
76
84
|
model = None
|
77
85
|
|
78
86
|
# force function calling for reliability, see https://platform.openai.com/docs/api-reference/chat/create#chat-create-tool_choice
|
79
87
|
# TODO(matt) move into LLMConfig
|
80
88
|
# TODO: This vllm checking is very brittle and is a patch at most
|
81
89
|
tool_choice = None
|
82
|
-
if
|
90
|
+
if llm_config.model_endpoint == "https://inference.memgpt.ai" or (llm_config.handle and "vllm" in self.llm_config.handle):
|
83
91
|
tool_choice = "auto" # TODO change to "required" once proxy supports it
|
84
92
|
elif tools:
|
85
93
|
# only set if tools is non-Null
|
@@ -94,11 +102,11 @@ class OpenAIClient(LLMClientBase):
|
|
94
102
|
tools=[OpenAITool(type="function", function=f) for f in tools] if tools else None,
|
95
103
|
tool_choice=tool_choice,
|
96
104
|
user=str(),
|
97
|
-
max_completion_tokens=
|
98
|
-
temperature=
|
105
|
+
max_completion_tokens=llm_config.max_tokens,
|
106
|
+
temperature=llm_config.temperature,
|
99
107
|
)
|
100
108
|
|
101
|
-
if "inference.memgpt.ai" in
|
109
|
+
if "inference.memgpt.ai" in llm_config.model_endpoint:
|
102
110
|
# override user id for inference.memgpt.ai
|
103
111
|
import uuid
|
104
112
|
|
letta/orm/organization.py
CHANGED
@@ -51,8 +51,10 @@ class Organization(SqlalchemyBase):
|
|
51
51
|
providers: Mapped[List["Provider"]] = relationship("Provider", back_populates="organization", cascade="all, delete-orphan")
|
52
52
|
identities: Mapped[List["Identity"]] = relationship("Identity", back_populates="organization", cascade="all, delete-orphan")
|
53
53
|
groups: Mapped[List["Group"]] = relationship("Group", back_populates="organization", cascade="all, delete-orphan")
|
54
|
-
llm_batch_jobs: Mapped[List["
|
55
|
-
llm_batch_items: Mapped[List["
|
54
|
+
llm_batch_jobs: Mapped[List["LLMBatchJob"]] = relationship("LLMBatchJob", back_populates="organization", cascade="all, delete-orphan")
|
55
|
+
llm_batch_items: Mapped[List["LLMBatchItem"]] = relationship(
|
56
|
+
"LLMBatchItem", back_populates="organization", cascade="all, delete-orphan"
|
57
|
+
)
|
56
58
|
|
57
59
|
@property
|
58
60
|
def passages(self) -> List[Union["SourcePassage", "AgentPassage"]]:
|
letta/orm/sqlalchemy_base.py
CHANGED
@@ -334,6 +334,9 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
334
334
|
if len(identifiers) > 0:
|
335
335
|
query = query.where(cls.id.in_(identifiers))
|
336
336
|
query_conditions.append(f"id='{identifiers}'")
|
337
|
+
elif not kwargs:
|
338
|
+
logger.debug(f"No identifiers provided for {cls.__name__}, returning empty list")
|
339
|
+
return []
|
337
340
|
|
338
341
|
if kwargs:
|
339
342
|
query = query.filter_by(**kwargs)
|
letta/schemas/enums.py
CHANGED
letta/schemas/group.py
CHANGED
@@ -42,11 +42,21 @@ class RoundRobinManager(ManagerConfig):
|
|
42
42
|
max_turns: Optional[int] = Field(None, description="")
|
43
43
|
|
44
44
|
|
45
|
+
class RoundRobinManagerUpdate(ManagerConfig):
|
46
|
+
manager_type: Literal[ManagerType.round_robin] = Field(ManagerType.round_robin, description="")
|
47
|
+
max_turns: Optional[int] = Field(None, description="")
|
48
|
+
|
49
|
+
|
45
50
|
class SupervisorManager(ManagerConfig):
|
46
51
|
manager_type: Literal[ManagerType.supervisor] = Field(ManagerType.supervisor, description="")
|
47
52
|
manager_agent_id: str = Field(..., description="")
|
48
53
|
|
49
54
|
|
55
|
+
class SupervisorManagerUpdate(ManagerConfig):
|
56
|
+
manager_type: Literal[ManagerType.supervisor] = Field(ManagerType.supervisor, description="")
|
57
|
+
manager_agent_id: Optional[str] = Field(..., description="")
|
58
|
+
|
59
|
+
|
50
60
|
class DynamicManager(ManagerConfig):
|
51
61
|
manager_type: Literal[ManagerType.dynamic] = Field(ManagerType.dynamic, description="")
|
52
62
|
manager_agent_id: str = Field(..., description="")
|
@@ -54,12 +64,25 @@ class DynamicManager(ManagerConfig):
|
|
54
64
|
max_turns: Optional[int] = Field(None, description="")
|
55
65
|
|
56
66
|
|
67
|
+
class DynamicManagerUpdate(ManagerConfig):
|
68
|
+
manager_type: Literal[ManagerType.dynamic] = Field(ManagerType.dynamic, description="")
|
69
|
+
manager_agent_id: Optional[str] = Field(None, description="")
|
70
|
+
termination_token: Optional[str] = Field(None, description="")
|
71
|
+
max_turns: Optional[int] = Field(None, description="")
|
72
|
+
|
73
|
+
|
57
74
|
class SleeptimeManager(ManagerConfig):
|
58
75
|
manager_type: Literal[ManagerType.sleeptime] = Field(ManagerType.sleeptime, description="")
|
59
76
|
manager_agent_id: str = Field(..., description="")
|
60
77
|
sleeptime_agent_frequency: Optional[int] = Field(None, description="")
|
61
78
|
|
62
79
|
|
80
|
+
class SleeptimeManagerUpdate(ManagerConfig):
|
81
|
+
manager_type: Literal[ManagerType.sleeptime] = Field(ManagerType.sleeptime, description="")
|
82
|
+
manager_agent_id: Optional[str] = Field(None, description="")
|
83
|
+
sleeptime_agent_frequency: Optional[int] = Field(None, description="")
|
84
|
+
|
85
|
+
|
63
86
|
# class SwarmGroup(ManagerConfig):
|
64
87
|
# manager_type: Literal[ManagerType.swarm] = Field(ManagerType.swarm, description="")
|
65
88
|
|
@@ -70,6 +93,12 @@ ManagerConfigUnion = Annotated[
|
|
70
93
|
]
|
71
94
|
|
72
95
|
|
96
|
+
ManagerConfigUpdateUnion = Annotated[
|
97
|
+
Union[RoundRobinManagerUpdate, SupervisorManagerUpdate, DynamicManagerUpdate, SleeptimeManagerUpdate],
|
98
|
+
Field(discriminator="manager_type"),
|
99
|
+
]
|
100
|
+
|
101
|
+
|
73
102
|
class GroupCreate(BaseModel):
|
74
103
|
agent_ids: List[str] = Field(..., description="")
|
75
104
|
description: str = Field(..., description="")
|
@@ -80,5 +109,5 @@ class GroupCreate(BaseModel):
|
|
80
109
|
class GroupUpdate(BaseModel):
|
81
110
|
agent_ids: Optional[List[str]] = Field(None, description="")
|
82
111
|
description: Optional[str] = Field(None, description="")
|
83
|
-
manager_config: Optional[
|
112
|
+
manager_config: Optional[ManagerConfigUpdateUnion] = Field(None, description="")
|
84
113
|
shared_block_ids: Optional[List[str]] = Field(None, description="")
|
letta/schemas/identity.py
CHANGED
@@ -61,6 +61,16 @@ class IdentityCreate(LettaBase):
|
|
61
61
|
properties: Optional[List[IdentityProperty]] = Field(None, description="List of properties associated with the identity.")
|
62
62
|
|
63
63
|
|
64
|
+
class IdentityUpsert(LettaBase):
|
65
|
+
identifier_key: str = Field(..., description="External, user-generated identifier key of the identity.")
|
66
|
+
name: str = Field(..., description="The name of the identity.")
|
67
|
+
identity_type: IdentityType = Field(..., description="The type of the identity.")
|
68
|
+
project_id: Optional[str] = Field(None, description="The project id of the identity, if applicable.")
|
69
|
+
agent_ids: Optional[List[str]] = Field(None, description="The agent ids that are associated with the identity.")
|
70
|
+
block_ids: Optional[List[str]] = Field(None, description="The IDs of the blocks associated with the identity.")
|
71
|
+
properties: Optional[List[IdentityProperty]] = Field(None, description="List of properties associated with the identity.")
|
72
|
+
|
73
|
+
|
64
74
|
class IdentityUpdate(LettaBase):
|
65
75
|
identifier_key: Optional[str] = Field(None, description="External, user-generated identifier key of the identity.")
|
66
76
|
name: Optional[str] = Field(None, description="The name of the identity.")
|
letta/schemas/letta_request.py
CHANGED
@@ -27,3 +27,7 @@ class LettaStreamingRequest(LettaRequest):
|
|
27
27
|
default=False,
|
28
28
|
description="Flag to determine if individual tokens should be streamed. Set to True for token streaming (requires stream_steps = True).",
|
29
29
|
)
|
30
|
+
|
31
|
+
|
32
|
+
class LettaBatchRequest(LettaRequest):
|
33
|
+
agent_id: str = Field(..., description="The ID of the agent to send this batch request for")
|
letta/schemas/letta_response.py
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
import html
|
2
2
|
import json
|
3
3
|
import re
|
4
|
+
from datetime import datetime
|
4
5
|
from typing import List, Union
|
5
6
|
|
6
7
|
from pydantic import BaseModel, Field
|
7
8
|
|
8
9
|
from letta.helpers.json_helpers import json_dumps
|
9
|
-
from letta.schemas.enums import MessageStreamStatus
|
10
|
+
from letta.schemas.enums import JobStatus, MessageStreamStatus
|
10
11
|
from letta.schemas.letta_message import LettaMessage, LettaMessageUnion
|
11
12
|
from letta.schemas.usage import LettaUsageStatistics
|
12
13
|
|
@@ -165,3 +166,10 @@ class LettaResponse(BaseModel):
|
|
165
166
|
|
166
167
|
# The streaming response is either [DONE], [DONE_STEP], [DONE], an error, or a LettaMessage
|
167
168
|
LettaStreamingResponse = Union[LettaMessage, MessageStreamStatus, LettaUsageStatistics]
|
169
|
+
|
170
|
+
|
171
|
+
class LettaBatchResponse(BaseModel):
|
172
|
+
batch_id: str = Field(..., description="A unique identifier for this batch request.")
|
173
|
+
status: JobStatus = Field(..., description="The current status of the batch request.")
|
174
|
+
last_polled_at: datetime = Field(..., description="The timestamp when the batch was last polled for updates.")
|
175
|
+
created_at: datetime = Field(..., description="The timestamp when the batch request was created.")
|
letta/schemas/llm_config.py
CHANGED
@@ -74,6 +74,13 @@ class LLMConfig(BaseModel):
|
|
74
74
|
# FIXME hack to silence pydantic protected namespace warning
|
75
75
|
model_config = ConfigDict(protected_namespaces=())
|
76
76
|
|
77
|
+
@model_validator(mode="before")
|
78
|
+
@classmethod
|
79
|
+
def set_default_enable_reasoner(cls, values):
|
80
|
+
if any(openai_reasoner_model in values.get("model", "") for openai_reasoner_model in ["o3-mini", "o1"]):
|
81
|
+
values["enable_reasoner"] = True
|
82
|
+
return values
|
83
|
+
|
77
84
|
@model_validator(mode="before")
|
78
85
|
@classmethod
|
79
86
|
def set_default_put_inner_thoughts(cls, values):
|
@@ -100,6 +107,9 @@ class LLMConfig(BaseModel):
|
|
100
107
|
logger.warning("max_tokens must be greater than max_reasoning_tokens (thinking budget)")
|
101
108
|
if self.put_inner_thoughts_in_kwargs:
|
102
109
|
logger.warning("Extended thinking is not compatible with put_inner_thoughts_in_kwargs")
|
110
|
+
elif self.max_reasoning_tokens and not self.enable_reasoner:
|
111
|
+
logger.warning("model will not use reasoning unless enable_reasoner is set to True")
|
112
|
+
|
103
113
|
return self
|
104
114
|
|
105
115
|
@classmethod
|
letta/schemas/message.py
CHANGED
@@ -137,19 +137,26 @@ class Message(BaseMessage):
|
|
137
137
|
"""
|
138
138
|
|
139
139
|
id: str = BaseMessage.generate_id_field()
|
140
|
-
role: MessageRole = Field(..., description="The role of the participant.")
|
141
|
-
content: Optional[List[LettaMessageContentUnion]] = Field(None, description="The content of the message.")
|
142
140
|
organization_id: Optional[str] = Field(None, description="The unique identifier of the organization.")
|
143
141
|
agent_id: Optional[str] = Field(None, description="The unique identifier of the agent.")
|
144
142
|
model: Optional[str] = Field(None, description="The model used to make the function call.")
|
145
|
-
|
146
|
-
|
147
|
-
|
143
|
+
# Basic OpenAI-style fields
|
144
|
+
role: MessageRole = Field(..., description="The role of the participant.")
|
145
|
+
content: Optional[List[LettaMessageContentUnion]] = Field(None, description="The content of the message.")
|
146
|
+
# NOTE: in OpenAI, this field is only used for roles 'user', 'assistant', and 'function' (now deprecated). 'tool' does not use it.
|
147
|
+
name: Optional[str] = Field(
|
148
|
+
None,
|
149
|
+
description="For role user/assistant: the (optional) name of the participant. For role tool/function: the name of the function called.",
|
150
|
+
)
|
151
|
+
tool_calls: Optional[List[OpenAIToolCall]] = Field(
|
152
|
+
None, description="The list of tool calls requested. Only applicable for role assistant."
|
153
|
+
)
|
154
|
+
tool_call_id: Optional[str] = Field(None, description="The ID of the tool call. Only applicable for role tool.")
|
155
|
+
# Extras
|
148
156
|
step_id: Optional[str] = Field(None, description="The id of the step that this message was created in.")
|
149
157
|
otid: Optional[str] = Field(None, description="The offline threading id associated with this message")
|
150
158
|
tool_returns: Optional[List[ToolReturn]] = Field(None, description="Tool execution return information for prior tool calls")
|
151
159
|
group_id: Optional[str] = Field(None, description="The multi-agent group that the message was sent in")
|
152
|
-
|
153
160
|
# This overrides the optional base orm schema, created_at MUST exist on all messages objects
|
154
161
|
created_at: datetime = Field(default_factory=get_utc_time, description="The timestamp when the object was created.")
|
155
162
|
|
@@ -406,7 +413,6 @@ class Message(BaseMessage):
|
|
406
413
|
|
407
414
|
@staticmethod
|
408
415
|
def dict_to_message(
|
409
|
-
user_id: str,
|
410
416
|
agent_id: str,
|
411
417
|
openai_message_dict: dict,
|
412
418
|
model: Optional[str] = None, # model used to make function call
|
@@ -560,7 +566,7 @@ class Message(BaseMessage):
|
|
560
566
|
# standard fields expected in an OpenAI ChatCompletion message object
|
561
567
|
role=MessageRole(openai_message_dict["role"]),
|
562
568
|
content=content,
|
563
|
-
name=name,
|
569
|
+
name=openai_message_dict["name"] if "name" in openai_message_dict else name,
|
564
570
|
tool_calls=tool_calls,
|
565
571
|
tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
|
566
572
|
created_at=created_at,
|
@@ -575,7 +581,7 @@ class Message(BaseMessage):
|
|
575
581
|
# standard fields expected in an OpenAI ChatCompletion message object
|
576
582
|
role=MessageRole(openai_message_dict["role"]),
|
577
583
|
content=content,
|
578
|
-
name=name,
|
584
|
+
name=openai_message_dict["name"] if "name" in openai_message_dict else name,
|
579
585
|
tool_calls=tool_calls,
|
580
586
|
tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
|
581
587
|
created_at=created_at,
|
@@ -592,6 +598,7 @@ class Message(BaseMessage):
|
|
592
598
|
self,
|
593
599
|
max_tool_id_length: int = TOOL_CALL_ID_MAX_LEN,
|
594
600
|
put_inner_thoughts_in_kwargs: bool = False,
|
601
|
+
use_developer_message: bool = False,
|
595
602
|
) -> dict:
|
596
603
|
"""Go from Message class to ChatCompletion message object"""
|
597
604
|
|
@@ -619,7 +626,7 @@ class Message(BaseMessage):
|
|
619
626
|
assert all([v is not None for v in [self.role]]), vars(self)
|
620
627
|
openai_message = {
|
621
628
|
"content": text_content,
|
622
|
-
"role": self.role,
|
629
|
+
"role": "developer" if use_developer_message else self.role,
|
623
630
|
}
|
624
631
|
|
625
632
|
elif self.role == "user":
|
@@ -809,7 +816,7 @@ class Message(BaseMessage):
|
|
809
816
|
text_content = None
|
810
817
|
|
811
818
|
if self.role != "tool" and self.name is not None:
|
812
|
-
warnings.warn(f"Using Google AI with non-null 'name' field ({self.name}) not yet supported.")
|
819
|
+
warnings.warn(f"Using Google AI with non-null 'name' field (name={self.name} role={self.role}), not yet supported.")
|
813
820
|
|
814
821
|
if self.role == "system":
|
815
822
|
# NOTE: Gemini API doesn't have a 'system' role, use 'user' instead
|
@@ -908,7 +915,9 @@ class Message(BaseMessage):
|
|
908
915
|
if "parts" not in google_ai_message or not google_ai_message["parts"]:
|
909
916
|
# If parts is empty, add a default text part
|
910
917
|
google_ai_message["parts"] = [{"text": "empty message"}]
|
911
|
-
warnings.warn(
|
918
|
+
warnings.warn(
|
919
|
+
f"Empty 'parts' detected in message with role '{self.role}'. Added default empty text part. Full message:\n{vars(self)}"
|
920
|
+
)
|
912
921
|
|
913
922
|
return google_ai_message
|
914
923
|
|
@@ -133,6 +133,7 @@ class ChatCompletionRequest(BaseModel):
|
|
133
133
|
temperature: Optional[float] = 1
|
134
134
|
top_p: Optional[float] = 1
|
135
135
|
user: Optional[str] = None # unique ID of the end-user (for monitoring)
|
136
|
+
parallel_tool_calls: Optional[bool] = None
|
136
137
|
|
137
138
|
# function-calling related
|
138
139
|
tools: Optional[List[Tool]] = None
|
letta/schemas/tool_rule.py
CHANGED
@@ -29,6 +29,19 @@ class ChildToolRule(BaseToolRule):
|
|
29
29
|
return set(self.children) if last_tool == self.tool_name else available_tools
|
30
30
|
|
31
31
|
|
32
|
+
class ParentToolRule(BaseToolRule):
|
33
|
+
"""
|
34
|
+
A ToolRule that only allows a child tool to be called if the parent has been called.
|
35
|
+
"""
|
36
|
+
|
37
|
+
type: Literal[ToolRuleType.parent_last_tool] = ToolRuleType.parent_last_tool
|
38
|
+
children: List[str] = Field(..., description="The children tools that can be invoked.")
|
39
|
+
|
40
|
+
def get_valid_tools(self, tool_call_history: List[str], available_tools: Set[str], last_function_response: Optional[str]) -> Set[str]:
|
41
|
+
last_tool = tool_call_history[-1] if tool_call_history else None
|
42
|
+
return set(self.children) if last_tool == self.tool_name else available_tools - set(self.children)
|
43
|
+
|
44
|
+
|
32
45
|
class ConditionalToolRule(BaseToolRule):
|
33
46
|
"""
|
34
47
|
A ToolRule that conditionally maps to different child tools based on the output.
|
@@ -128,6 +141,6 @@ class MaxCountPerStepToolRule(BaseToolRule):
|
|
128
141
|
|
129
142
|
|
130
143
|
ToolRule = Annotated[
|
131
|
-
Union[ChildToolRule, InitToolRule, TerminalToolRule, ConditionalToolRule, ContinueToolRule, MaxCountPerStepToolRule],
|
144
|
+
Union[ChildToolRule, InitToolRule, TerminalToolRule, ConditionalToolRule, ContinueToolRule, MaxCountPerStepToolRule, ParentToolRule],
|
132
145
|
Field(discriminator="type"),
|
133
146
|
]
|
@@ -29,6 +29,7 @@ from letta.schemas.openai.chat_completion_response import ChatCompletionChunkRes
|
|
29
29
|
from letta.server.rest_api.optimistic_json_parser import OptimisticJSONParser
|
30
30
|
from letta.streaming_interface import AgentChunkStreamingInterface
|
31
31
|
from letta.streaming_utils import FunctionArgumentsStreamHandler, JSONInnerThoughtsExtractor
|
32
|
+
from letta.utils import parse_json
|
32
33
|
|
33
34
|
|
34
35
|
# TODO strip from code / deprecate
|
@@ -408,7 +409,7 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
408
409
|
# if self.expect_reasoning_content_buffer is not None:
|
409
410
|
# try:
|
410
411
|
# # NOTE: this is hardcoded for our DeepSeek API integration
|
411
|
-
# json_reasoning_content =
|
412
|
+
# json_reasoning_content = parse_json(self.expect_reasoning_content_buffer)
|
412
413
|
|
413
414
|
# if "name" in json_reasoning_content:
|
414
415
|
# self._push_to_buffer(
|
@@ -528,7 +529,7 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
528
529
|
|
529
530
|
try:
|
530
531
|
# NOTE: this is hardcoded for our DeepSeek API integration
|
531
|
-
json_reasoning_content =
|
532
|
+
json_reasoning_content = parse_json(self.expect_reasoning_content_buffer)
|
532
533
|
print(f"json_reasoning_content: {json_reasoning_content}")
|
533
534
|
|
534
535
|
processed_chunk = ToolCallMessage(
|
@@ -1188,7 +1189,7 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
1188
1189
|
# "date": "2024-06-22T23:04:32.141923+00:00"
|
1189
1190
|
# }
|
1190
1191
|
try:
|
1191
|
-
func_args =
|
1192
|
+
func_args = parse_json(function_call.function.arguments)
|
1192
1193
|
except:
|
1193
1194
|
func_args = function_call.function.arguments
|
1194
1195
|
# processed_chunk = {
|
@@ -1224,7 +1225,7 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
1224
1225
|
else:
|
1225
1226
|
|
1226
1227
|
try:
|
1227
|
-
func_args =
|
1228
|
+
func_args = parse_json(function_call.function.arguments)
|
1228
1229
|
except:
|
1229
1230
|
warnings.warn(f"Failed to parse function arguments: {function_call.function.arguments}")
|
1230
1231
|
func_args = {}
|
@@ -1,14 +1,15 @@
|
|
1
1
|
import json
|
2
2
|
import traceback
|
3
3
|
from datetime import datetime
|
4
|
-
from typing import Annotated, List, Optional
|
4
|
+
from typing import Annotated, Any, List, Optional
|
5
5
|
|
6
6
|
from fastapi import APIRouter, BackgroundTasks, Body, Depends, File, Header, HTTPException, Query, UploadFile, status
|
7
7
|
from fastapi.responses import JSONResponse
|
8
8
|
from marshmallow import ValidationError
|
9
|
+
from orjson import orjson
|
9
10
|
from pydantic import Field
|
10
11
|
from sqlalchemy.exc import IntegrityError, OperationalError
|
11
|
-
from starlette.responses import StreamingResponse
|
12
|
+
from starlette.responses import Response, StreamingResponse
|
12
13
|
|
13
14
|
from letta.agents.letta_agent import LettaAgent
|
14
15
|
from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
|
@@ -22,7 +23,6 @@ from letta.schemas.letta_request import LettaRequest, LettaStreamingRequest
|
|
22
23
|
from letta.schemas.letta_response import LettaResponse
|
23
24
|
from letta.schemas.memory import ContextWindowOverview, CreateArchivalMemory, Memory
|
24
25
|
from letta.schemas.message import MessageCreate
|
25
|
-
from letta.schemas.openai.chat_completion_request import UserMessage
|
26
26
|
from letta.schemas.passage import Passage, PassageUpdate
|
27
27
|
from letta.schemas.run import Run
|
28
28
|
from letta.schemas.source import Source
|
@@ -103,19 +103,30 @@ def list_agents(
|
|
103
103
|
)
|
104
104
|
|
105
105
|
|
106
|
-
|
106
|
+
class IndentedORJSONResponse(Response):
|
107
|
+
media_type = "application/json"
|
108
|
+
|
109
|
+
def render(self, content: Any) -> bytes:
|
110
|
+
return orjson.dumps(content, option=orjson.OPT_INDENT_2)
|
111
|
+
|
112
|
+
|
113
|
+
@router.get("/{agent_id}/export", response_class=IndentedORJSONResponse, operation_id="export_agent_serialized")
|
107
114
|
def export_agent_serialized(
|
108
115
|
agent_id: str,
|
109
116
|
server: "SyncServer" = Depends(get_letta_server),
|
110
117
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
111
|
-
|
118
|
+
# do not remove, used to autogeneration of spec
|
119
|
+
# TODO: Think of a better way to export AgentSchema
|
120
|
+
spec: Optional[AgentSchema] = None,
|
121
|
+
) -> JSONResponse:
|
112
122
|
"""
|
113
|
-
Export the serialized JSON representation of an agent.
|
123
|
+
Export the serialized JSON representation of an agent, formatted with indentation.
|
114
124
|
"""
|
115
125
|
actor = server.user_manager.get_user_or_default(user_id=actor_id)
|
116
126
|
|
117
127
|
try:
|
118
|
-
|
128
|
+
agent = server.agent_manager.serialize(agent_id=agent_id, actor=actor)
|
129
|
+
return agent.model_dump()
|
119
130
|
except NoResultFound:
|
120
131
|
raise HTTPException(status_code=404, detail=f"Agent with id={agent_id} not found for user_id={actor.id}.")
|
121
132
|
|
@@ -610,9 +621,7 @@ async def send_message(
|
|
610
621
|
actor=actor,
|
611
622
|
)
|
612
623
|
|
613
|
-
|
614
|
-
content = messages[0].content[0].text if messages and not isinstance(messages[0].content, str) else messages[0].content
|
615
|
-
result = await experimental_agent.step(UserMessage(content=content), max_steps=10)
|
624
|
+
result = await experimental_agent.step(request.messages, max_steps=10)
|
616
625
|
else:
|
617
626
|
result = await server.send_message_to_agent(
|
618
627
|
agent_id=agent_id,
|
@@ -672,10 +681,8 @@ async def send_message_streaming(
|
|
672
681
|
actor=actor,
|
673
682
|
)
|
674
683
|
|
675
|
-
messages = request.messages
|
676
|
-
content = messages[0].content[0].text if messages and not isinstance(messages[0].content, str) else messages[0].content
|
677
684
|
result = StreamingResponse(
|
678
|
-
experimental_agent.step_stream(
|
685
|
+
experimental_agent.step_stream(request.messages, max_steps=10, use_assistant_message=request.use_assistant_message),
|
679
686
|
media_type="text/event-stream",
|
680
687
|
)
|
681
688
|
else:
|
@@ -74,7 +74,7 @@ def create_group(
|
|
74
74
|
raise HTTPException(status_code=500, detail=str(e))
|
75
75
|
|
76
76
|
|
77
|
-
@router.
|
77
|
+
@router.patch("/{group_id}", response_model=Group, operation_id="modify_group")
|
78
78
|
def modify_group(
|
79
79
|
group_id: str,
|
80
80
|
group: GroupUpdate = Body(...),
|
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, List, Optional
|
|
3
3
|
from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query
|
4
4
|
|
5
5
|
from letta.orm.errors import NoResultFound, UniqueConstraintViolationError
|
6
|
-
from letta.schemas.identity import Identity, IdentityCreate, IdentityType, IdentityUpdate
|
6
|
+
from letta.schemas.identity import Identity, IdentityCreate, IdentityProperty, IdentityType, IdentityUpdate, IdentityUpsert
|
7
7
|
from letta.server.rest_api.utils import get_letta_server
|
8
8
|
|
9
9
|
if TYPE_CHECKING:
|
@@ -88,7 +88,7 @@ def create_identity(
|
|
88
88
|
|
89
89
|
@router.put("/", tags=["identities"], response_model=Identity, operation_id="upsert_identity")
|
90
90
|
def upsert_identity(
|
91
|
-
identity:
|
91
|
+
identity: IdentityUpsert = Body(...),
|
92
92
|
server: "SyncServer" = Depends(get_letta_server),
|
93
93
|
actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
94
94
|
x_project: Optional[str] = Header(None, alias="X-Project"), # Only handled by next js middleware
|
@@ -118,6 +118,27 @@ def modify_identity(
|
|
118
118
|
raise
|
119
119
|
except NoResultFound as e:
|
120
120
|
raise HTTPException(status_code=404, detail=str(e))
|
121
|
+
except Exception as e:
|
122
|
+
import traceback
|
123
|
+
|
124
|
+
print(traceback.format_exc())
|
125
|
+
raise HTTPException(status_code=500, detail=f"{e}")
|
126
|
+
|
127
|
+
|
128
|
+
@router.put("/{identity_id}/properties", tags=["identities"], operation_id="upsert_identity_properties")
|
129
|
+
def upsert_identity_properties(
|
130
|
+
identity_id: str,
|
131
|
+
properties: List[IdentityProperty] = Body(...),
|
132
|
+
server: "SyncServer" = Depends(get_letta_server),
|
133
|
+
actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
134
|
+
):
|
135
|
+
try:
|
136
|
+
actor = server.user_manager.get_user_or_default(user_id=actor_id)
|
137
|
+
return server.identity_manager.upsert_identity_properties(identity_id=identity_id, properties=properties, actor=actor)
|
138
|
+
except HTTPException:
|
139
|
+
raise
|
140
|
+
except NoResultFound as e:
|
141
|
+
raise HTTPException(status_code=404, detail=str(e))
|
121
142
|
except Exception as e:
|
122
143
|
raise HTTPException(status_code=500, detail=f"{e}")
|
123
144
|
|
letta/server/rest_api/utils.py
CHANGED
@@ -19,7 +19,7 @@ from letta.helpers.datetime_helpers import get_utc_time
|
|
19
19
|
from letta.log import get_logger
|
20
20
|
from letta.schemas.enums import MessageRole
|
21
21
|
from letta.schemas.letta_message_content import OmittedReasoningContent, ReasoningContent, RedactedReasoningContent, TextContent
|
22
|
-
from letta.schemas.message import Message
|
22
|
+
from letta.schemas.message import Message, MessageCreate
|
23
23
|
from letta.schemas.usage import LettaUsageStatistics
|
24
24
|
from letta.schemas.user import User
|
25
25
|
from letta.server.rest_api.interface import StreamingServerInterface
|
@@ -140,31 +140,29 @@ def log_error_to_sentry(e):
|
|
140
140
|
sentry_sdk.capture_exception(e)
|
141
141
|
|
142
142
|
|
143
|
-
def
|
143
|
+
def create_input_messages(input_messages: List[MessageCreate], agent_id: str, actor: User) -> List[Message]:
|
144
144
|
"""
|
145
145
|
Converts a user input message into the internal structured format.
|
146
146
|
"""
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
created_at=get_utc_time(),
|
165
|
-
)
|
147
|
+
new_messages = []
|
148
|
+
for input_message in input_messages:
|
149
|
+
# Construct the Message object
|
150
|
+
new_message = Message(
|
151
|
+
id=f"message-{uuid.uuid4()}",
|
152
|
+
role=input_message.role,
|
153
|
+
content=input_message.content,
|
154
|
+
name=input_message.name,
|
155
|
+
otid=input_message.otid,
|
156
|
+
organization_id=actor.organization_id,
|
157
|
+
agent_id=agent_id,
|
158
|
+
model=None,
|
159
|
+
tool_calls=None,
|
160
|
+
tool_call_id=None,
|
161
|
+
created_at=get_utc_time(),
|
162
|
+
)
|
163
|
+
new_messages.append(new_message)
|
166
164
|
|
167
|
-
return
|
165
|
+
return new_messages
|
168
166
|
|
169
167
|
|
170
168
|
def create_letta_messages_from_llm_response(
|