letta-nightly 0.5.5.dev20241122170833__py3-none-any.whl → 0.6.0.dev20241204051808__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 +2 -2
- letta/agent.py +155 -166
- letta/agent_store/chroma.py +2 -0
- letta/agent_store/db.py +1 -1
- letta/cli/cli.py +12 -8
- letta/cli/cli_config.py +1 -1
- letta/client/client.py +765 -137
- letta/config.py +2 -2
- letta/constants.py +10 -14
- letta/errors.py +12 -0
- letta/functions/function_sets/base.py +38 -1
- letta/functions/functions.py +40 -57
- letta/functions/helpers.py +0 -4
- letta/functions/schema_generator.py +279 -18
- letta/helpers/tool_rule_solver.py +6 -5
- letta/llm_api/helpers.py +99 -5
- letta/llm_api/openai.py +8 -2
- letta/local_llm/utils.py +13 -6
- letta/log.py +7 -9
- letta/main.py +1 -1
- letta/metadata.py +53 -38
- letta/o1_agent.py +1 -4
- letta/orm/__init__.py +2 -0
- letta/orm/block.py +7 -3
- letta/orm/blocks_agents.py +32 -0
- letta/orm/errors.py +8 -0
- letta/orm/mixins.py +8 -0
- letta/orm/organization.py +8 -1
- letta/orm/sandbox_config.py +56 -0
- letta/orm/sqlalchemy_base.py +68 -10
- letta/persistence_manager.py +1 -0
- letta/schemas/agent.py +57 -52
- letta/schemas/block.py +85 -26
- letta/schemas/blocks_agents.py +32 -0
- letta/schemas/enums.py +14 -0
- letta/schemas/letta_base.py +10 -1
- letta/schemas/letta_request.py +11 -23
- letta/schemas/letta_response.py +1 -2
- letta/schemas/memory.py +41 -76
- letta/schemas/message.py +3 -3
- letta/schemas/sandbox_config.py +114 -0
- letta/schemas/tool.py +37 -1
- letta/schemas/tool_rule.py +13 -5
- letta/server/rest_api/app.py +5 -4
- letta/server/rest_api/interface.py +12 -19
- letta/server/rest_api/routers/openai/assistants/threads.py +2 -3
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +0 -2
- letta/server/rest_api/routers/v1/__init__.py +4 -9
- letta/server/rest_api/routers/v1/agents.py +145 -61
- letta/server/rest_api/routers/v1/blocks.py +50 -5
- letta/server/rest_api/routers/v1/sandbox_configs.py +127 -0
- letta/server/rest_api/routers/v1/sources.py +8 -1
- letta/server/rest_api/routers/v1/tools.py +139 -13
- letta/server/rest_api/utils.py +6 -0
- letta/server/server.py +397 -340
- letta/server/static_files/assets/index-9fa459a2.js +1 -1
- letta/services/block_manager.py +23 -2
- letta/services/blocks_agents_manager.py +106 -0
- letta/services/per_agent_lock_manager.py +18 -0
- letta/services/sandbox_config_manager.py +256 -0
- letta/services/tool_execution_sandbox.py +352 -0
- letta/services/tool_manager.py +16 -22
- letta/services/tool_sandbox_env/.gitkeep +0 -0
- letta/settings.py +4 -0
- letta/utils.py +0 -7
- {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/METADATA +8 -6
- {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/RECORD +70 -60
- {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/LICENSE +0 -0
- {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/WHEEL +0 -0
- {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/entry_points.txt +0 -0
letta/schemas/letta_response.py
CHANGED
|
@@ -7,7 +7,6 @@ from pydantic import BaseModel, Field
|
|
|
7
7
|
|
|
8
8
|
from letta.schemas.enums import MessageStreamStatus
|
|
9
9
|
from letta.schemas.letta_message import LettaMessage, LettaMessageUnion
|
|
10
|
-
from letta.schemas.message import Message
|
|
11
10
|
from letta.schemas.usage import LettaUsageStatistics
|
|
12
11
|
from letta.utils import json_dumps
|
|
13
12
|
|
|
@@ -24,7 +23,7 @@ class LettaResponse(BaseModel):
|
|
|
24
23
|
usage (LettaUsageStatistics): The usage statistics
|
|
25
24
|
"""
|
|
26
25
|
|
|
27
|
-
messages:
|
|
26
|
+
messages: List[LettaMessageUnion] = Field(..., description="The messages returned by the agent.")
|
|
28
27
|
usage: LettaUsageStatistics = Field(..., description="The usage statistics of the agent.")
|
|
29
28
|
|
|
30
29
|
def __str__(self):
|
letta/schemas/memory.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING,
|
|
1
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
2
2
|
|
|
3
3
|
from jinja2 import Template, TemplateSyntaxError
|
|
4
4
|
from pydantic import BaseModel, Field
|
|
5
5
|
|
|
6
6
|
# Forward referencing to avoid circular import with Agent -> Memory -> Agent
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
|
-
|
|
8
|
+
pass
|
|
9
9
|
|
|
10
|
+
from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT
|
|
10
11
|
from letta.schemas.block import Block
|
|
11
12
|
from letta.schemas.message import Message
|
|
12
13
|
from letta.schemas.openai.chat_completion_request import Tool
|
|
@@ -54,19 +55,16 @@ class ContextWindowOverview(BaseModel):
|
|
|
54
55
|
class Memory(BaseModel, validate_assignment=True):
|
|
55
56
|
"""
|
|
56
57
|
|
|
57
|
-
Represents the in-context memory of the agent. This includes both the `Block` objects (labelled by sections), as well as tools to edit the blocks.
|
|
58
|
-
|
|
59
|
-
Attributes:
|
|
60
|
-
memory (Dict[str, Block]): Mapping from memory block section to memory block.
|
|
58
|
+
Represents the in-context memory (i.e. Core memory) of the agent. This includes both the `Block` objects (labelled by sections), as well as tools to edit the blocks.
|
|
61
59
|
|
|
62
60
|
"""
|
|
63
61
|
|
|
64
|
-
# Memory.
|
|
65
|
-
|
|
62
|
+
# Memory.block contains the list of memory blocks in the core memory
|
|
63
|
+
blocks: List[Block] = Field(..., description="Memory blocks contained in the agent's in-context memory")
|
|
66
64
|
|
|
67
65
|
# Memory.template is a Jinja2 template for compiling memory module into a prompt string.
|
|
68
66
|
prompt_template: str = Field(
|
|
69
|
-
default="{% for block in
|
|
67
|
+
default="{% for block in blocks %}"
|
|
70
68
|
'<{{ block.label }} characters="{{ block.value|length }}/{{ block.limit }}">\n'
|
|
71
69
|
"{{ block.value }}\n"
|
|
72
70
|
"</{{ block.label }}>"
|
|
@@ -89,7 +87,7 @@ class Memory(BaseModel, validate_assignment=True):
|
|
|
89
87
|
Template(prompt_template)
|
|
90
88
|
|
|
91
89
|
# Validate compatibility with current memory structure
|
|
92
|
-
test_render = Template(prompt_template).render(
|
|
90
|
+
test_render = Template(prompt_template).render(blocks=self.blocks)
|
|
93
91
|
|
|
94
92
|
# If we get here, the template is valid and compatible
|
|
95
93
|
self.prompt_template = prompt_template
|
|
@@ -98,74 +96,49 @@ class Memory(BaseModel, validate_assignment=True):
|
|
|
98
96
|
except Exception as e:
|
|
99
97
|
raise ValueError(f"Prompt template is not compatible with current memory structure: {str(e)}")
|
|
100
98
|
|
|
101
|
-
@classmethod
|
|
102
|
-
def load(cls, state: dict):
|
|
103
|
-
"""Load memory from dictionary object"""
|
|
104
|
-
obj = cls()
|
|
105
|
-
if len(state.keys()) == 2 and "memory" in state and "prompt_template" in state:
|
|
106
|
-
# New format
|
|
107
|
-
obj.prompt_template = state["prompt_template"]
|
|
108
|
-
for key, value in state["memory"].items():
|
|
109
|
-
# TODO: This is migration code, please take a look at a later time to get rid of this
|
|
110
|
-
if "name" in value:
|
|
111
|
-
value["template_name"] = value["name"]
|
|
112
|
-
value.pop("name")
|
|
113
|
-
obj.memory[key] = Block(**value)
|
|
114
|
-
else:
|
|
115
|
-
# Old format (pre-template)
|
|
116
|
-
for key, value in state.items():
|
|
117
|
-
obj.memory[key] = Block(**value)
|
|
118
|
-
return obj
|
|
119
|
-
|
|
120
99
|
def compile(self) -> str:
|
|
121
100
|
"""Generate a string representation of the memory in-context using the Jinja2 template"""
|
|
122
101
|
template = Template(self.prompt_template)
|
|
123
|
-
return template.render(
|
|
124
|
-
|
|
125
|
-
def to_dict(self):
|
|
126
|
-
"""Convert to dictionary representation"""
|
|
127
|
-
return {
|
|
128
|
-
"memory": {key: value.model_dump() for key, value in self.memory.items()},
|
|
129
|
-
"prompt_template": self.prompt_template,
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
def to_flat_dict(self):
|
|
133
|
-
"""Convert to a dictionary that maps directly from block label to values"""
|
|
134
|
-
return {k: v.value for k, v in self.memory.items() if v is not None}
|
|
102
|
+
return template.render(blocks=self.blocks)
|
|
135
103
|
|
|
136
104
|
def list_block_labels(self) -> List[str]:
|
|
137
105
|
"""Return a list of the block names held inside the memory object"""
|
|
138
|
-
return list(self.memory.keys())
|
|
106
|
+
# return list(self.memory.keys())
|
|
107
|
+
return [block.label for block in self.blocks]
|
|
139
108
|
|
|
140
109
|
# TODO: these should actually be label, not name
|
|
141
110
|
def get_block(self, label: str) -> Block:
|
|
142
111
|
"""Correct way to index into the memory.memory field, returns a Block"""
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
112
|
+
keys = []
|
|
113
|
+
for block in self.blocks:
|
|
114
|
+
if block.label == label:
|
|
115
|
+
return block
|
|
116
|
+
keys.append(block.label)
|
|
117
|
+
raise KeyError(f"Block field {label} does not exist (available sections = {', '.join(keys)})")
|
|
147
118
|
|
|
148
119
|
def get_blocks(self) -> List[Block]:
|
|
149
120
|
"""Return a list of the blocks held inside the memory object"""
|
|
150
|
-
return list(self.memory.values())
|
|
121
|
+
# return list(self.memory.values())
|
|
122
|
+
return self.blocks
|
|
151
123
|
|
|
152
|
-
def
|
|
153
|
-
"""
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
self.memory[block.label] = block
|
|
124
|
+
def set_block(self, block: Block):
|
|
125
|
+
"""Set a block in the memory object"""
|
|
126
|
+
for i, b in enumerate(self.blocks):
|
|
127
|
+
if b.label == block.label:
|
|
128
|
+
self.blocks[i] = block
|
|
129
|
+
return
|
|
130
|
+
self.blocks.append(block)
|
|
160
131
|
|
|
161
132
|
def update_block_value(self, label: str, value: str):
|
|
162
133
|
"""Update the value of a block"""
|
|
163
|
-
if label not in self.memory:
|
|
164
|
-
raise ValueError(f"Block with label {label} does not exist")
|
|
165
134
|
if not isinstance(value, str):
|
|
166
135
|
raise ValueError(f"Provided value must be a string")
|
|
167
136
|
|
|
168
|
-
self.
|
|
137
|
+
for block in self.blocks:
|
|
138
|
+
if block.label == label:
|
|
139
|
+
block.value = value
|
|
140
|
+
return
|
|
141
|
+
raise ValueError(f"Block with label {label} does not exist")
|
|
169
142
|
|
|
170
143
|
|
|
171
144
|
# TODO: ideally this is refactored into ChatMemory and the subclasses are given more specific names.
|
|
@@ -188,15 +161,9 @@ class BasicBlockMemory(Memory):
|
|
|
188
161
|
Args:
|
|
189
162
|
blocks (List[Block]): List of blocks to be linked to the memory object.
|
|
190
163
|
"""
|
|
191
|
-
super().__init__()
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
# assert block.name is not None and block.name != "", "each existing chat block must have a name"
|
|
195
|
-
# self.link_block(name=block.name, block=block)
|
|
196
|
-
assert block.label is not None and block.label != "", "each existing chat block must have a name"
|
|
197
|
-
self.link_block(block=block)
|
|
198
|
-
|
|
199
|
-
def core_memory_append(self: "Agent", label: str, content: str) -> Optional[str]: # type: ignore
|
|
164
|
+
super().__init__(blocks=blocks)
|
|
165
|
+
|
|
166
|
+
def core_memory_append(agent_state: "AgentState", label: str, content: str) -> Optional[str]: # type: ignore
|
|
200
167
|
"""
|
|
201
168
|
Append to the contents of core memory.
|
|
202
169
|
|
|
@@ -207,12 +174,12 @@ class BasicBlockMemory(Memory):
|
|
|
207
174
|
Returns:
|
|
208
175
|
Optional[str]: None is always returned as this function does not produce a response.
|
|
209
176
|
"""
|
|
210
|
-
current_value = str(
|
|
177
|
+
current_value = str(agent_state.memory.get_block(label).value)
|
|
211
178
|
new_value = current_value + "\n" + str(content)
|
|
212
|
-
|
|
179
|
+
agent_state.memory.update_block_value(label=label, value=new_value)
|
|
213
180
|
return None
|
|
214
181
|
|
|
215
|
-
def core_memory_replace(
|
|
182
|
+
def core_memory_replace(agent_state: "AgentState", label: str, old_content: str, new_content: str) -> Optional[str]: # type: ignore
|
|
216
183
|
"""
|
|
217
184
|
Replace the contents of core memory. To delete memories, use an empty string for new_content.
|
|
218
185
|
|
|
@@ -224,11 +191,11 @@ class BasicBlockMemory(Memory):
|
|
|
224
191
|
Returns:
|
|
225
192
|
Optional[str]: None is always returned as this function does not produce a response.
|
|
226
193
|
"""
|
|
227
|
-
current_value = str(
|
|
194
|
+
current_value = str(agent_state.memory.get_block(label).value)
|
|
228
195
|
if old_content not in current_value:
|
|
229
196
|
raise ValueError(f"Old content '{old_content}' not found in memory block '{label}'")
|
|
230
197
|
new_value = current_value.replace(str(old_content), str(new_content))
|
|
231
|
-
|
|
198
|
+
agent_state.memory.update_block_value(label=label, value=new_value)
|
|
232
199
|
return None
|
|
233
200
|
|
|
234
201
|
|
|
@@ -237,7 +204,7 @@ class ChatMemory(BasicBlockMemory):
|
|
|
237
204
|
ChatMemory initializes a BaseChatMemory with two default blocks, `human` and `persona`.
|
|
238
205
|
"""
|
|
239
206
|
|
|
240
|
-
def __init__(self, persona: str, human: str, limit: int =
|
|
207
|
+
def __init__(self, persona: str, human: str, limit: int = CORE_MEMORY_BLOCK_CHAR_LIMIT):
|
|
241
208
|
"""
|
|
242
209
|
Initialize the ChatMemory object with a persona and human string.
|
|
243
210
|
|
|
@@ -246,9 +213,7 @@ class ChatMemory(BasicBlockMemory):
|
|
|
246
213
|
human (str): The starter value for the human block.
|
|
247
214
|
limit (int): The character limit for each block.
|
|
248
215
|
"""
|
|
249
|
-
super().__init__()
|
|
250
|
-
self.link_block(block=Block(value=persona, limit=limit, label="persona"))
|
|
251
|
-
self.link_block(block=Block(value=human, limit=limit, label="human"))
|
|
216
|
+
super().__init__(blocks=[Block(value=persona, limit=limit, label="persona"), Block(value=human, limit=limit, label="human")])
|
|
252
217
|
|
|
253
218
|
|
|
254
219
|
class UpdateMemory(BaseModel):
|
letta/schemas/message.py
CHANGED
|
@@ -134,8 +134,8 @@ class Message(BaseMessage):
|
|
|
134
134
|
def to_letta_message(
|
|
135
135
|
self,
|
|
136
136
|
assistant_message: bool = False,
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
assistant_message_tool_name: str = DEFAULT_MESSAGE_TOOL,
|
|
138
|
+
assistant_message_tool_kwarg: str = DEFAULT_MESSAGE_TOOL_KWARG,
|
|
139
139
|
) -> List[LettaMessage]:
|
|
140
140
|
"""Convert message object (in DB format) to the style used by the original Letta API"""
|
|
141
141
|
|
|
@@ -156,7 +156,7 @@ class Message(BaseMessage):
|
|
|
156
156
|
for tool_call in self.tool_calls:
|
|
157
157
|
# If we're supporting using assistant message,
|
|
158
158
|
# then we want to treat certain function calls as a special case
|
|
159
|
-
if assistant_message and tool_call.function.name ==
|
|
159
|
+
if assistant_message and tool_call.function.name == assistant_message_tool_name:
|
|
160
160
|
# We need to unpack the actual message contents from the function call
|
|
161
161
|
try:
|
|
162
162
|
func_args = json.loads(tool_call.function.arguments)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any, Dict, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from letta.schemas.agent import AgentState
|
|
9
|
+
from letta.schemas.letta_base import LettaBase, OrmMetadataBase
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Sandbox Config
|
|
13
|
+
class SandboxType(str, Enum):
|
|
14
|
+
E2B = "e2b"
|
|
15
|
+
LOCAL = "local"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SandboxRunResult(BaseModel):
|
|
19
|
+
func_return: Optional[Any] = Field(None, description="The function return object")
|
|
20
|
+
agent_state: Optional[AgentState] = Field(None, description="The agent state")
|
|
21
|
+
stdout: Optional[List[str]] = Field(None, description="Captured stdout (e.g. prints, logs) from the function invocation")
|
|
22
|
+
sandbox_config_fingerprint: str = Field(None, description="The fingerprint of the config for the sandbox")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LocalSandboxConfig(BaseModel):
|
|
26
|
+
sandbox_dir: str = Field(..., description="Directory for the sandbox environment.")
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def type(self) -> "SandboxType":
|
|
30
|
+
return SandboxType.LOCAL
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class E2BSandboxConfig(BaseModel):
|
|
34
|
+
timeout: int = Field(5 * 60, description="Time limit for the sandbox (in seconds).")
|
|
35
|
+
template: Optional[str] = Field(None, description="The E2B template id (docker image).")
|
|
36
|
+
pip_requirements: Optional[List[str]] = Field(None, description="A list of pip packages to install on the E2B Sandbox")
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def type(self) -> "SandboxType":
|
|
40
|
+
return SandboxType.E2B
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SandboxConfigBase(OrmMetadataBase):
|
|
44
|
+
__id_prefix__ = "sandbox"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SandboxConfig(SandboxConfigBase):
|
|
48
|
+
id: str = SandboxConfigBase.generate_id_field()
|
|
49
|
+
type: SandboxType = Field(None, description="The type of sandbox.")
|
|
50
|
+
organization_id: Optional[str] = Field(None, description="The unique identifier of the organization associated with the sandbox.")
|
|
51
|
+
config: Dict = Field(default_factory=lambda: {}, description="The JSON sandbox settings data.")
|
|
52
|
+
|
|
53
|
+
def get_e2b_config(self) -> E2BSandboxConfig:
|
|
54
|
+
return E2BSandboxConfig(**self.config)
|
|
55
|
+
|
|
56
|
+
def get_local_config(self) -> LocalSandboxConfig:
|
|
57
|
+
return LocalSandboxConfig(**self.config)
|
|
58
|
+
|
|
59
|
+
def fingerprint(self) -> str:
|
|
60
|
+
# Only take into account type, org_id, and the config items
|
|
61
|
+
# Canonicalize input data into JSON with sorted keys
|
|
62
|
+
hash_input = json.dumps(
|
|
63
|
+
{
|
|
64
|
+
"type": self.type.value,
|
|
65
|
+
"organization_id": self.organization_id,
|
|
66
|
+
"config": self.config,
|
|
67
|
+
},
|
|
68
|
+
sort_keys=True, # Ensure stable ordering
|
|
69
|
+
separators=(",", ":"), # Minimize serialization differences
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Compute SHA-256 hash
|
|
73
|
+
hash_digest = hashlib.sha256(hash_input.encode("utf-8")).digest()
|
|
74
|
+
|
|
75
|
+
# Convert the digest to an integer for compatibility with Python's hash requirements
|
|
76
|
+
return str(int.from_bytes(hash_digest, byteorder="big"))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class SandboxConfigCreate(LettaBase):
|
|
80
|
+
config: Union[LocalSandboxConfig, E2BSandboxConfig] = Field(..., description="The configuration for the sandbox.")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class SandboxConfigUpdate(LettaBase):
|
|
84
|
+
"""Pydantic model for updating SandboxConfig fields."""
|
|
85
|
+
|
|
86
|
+
config: Union[LocalSandboxConfig, E2BSandboxConfig] = Field(None, description="The JSON configuration data for the sandbox.")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Environment Variable
|
|
90
|
+
class SandboxEnvironmentVariableBase(OrmMetadataBase):
|
|
91
|
+
__id_prefix__ = "sandbox-env"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class SandboxEnvironmentVariable(SandboxEnvironmentVariableBase):
|
|
95
|
+
id: str = SandboxEnvironmentVariableBase.generate_id_field()
|
|
96
|
+
key: str = Field(..., description="The name of the environment variable.")
|
|
97
|
+
value: str = Field(..., description="The value of the environment variable.")
|
|
98
|
+
description: Optional[str] = Field(None, description="An optional description of the environment variable.")
|
|
99
|
+
sandbox_config_id: str = Field(..., description="The ID of the sandbox config this environment variable belongs to.")
|
|
100
|
+
organization_id: Optional[str] = Field(None, description="The ID of the organization this environment variable belongs to.")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class SandboxEnvironmentVariableCreate(LettaBase):
|
|
104
|
+
key: str = Field(..., description="The name of the environment variable.")
|
|
105
|
+
value: str = Field(..., description="The value of the environment variable.")
|
|
106
|
+
description: Optional[str] = Field(None, description="An optional description of the environment variable.")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class SandboxEnvironmentVariableUpdate(LettaBase):
|
|
110
|
+
"""Pydantic model for updating SandboxEnvironmentVariable fields."""
|
|
111
|
+
|
|
112
|
+
key: Optional[str] = Field(None, description="The name of the environment variable.")
|
|
113
|
+
value: Optional[str] = Field(None, description="The value of the environment variable.")
|
|
114
|
+
description: Optional[str] = Field(None, description="An optional description of the environment variable.")
|
letta/schemas/tool.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from typing import Dict, List, Optional
|
|
2
2
|
|
|
3
|
-
from pydantic import Field
|
|
3
|
+
from pydantic import Field, model_validator
|
|
4
4
|
|
|
5
|
+
from letta.functions.functions import derive_openai_json_schema
|
|
5
6
|
from letta.functions.helpers import (
|
|
6
7
|
generate_composio_tool_wrapper,
|
|
7
8
|
generate_langchain_tool_wrapper,
|
|
@@ -44,6 +45,29 @@ class Tool(BaseTool):
|
|
|
44
45
|
created_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
|
|
45
46
|
last_updated_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
|
|
46
47
|
|
|
48
|
+
@model_validator(mode="after")
|
|
49
|
+
def populate_missing_fields(self):
|
|
50
|
+
"""
|
|
51
|
+
Populate missing fields: name, description, and json_schema.
|
|
52
|
+
"""
|
|
53
|
+
# Derive JSON schema if not provided
|
|
54
|
+
if not self.json_schema:
|
|
55
|
+
self.json_schema = derive_openai_json_schema(source_code=self.source_code)
|
|
56
|
+
|
|
57
|
+
# Derive name from the JSON schema if not provided
|
|
58
|
+
if not self.name:
|
|
59
|
+
# TODO: This in theory could error, but name should always be on json_schema
|
|
60
|
+
# TODO: Make JSON schema a typed pydantic object
|
|
61
|
+
self.name = self.json_schema.get("name")
|
|
62
|
+
|
|
63
|
+
# Derive description from the JSON schema if not provided
|
|
64
|
+
if not self.description:
|
|
65
|
+
# TODO: This in theory could error, but description should always be on json_schema
|
|
66
|
+
# TODO: Make JSON schema a typed pydantic object
|
|
67
|
+
self.description = self.json_schema.get("description")
|
|
68
|
+
|
|
69
|
+
return self
|
|
70
|
+
|
|
47
71
|
def to_dict(self):
|
|
48
72
|
"""
|
|
49
73
|
Convert tool into OpenAI representation.
|
|
@@ -177,3 +201,15 @@ class ToolUpdate(LettaBase):
|
|
|
177
201
|
class Config:
|
|
178
202
|
extra = "ignore" # Allows extra fields without validation errors
|
|
179
203
|
# TODO: Remove this, and clean usage of ToolUpdate everywhere else
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class ToolRun(LettaBase):
|
|
207
|
+
id: str = Field(..., description="The ID of the tool to run.")
|
|
208
|
+
args: str = Field(..., description="The arguments to pass to the tool (as stringified JSON).")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class ToolRunFromSource(LettaBase):
|
|
212
|
+
source_code: str = Field(..., description="The source code of the function.")
|
|
213
|
+
args: str = Field(..., description="The arguments to pass to the tool (as stringified JSON).")
|
|
214
|
+
name: Optional[str] = Field(None, description="The name of the tool to run.")
|
|
215
|
+
source_type: Optional[str] = Field(None, description="The type of the source code.")
|
letta/schemas/tool_rule.py
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
from typing import List
|
|
1
|
+
from typing import List, Union
|
|
2
2
|
|
|
3
3
|
from pydantic import Field
|
|
4
4
|
|
|
5
|
+
from letta.schemas.enums import ToolRuleType
|
|
5
6
|
from letta.schemas.letta_base import LettaBase
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class BaseToolRule(LettaBase):
|
|
9
10
|
__id_prefix__ = "tool_rule"
|
|
10
11
|
tool_name: str = Field(..., description="The name of the tool. Must exist in the database for the user's organization.")
|
|
12
|
+
type: ToolRuleType
|
|
11
13
|
|
|
12
14
|
|
|
13
|
-
class
|
|
15
|
+
class ChildToolRule(BaseToolRule):
|
|
14
16
|
"""
|
|
15
17
|
A ToolRule represents a tool that can be invoked by the agent.
|
|
16
18
|
"""
|
|
17
19
|
|
|
18
|
-
type: str = Field("ToolRule")
|
|
20
|
+
# type: str = Field("ToolRule")
|
|
21
|
+
type: ToolRuleType = ToolRuleType.constrain_child_tools
|
|
19
22
|
children: List[str] = Field(..., description="The children tools that can be invoked.")
|
|
20
23
|
|
|
21
24
|
|
|
@@ -24,7 +27,8 @@ class InitToolRule(BaseToolRule):
|
|
|
24
27
|
Represents the initial tool rule configuration.
|
|
25
28
|
"""
|
|
26
29
|
|
|
27
|
-
type: str = Field("InitToolRule")
|
|
30
|
+
# type: str = Field("InitToolRule")
|
|
31
|
+
type: ToolRuleType = ToolRuleType.run_first
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
class TerminalToolRule(BaseToolRule):
|
|
@@ -32,4 +36,8 @@ class TerminalToolRule(BaseToolRule):
|
|
|
32
36
|
Represents a terminal tool rule configuration where if this tool gets called, it must end the agent loop.
|
|
33
37
|
"""
|
|
34
38
|
|
|
35
|
-
type: str = Field("TerminalToolRule")
|
|
39
|
+
# type: str = Field("TerminalToolRule")
|
|
40
|
+
type: ToolRuleType = ToolRuleType.exit_loop
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
ToolRule = Union[ChildToolRule, InitToolRule, TerminalToolRule]
|
letta/server/rest_api/app.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
+
import os
|
|
3
4
|
import sys
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Optional
|
|
@@ -103,7 +104,7 @@ def generate_password():
|
|
|
103
104
|
return secrets.token_urlsafe(16)
|
|
104
105
|
|
|
105
106
|
|
|
106
|
-
random_password = generate_password()
|
|
107
|
+
random_password = os.getenv("LETTA_SERVER_PASSWORD") or generate_password()
|
|
107
108
|
|
|
108
109
|
|
|
109
110
|
class CheckPasswordMiddleware(BaseHTTPMiddleware):
|
|
@@ -132,11 +133,11 @@ def create_application() -> "FastAPI":
|
|
|
132
133
|
debug=True,
|
|
133
134
|
)
|
|
134
135
|
|
|
135
|
-
if "--ade" in sys.argv:
|
|
136
|
+
if (os.getenv("LETTA_SERVER_ADE") == "true") or "--ade" in sys.argv:
|
|
136
137
|
settings.cors_origins.append("https://app.letta.com")
|
|
137
|
-
print(f"▶ View using ADE at: https://app.letta.com/
|
|
138
|
+
print(f"▶ View using ADE at: https://app.letta.com/development-servers/local/dashboard")
|
|
138
139
|
|
|
139
|
-
if "--secure" in sys.argv:
|
|
140
|
+
if (os.getenv("LETTA_SERVER_SECURE") == "true") or "--secure" in sys.argv:
|
|
140
141
|
print(f"▶ Using secure mode with password: {random_password}")
|
|
141
142
|
app.add_middleware(CheckPasswordMiddleware)
|
|
142
143
|
|
|
@@ -271,9 +271,8 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
|
271
271
|
self,
|
|
272
272
|
multi_step=True,
|
|
273
273
|
# Related to if we want to try and pass back the AssistantMessage as a special case function
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
assistant_message_function_kwarg=DEFAULT_MESSAGE_TOOL_KWARG,
|
|
274
|
+
assistant_message_tool_name=DEFAULT_MESSAGE_TOOL,
|
|
275
|
+
assistant_message_tool_kwarg=DEFAULT_MESSAGE_TOOL_KWARG,
|
|
277
276
|
# Related to if we expect inner_thoughts to be in the kwargs
|
|
278
277
|
inner_thoughts_in_kwargs=True,
|
|
279
278
|
inner_thoughts_kwarg=INNER_THOUGHTS_KWARG,
|
|
@@ -287,7 +286,7 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
|
287
286
|
self.streaming_chat_completion_mode_function_name = None # NOTE: sadly need to track state during stream
|
|
288
287
|
# If chat completion mode, we need a special stream reader to
|
|
289
288
|
# turn function argument to send_message into a normal text stream
|
|
290
|
-
self.streaming_chat_completion_json_reader = FunctionArgumentsStreamHandler(json_key=
|
|
289
|
+
self.streaming_chat_completion_json_reader = FunctionArgumentsStreamHandler(json_key=assistant_message_tool_kwarg)
|
|
291
290
|
|
|
292
291
|
self._chunks = deque()
|
|
293
292
|
self._event = asyncio.Event() # Use an event to notify when chunks are available
|
|
@@ -300,9 +299,9 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
|
300
299
|
self.multi_step_gen_indicator = MessageStreamStatus.done_generation
|
|
301
300
|
|
|
302
301
|
# Support for AssistantMessage
|
|
303
|
-
self.use_assistant_message =
|
|
304
|
-
self.
|
|
305
|
-
self.
|
|
302
|
+
self.use_assistant_message = False # TODO: Remove this
|
|
303
|
+
self.assistant_message_tool_name = assistant_message_tool_name
|
|
304
|
+
self.assistant_message_tool_kwarg = assistant_message_tool_kwarg
|
|
306
305
|
|
|
307
306
|
# Support for inner_thoughts_in_kwargs
|
|
308
307
|
self.inner_thoughts_in_kwargs = inner_thoughts_in_kwargs
|
|
@@ -455,17 +454,14 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
|
455
454
|
|
|
456
455
|
# If we get a "hit" on the special keyword we're looking for, we want to skip to the next chunk
|
|
457
456
|
# TODO I don't think this handles the function name in multi-pieces problem. Instead, we should probably reset the streaming_chat_completion_mode_function_name when we make this hit?
|
|
458
|
-
# if self.streaming_chat_completion_mode_function_name == self.
|
|
459
|
-
if tool_call.function.name == self.
|
|
457
|
+
# if self.streaming_chat_completion_mode_function_name == self.assistant_message_tool_name:
|
|
458
|
+
if tool_call.function.name == self.assistant_message_tool_name:
|
|
460
459
|
self.streaming_chat_completion_json_reader.reset()
|
|
461
460
|
# early exit to turn into content mode
|
|
462
461
|
return None
|
|
463
462
|
|
|
464
463
|
# if we're in the middle of parsing a send_message, we'll keep processing the JSON chunks
|
|
465
|
-
if
|
|
466
|
-
tool_call.function.arguments
|
|
467
|
-
and self.streaming_chat_completion_mode_function_name == self.assistant_message_function_name
|
|
468
|
-
):
|
|
464
|
+
if tool_call.function.arguments and self.streaming_chat_completion_mode_function_name == self.assistant_message_tool_name:
|
|
469
465
|
# Strip out any extras tokens
|
|
470
466
|
cleaned_func_args = self.streaming_chat_completion_json_reader.process_json_chunk(tool_call.function.arguments)
|
|
471
467
|
# In the case that we just have the prefix of something, no message yet, then we should early exit to move to the next chunk
|
|
@@ -500,9 +496,6 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
|
500
496
|
)
|
|
501
497
|
|
|
502
498
|
elif self.inner_thoughts_in_kwargs and tool_call.function:
|
|
503
|
-
if self.use_assistant_message:
|
|
504
|
-
raise NotImplementedError("inner_thoughts_in_kwargs with use_assistant_message not yet supported")
|
|
505
|
-
|
|
506
499
|
processed_chunk = None
|
|
507
500
|
|
|
508
501
|
if tool_call.function.name:
|
|
@@ -909,13 +902,13 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
|
909
902
|
|
|
910
903
|
if (
|
|
911
904
|
self.use_assistant_message
|
|
912
|
-
and function_call.function.name == self.
|
|
913
|
-
and self.
|
|
905
|
+
and function_call.function.name == self.assistant_message_tool_name
|
|
906
|
+
and self.assistant_message_tool_kwarg in func_args
|
|
914
907
|
):
|
|
915
908
|
processed_chunk = AssistantMessage(
|
|
916
909
|
id=msg_obj.id,
|
|
917
910
|
date=msg_obj.created_at,
|
|
918
|
-
assistant_message=func_args[self.
|
|
911
|
+
assistant_message=func_args[self.assistant_message_tool_kwarg],
|
|
919
912
|
)
|
|
920
913
|
else:
|
|
921
914
|
processed_chunk = FunctionCallMessage(
|
|
@@ -117,7 +117,7 @@ def create_message(
|
|
|
117
117
|
tool_call_id=None,
|
|
118
118
|
name=None,
|
|
119
119
|
)
|
|
120
|
-
agent = server.
|
|
120
|
+
agent = server.load_agent(agent_id=agent_id)
|
|
121
121
|
# add message to agent
|
|
122
122
|
agent._append_to_messages([message])
|
|
123
123
|
|
|
@@ -161,7 +161,6 @@ def list_messages(
|
|
|
161
161
|
before=before_uuid,
|
|
162
162
|
order_by="created_at",
|
|
163
163
|
reverse=reverse,
|
|
164
|
-
return_message_object=True,
|
|
165
164
|
)
|
|
166
165
|
assert isinstance(json_messages, List)
|
|
167
166
|
assert all([isinstance(message, Message) for message in json_messages])
|
|
@@ -247,7 +246,7 @@ def create_run(
|
|
|
247
246
|
# TODO: add request.instructions as a message?
|
|
248
247
|
agent_id = thread_id
|
|
249
248
|
# TODO: override preset of agent with request.assistant_id
|
|
250
|
-
agent = server.
|
|
249
|
+
agent = server.load_agent(agent_id=agent_id)
|
|
251
250
|
agent.inner_step(messages=[]) # already has messages added
|
|
252
251
|
run_id = str(uuid.uuid4())
|
|
253
252
|
create_time = int(get_utc_time().timestamp())
|
|
@@ -68,7 +68,6 @@ async def create_chat_completion(
|
|
|
68
68
|
stream_tokens=True,
|
|
69
69
|
# Turn on ChatCompletion mode (eg remaps send_message to content)
|
|
70
70
|
chat_completion_mode=True,
|
|
71
|
-
return_message_object=False,
|
|
72
71
|
)
|
|
73
72
|
|
|
74
73
|
else:
|
|
@@ -86,7 +85,6 @@ async def create_chat_completion(
|
|
|
86
85
|
# Turn streaming OFF
|
|
87
86
|
stream_steps=False,
|
|
88
87
|
stream_tokens=False,
|
|
89
|
-
return_message_object=False,
|
|
90
88
|
)
|
|
91
89
|
# print(response_messages)
|
|
92
90
|
|
|
@@ -3,15 +3,10 @@ from letta.server.rest_api.routers.v1.blocks import router as blocks_router
|
|
|
3
3
|
from letta.server.rest_api.routers.v1.health import router as health_router
|
|
4
4
|
from letta.server.rest_api.routers.v1.jobs import router as jobs_router
|
|
5
5
|
from letta.server.rest_api.routers.v1.llms import router as llm_router
|
|
6
|
+
from letta.server.rest_api.routers.v1.sandbox_configs import (
|
|
7
|
+
router as sandbox_configs_router,
|
|
8
|
+
)
|
|
6
9
|
from letta.server.rest_api.routers.v1.sources import router as sources_router
|
|
7
10
|
from letta.server.rest_api.routers.v1.tools import router as tools_router
|
|
8
11
|
|
|
9
|
-
ROUTERS = [
|
|
10
|
-
tools_router,
|
|
11
|
-
sources_router,
|
|
12
|
-
agents_router,
|
|
13
|
-
llm_router,
|
|
14
|
-
blocks_router,
|
|
15
|
-
jobs_router,
|
|
16
|
-
health_router,
|
|
17
|
-
]
|
|
12
|
+
ROUTERS = [tools_router, sources_router, agents_router, llm_router, blocks_router, jobs_router, health_router, sandbox_configs_router]
|