letta-nightly 0.5.4.dev20241122104229__py3-none-any.whl → 0.5.4.dev20241124104049__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/agent.py +23 -3
- letta/agent_store/db.py +1 -1
- letta/client/client.py +290 -0
- letta/constants.py +5 -14
- letta/functions/helpers.py +0 -4
- letta/functions/schema_generator.py +24 -4
- letta/local_llm/utils.py +6 -3
- letta/log.py +7 -9
- letta/orm/__init__.py +2 -0
- letta/orm/block.py +5 -2
- letta/orm/blocks_agents.py +29 -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 +9 -3
- letta/schemas/blocks_agents.py +32 -0
- letta/schemas/letta_base.py +9 -0
- letta/schemas/memory.py +9 -8
- letta/schemas/sandbox_config.py +114 -0
- letta/server/rest_api/routers/v1/__init__.py +4 -9
- letta/server/rest_api/routers/v1/sandbox_configs.py +108 -0
- letta/server/rest_api/routers/v1/tools.py +3 -5
- letta/server/rest_api/utils.py +6 -0
- letta/server/server.py +10 -5
- letta/services/block_manager.py +4 -2
- letta/services/blocks_agents_manager.py +84 -0
- letta/services/sandbox_config_manager.py +256 -0
- letta/services/tool_execution_sandbox.py +326 -0
- letta/services/tool_manager.py +10 -10
- letta/services/tool_sandbox_env/.gitkeep +0 -0
- letta/settings.py +4 -0
- {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241124104049.dist-info}/METADATA +3 -1
- {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241124104049.dist-info}/RECORD +36 -27
- {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241124104049.dist-info}/LICENSE +0 -0
- {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241124104049.dist-info}/WHEEL +0 -0
- {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241124104049.dist-info}/entry_points.txt +0 -0
letta/agent.py
CHANGED
|
@@ -9,6 +9,7 @@ from tqdm import tqdm
|
|
|
9
9
|
|
|
10
10
|
from letta.agent_store.storage import StorageConnector
|
|
11
11
|
from letta.constants import (
|
|
12
|
+
BASE_TOOLS,
|
|
12
13
|
CLI_WARNING_PREFIX,
|
|
13
14
|
FIRST_MESSAGE_ATTEMPTS,
|
|
14
15
|
FUNC_FAILED_HEARTBEAT_MESSAGE,
|
|
@@ -49,6 +50,7 @@ from letta.schemas.tool_rule import TerminalToolRule
|
|
|
49
50
|
from letta.schemas.usage import LettaUsageStatistics
|
|
50
51
|
from letta.services.block_manager import BlockManager
|
|
51
52
|
from letta.services.source_manager import SourceManager
|
|
53
|
+
from letta.services.tool_execution_sandbox import ToolExecutionSandbox
|
|
52
54
|
from letta.services.user_manager import UserManager
|
|
53
55
|
from letta.streaming_interface import StreamingRefreshCLIInterface
|
|
54
56
|
from letta.system import (
|
|
@@ -725,9 +727,27 @@ class Agent(BaseAgent):
|
|
|
725
727
|
if isinstance(function_args[name], dict):
|
|
726
728
|
function_args[name] = spec[name](**function_args[name])
|
|
727
729
|
|
|
728
|
-
|
|
730
|
+
# TODO: This needs to be rethought, how do we allow functions that modify agent state/db?
|
|
731
|
+
# TODO: There should probably be two types of tools: stateless/stateful
|
|
732
|
+
|
|
733
|
+
if function_name in BASE_TOOLS:
|
|
734
|
+
function_args["self"] = self # need to attach self to arg since it's dynamically linked
|
|
735
|
+
function_response = function_to_call(**function_args)
|
|
736
|
+
else:
|
|
737
|
+
# execute tool in a sandbox
|
|
738
|
+
# TODO: allow agent_state to specify which sandbox to execute tools in
|
|
739
|
+
sandbox_run_result = ToolExecutionSandbox(function_name, function_args, self.agent_state.user_id).run(
|
|
740
|
+
agent_state=self.agent_state
|
|
741
|
+
)
|
|
742
|
+
function_response, updated_agent_state = sandbox_run_result.func_return, sandbox_run_result.agent_state
|
|
743
|
+
# update agent state
|
|
744
|
+
if self.agent_state != updated_agent_state and updated_agent_state is not None:
|
|
745
|
+
self.agent_state = updated_agent_state
|
|
746
|
+
self.memory = self.agent_state.memory # TODO: don't duplicate
|
|
747
|
+
|
|
748
|
+
# rebuild memory
|
|
749
|
+
self.rebuild_memory()
|
|
729
750
|
|
|
730
|
-
function_response = function_to_call(**function_args)
|
|
731
751
|
if function_name in ["conversation_search", "conversation_search_date", "archival_memory_search"]:
|
|
732
752
|
# with certain functions we rely on the paging mechanism to handle overflow
|
|
733
753
|
truncate = False
|
|
@@ -747,6 +767,7 @@ class Agent(BaseAgent):
|
|
|
747
767
|
error_msg_user = f"{error_msg}\n{traceback.format_exc()}"
|
|
748
768
|
printd(error_msg_user)
|
|
749
769
|
function_response = package_function_response(False, error_msg)
|
|
770
|
+
# TODO: truncate error message somehow
|
|
750
771
|
messages.append(
|
|
751
772
|
Message.dict_to_message(
|
|
752
773
|
agent_id=self.agent_state.id,
|
|
@@ -1284,7 +1305,6 @@ class Agent(BaseAgent):
|
|
|
1284
1305
|
assert isinstance(new_system_prompt, str)
|
|
1285
1306
|
|
|
1286
1307
|
if new_system_prompt == self.system:
|
|
1287
|
-
input("same???")
|
|
1288
1308
|
return
|
|
1289
1309
|
|
|
1290
1310
|
self.system = new_system_prompt
|
letta/agent_store/db.py
CHANGED
|
@@ -433,7 +433,7 @@ class PostgresStorageConnector(SQLStorageConnector):
|
|
|
433
433
|
else:
|
|
434
434
|
db_record = self.db_model(**record.dict())
|
|
435
435
|
session.add(db_record)
|
|
436
|
-
print(f"Added record with id {record.id}")
|
|
436
|
+
# print(f"Added record with id {record.id}")
|
|
437
437
|
session.commit()
|
|
438
438
|
|
|
439
439
|
added_ids.append(record.id)
|
letta/client/client.py
CHANGED
|
@@ -39,6 +39,16 @@ from letta.schemas.message import Message, MessageCreate, UpdateMessage
|
|
|
39
39
|
from letta.schemas.openai.chat_completions import ToolCall
|
|
40
40
|
from letta.schemas.organization import Organization
|
|
41
41
|
from letta.schemas.passage import Passage
|
|
42
|
+
from letta.schemas.sandbox_config import (
|
|
43
|
+
E2BSandboxConfig,
|
|
44
|
+
LocalSandboxConfig,
|
|
45
|
+
SandboxConfig,
|
|
46
|
+
SandboxConfigCreate,
|
|
47
|
+
SandboxConfigUpdate,
|
|
48
|
+
SandboxEnvironmentVariable,
|
|
49
|
+
SandboxEnvironmentVariableCreate,
|
|
50
|
+
SandboxEnvironmentVariableUpdate,
|
|
51
|
+
)
|
|
42
52
|
from letta.schemas.source import Source, SourceCreate, SourceUpdate
|
|
43
53
|
from letta.schemas.tool import Tool, ToolCreate, ToolUpdate
|
|
44
54
|
from letta.schemas.tool_rule import BaseToolRule
|
|
@@ -296,6 +306,112 @@ class AbstractClient(object):
|
|
|
296
306
|
def delete_org(self, org_id: str) -> Organization:
|
|
297
307
|
raise NotImplementedError
|
|
298
308
|
|
|
309
|
+
def create_sandbox_config(self, config: Union[LocalSandboxConfig, E2BSandboxConfig]) -> SandboxConfig:
|
|
310
|
+
"""
|
|
311
|
+
Create a new sandbox configuration.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
config (Union[LocalSandboxConfig, E2BSandboxConfig]): The sandbox settings.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
SandboxConfig: The created sandbox configuration.
|
|
318
|
+
"""
|
|
319
|
+
raise NotImplementedError
|
|
320
|
+
|
|
321
|
+
def update_sandbox_config(self, sandbox_config_id: str, config: Union[LocalSandboxConfig, E2BSandboxConfig]) -> SandboxConfig:
|
|
322
|
+
"""
|
|
323
|
+
Update an existing sandbox configuration.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
sandbox_config_id (str): The ID of the sandbox configuration to update.
|
|
327
|
+
config (Union[LocalSandboxConfig, E2BSandboxConfig]): The updated sandbox settings.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
SandboxConfig: The updated sandbox configuration.
|
|
331
|
+
"""
|
|
332
|
+
raise NotImplementedError
|
|
333
|
+
|
|
334
|
+
def delete_sandbox_config(self, sandbox_config_id: str) -> None:
|
|
335
|
+
"""
|
|
336
|
+
Delete a sandbox configuration.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
sandbox_config_id (str): The ID of the sandbox configuration to delete.
|
|
340
|
+
"""
|
|
341
|
+
raise NotImplementedError
|
|
342
|
+
|
|
343
|
+
def list_sandbox_configs(self, limit: int = 50, cursor: Optional[str] = None) -> List[SandboxConfig]:
|
|
344
|
+
"""
|
|
345
|
+
List all sandbox configurations.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
limit (int, optional): The maximum number of sandbox configurations to return. Defaults to 50.
|
|
349
|
+
cursor (Optional[str], optional): The pagination cursor for retrieving the next set of results.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
List[SandboxConfig]: A list of sandbox configurations.
|
|
353
|
+
"""
|
|
354
|
+
raise NotImplementedError
|
|
355
|
+
|
|
356
|
+
def create_sandbox_env_var(
|
|
357
|
+
self, sandbox_config_id: str, key: str, value: str, description: Optional[str] = None
|
|
358
|
+
) -> SandboxEnvironmentVariable:
|
|
359
|
+
"""
|
|
360
|
+
Create a new environment variable for a sandbox configuration.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
sandbox_config_id (str): The ID of the sandbox configuration to associate the environment variable with.
|
|
364
|
+
key (str): The name of the environment variable.
|
|
365
|
+
value (str): The value of the environment variable.
|
|
366
|
+
description (Optional[str], optional): A description of the environment variable. Defaults to None.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
SandboxEnvironmentVariable: The created environment variable.
|
|
370
|
+
"""
|
|
371
|
+
raise NotImplementedError
|
|
372
|
+
|
|
373
|
+
def update_sandbox_env_var(
|
|
374
|
+
self, env_var_id: str, key: Optional[str] = None, value: Optional[str] = None, description: Optional[str] = None
|
|
375
|
+
) -> SandboxEnvironmentVariable:
|
|
376
|
+
"""
|
|
377
|
+
Update an existing environment variable.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
env_var_id (str): The ID of the environment variable to update.
|
|
381
|
+
key (Optional[str], optional): The updated name of the environment variable. Defaults to None.
|
|
382
|
+
value (Optional[str], optional): The updated value of the environment variable. Defaults to None.
|
|
383
|
+
description (Optional[str], optional): The updated description of the environment variable. Defaults to None.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
SandboxEnvironmentVariable: The updated environment variable.
|
|
387
|
+
"""
|
|
388
|
+
raise NotImplementedError
|
|
389
|
+
|
|
390
|
+
def delete_sandbox_env_var(self, env_var_id: str) -> None:
|
|
391
|
+
"""
|
|
392
|
+
Delete an environment variable by its ID.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
env_var_id (str): The ID of the environment variable to delete.
|
|
396
|
+
"""
|
|
397
|
+
raise NotImplementedError
|
|
398
|
+
|
|
399
|
+
def list_sandbox_env_vars(
|
|
400
|
+
self, sandbox_config_id: str, limit: int = 50, cursor: Optional[str] = None
|
|
401
|
+
) -> List[SandboxEnvironmentVariable]:
|
|
402
|
+
"""
|
|
403
|
+
List all environment variables associated with a sandbox configuration.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
sandbox_config_id (str): The ID of the sandbox configuration to retrieve environment variables for.
|
|
407
|
+
limit (int, optional): The maximum number of environment variables to return. Defaults to 50.
|
|
408
|
+
cursor (Optional[str], optional): The pagination cursor for retrieving the next set of results.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
List[SandboxEnvironmentVariable]: A list of environment variables.
|
|
412
|
+
"""
|
|
413
|
+
raise NotImplementedError
|
|
414
|
+
|
|
299
415
|
|
|
300
416
|
class RESTClient(AbstractClient):
|
|
301
417
|
"""
|
|
@@ -1565,6 +1681,114 @@ class RESTClient(AbstractClient):
|
|
|
1565
1681
|
# Parse and return the deleted organization
|
|
1566
1682
|
return Organization(**response.json())
|
|
1567
1683
|
|
|
1684
|
+
def create_sandbox_config(self, config: Union[LocalSandboxConfig, E2BSandboxConfig]) -> SandboxConfig:
|
|
1685
|
+
"""
|
|
1686
|
+
Create a new sandbox configuration.
|
|
1687
|
+
"""
|
|
1688
|
+
payload = {
|
|
1689
|
+
"config": config.model_dump(),
|
|
1690
|
+
}
|
|
1691
|
+
response = requests.post(f"{self.base_url}/{self.api_prefix}/sandbox-config", headers=self.headers, json=payload)
|
|
1692
|
+
if response.status_code != 200:
|
|
1693
|
+
raise ValueError(f"Failed to create sandbox config: {response.text}")
|
|
1694
|
+
return SandboxConfig(**response.json())
|
|
1695
|
+
|
|
1696
|
+
def update_sandbox_config(self, sandbox_config_id: str, config: Union[LocalSandboxConfig, E2BSandboxConfig]) -> SandboxConfig:
|
|
1697
|
+
"""
|
|
1698
|
+
Update an existing sandbox configuration.
|
|
1699
|
+
"""
|
|
1700
|
+
payload = {
|
|
1701
|
+
"config": config.model_dump(),
|
|
1702
|
+
}
|
|
1703
|
+
response = requests.patch(
|
|
1704
|
+
f"{self.base_url}/{self.api_prefix}/sandbox-config/{sandbox_config_id}",
|
|
1705
|
+
headers=self.headers,
|
|
1706
|
+
json=payload,
|
|
1707
|
+
)
|
|
1708
|
+
if response.status_code != 200:
|
|
1709
|
+
raise ValueError(f"Failed to update sandbox config with ID '{sandbox_config_id}': {response.text}")
|
|
1710
|
+
return SandboxConfig(**response.json())
|
|
1711
|
+
|
|
1712
|
+
def delete_sandbox_config(self, sandbox_config_id: str) -> None:
|
|
1713
|
+
"""
|
|
1714
|
+
Delete a sandbox configuration.
|
|
1715
|
+
"""
|
|
1716
|
+
response = requests.delete(f"{self.base_url}/{self.api_prefix}/sandbox-config/{sandbox_config_id}", headers=self.headers)
|
|
1717
|
+
if response.status_code == 404:
|
|
1718
|
+
raise ValueError(f"Sandbox config with ID '{sandbox_config_id}' does not exist")
|
|
1719
|
+
elif response.status_code != 204:
|
|
1720
|
+
raise ValueError(f"Failed to delete sandbox config with ID '{sandbox_config_id}': {response.text}")
|
|
1721
|
+
|
|
1722
|
+
def list_sandbox_configs(self, limit: int = 50, cursor: Optional[str] = None) -> List[SandboxConfig]:
|
|
1723
|
+
"""
|
|
1724
|
+
List all sandbox configurations.
|
|
1725
|
+
"""
|
|
1726
|
+
params = {"limit": limit, "cursor": cursor}
|
|
1727
|
+
response = requests.get(f"{self.base_url}/{self.api_prefix}/sandbox-config", headers=self.headers, params=params)
|
|
1728
|
+
if response.status_code != 200:
|
|
1729
|
+
raise ValueError(f"Failed to list sandbox configs: {response.text}")
|
|
1730
|
+
return [SandboxConfig(**config_data) for config_data in response.json()]
|
|
1731
|
+
|
|
1732
|
+
def create_sandbox_env_var(
|
|
1733
|
+
self, sandbox_config_id: str, key: str, value: str, description: Optional[str] = None
|
|
1734
|
+
) -> SandboxEnvironmentVariable:
|
|
1735
|
+
"""
|
|
1736
|
+
Create a new environment variable for a sandbox configuration.
|
|
1737
|
+
"""
|
|
1738
|
+
payload = {"key": key, "value": value, "description": description}
|
|
1739
|
+
response = requests.post(
|
|
1740
|
+
f"{self.base_url}/{self.api_prefix}/sandbox-config/{sandbox_config_id}/environment-variable",
|
|
1741
|
+
headers=self.headers,
|
|
1742
|
+
json=payload,
|
|
1743
|
+
)
|
|
1744
|
+
if response.status_code != 200:
|
|
1745
|
+
raise ValueError(f"Failed to create environment variable for sandbox config ID '{sandbox_config_id}': {response.text}")
|
|
1746
|
+
return SandboxEnvironmentVariable(**response.json())
|
|
1747
|
+
|
|
1748
|
+
def update_sandbox_env_var(
|
|
1749
|
+
self, env_var_id: str, key: Optional[str] = None, value: Optional[str] = None, description: Optional[str] = None
|
|
1750
|
+
) -> SandboxEnvironmentVariable:
|
|
1751
|
+
"""
|
|
1752
|
+
Update an existing environment variable.
|
|
1753
|
+
"""
|
|
1754
|
+
payload = {k: v for k, v in {"key": key, "value": value, "description": description}.items() if v is not None}
|
|
1755
|
+
response = requests.patch(
|
|
1756
|
+
f"{self.base_url}/{self.api_prefix}/sandbox-config/environment-variable/{env_var_id}",
|
|
1757
|
+
headers=self.headers,
|
|
1758
|
+
json=payload,
|
|
1759
|
+
)
|
|
1760
|
+
if response.status_code != 200:
|
|
1761
|
+
raise ValueError(f"Failed to update environment variable with ID '{env_var_id}': {response.text}")
|
|
1762
|
+
return SandboxEnvironmentVariable(**response.json())
|
|
1763
|
+
|
|
1764
|
+
def delete_sandbox_env_var(self, env_var_id: str) -> None:
|
|
1765
|
+
"""
|
|
1766
|
+
Delete an environment variable by its ID.
|
|
1767
|
+
"""
|
|
1768
|
+
response = requests.delete(
|
|
1769
|
+
f"{self.base_url}/{self.api_prefix}/sandbox-config/environment-variable/{env_var_id}", headers=self.headers
|
|
1770
|
+
)
|
|
1771
|
+
if response.status_code == 404:
|
|
1772
|
+
raise ValueError(f"Environment variable with ID '{env_var_id}' does not exist")
|
|
1773
|
+
elif response.status_code != 204:
|
|
1774
|
+
raise ValueError(f"Failed to delete environment variable with ID '{env_var_id}': {response.text}")
|
|
1775
|
+
|
|
1776
|
+
def list_sandbox_env_vars(
|
|
1777
|
+
self, sandbox_config_id: str, limit: int = 50, cursor: Optional[str] = None
|
|
1778
|
+
) -> List[SandboxEnvironmentVariable]:
|
|
1779
|
+
"""
|
|
1780
|
+
List all environment variables associated with a sandbox configuration.
|
|
1781
|
+
"""
|
|
1782
|
+
params = {"limit": limit, "cursor": cursor}
|
|
1783
|
+
response = requests.get(
|
|
1784
|
+
f"{self.base_url}/{self.api_prefix}/sandbox-config/{sandbox_config_id}/environment-variable",
|
|
1785
|
+
headers=self.headers,
|
|
1786
|
+
params=params,
|
|
1787
|
+
)
|
|
1788
|
+
if response.status_code != 200:
|
|
1789
|
+
raise ValueError(f"Failed to list environment variables for sandbox config ID '{sandbox_config_id}': {response.text}")
|
|
1790
|
+
return [SandboxEnvironmentVariable(**var_data) for var_data in response.json()]
|
|
1791
|
+
|
|
1568
1792
|
def update_agent_memory_label(self, agent_id: str, current_label: str, new_label: str) -> Memory:
|
|
1569
1793
|
|
|
1570
1794
|
# @router.patch("/{agent_id}/memory/label", response_model=Memory, operation_id="update_agent_memory_label")
|
|
@@ -2821,6 +3045,72 @@ class LocalClient(AbstractClient):
|
|
|
2821
3045
|
def delete_org(self, org_id: str) -> Organization:
|
|
2822
3046
|
return self.server.organization_manager.delete_organization_by_id(org_id=org_id)
|
|
2823
3047
|
|
|
3048
|
+
def create_sandbox_config(self, config: Union[LocalSandboxConfig, E2BSandboxConfig]) -> SandboxConfig:
|
|
3049
|
+
"""
|
|
3050
|
+
Create a new sandbox configuration.
|
|
3051
|
+
"""
|
|
3052
|
+
config_create = SandboxConfigCreate(config=config)
|
|
3053
|
+
return self.server.sandbox_config_manager.create_or_update_sandbox_config(sandbox_config_create=config_create, actor=self.user)
|
|
3054
|
+
|
|
3055
|
+
def update_sandbox_config(self, sandbox_config_id: str, config: Union[LocalSandboxConfig, E2BSandboxConfig]) -> SandboxConfig:
|
|
3056
|
+
"""
|
|
3057
|
+
Update an existing sandbox configuration.
|
|
3058
|
+
"""
|
|
3059
|
+
sandbox_update = SandboxConfigUpdate(config=config)
|
|
3060
|
+
return self.server.sandbox_config_manager.update_sandbox_config(
|
|
3061
|
+
sandbox_config_id=sandbox_config_id, sandbox_update=sandbox_update, actor=self.user
|
|
3062
|
+
)
|
|
3063
|
+
|
|
3064
|
+
def delete_sandbox_config(self, sandbox_config_id: str) -> None:
|
|
3065
|
+
"""
|
|
3066
|
+
Delete a sandbox configuration.
|
|
3067
|
+
"""
|
|
3068
|
+
return self.server.sandbox_config_manager.delete_sandbox_config(sandbox_config_id=sandbox_config_id, actor=self.user)
|
|
3069
|
+
|
|
3070
|
+
def list_sandbox_configs(self, limit: int = 50, cursor: Optional[str] = None) -> List[SandboxConfig]:
|
|
3071
|
+
"""
|
|
3072
|
+
List all sandbox configurations.
|
|
3073
|
+
"""
|
|
3074
|
+
return self.server.sandbox_config_manager.list_sandbox_configs(actor=self.user, limit=limit, cursor=cursor)
|
|
3075
|
+
|
|
3076
|
+
def create_sandbox_env_var(
|
|
3077
|
+
self, sandbox_config_id: str, key: str, value: str, description: Optional[str] = None
|
|
3078
|
+
) -> SandboxEnvironmentVariable:
|
|
3079
|
+
"""
|
|
3080
|
+
Create a new environment variable for a sandbox configuration.
|
|
3081
|
+
"""
|
|
3082
|
+
env_var_create = SandboxEnvironmentVariableCreate(key=key, value=value, description=description)
|
|
3083
|
+
return self.server.sandbox_config_manager.create_sandbox_env_var(
|
|
3084
|
+
env_var_create=env_var_create, sandbox_config_id=sandbox_config_id, actor=self.user
|
|
3085
|
+
)
|
|
3086
|
+
|
|
3087
|
+
def update_sandbox_env_var(
|
|
3088
|
+
self, env_var_id: str, key: Optional[str] = None, value: Optional[str] = None, description: Optional[str] = None
|
|
3089
|
+
) -> SandboxEnvironmentVariable:
|
|
3090
|
+
"""
|
|
3091
|
+
Update an existing environment variable.
|
|
3092
|
+
"""
|
|
3093
|
+
env_var_update = SandboxEnvironmentVariableUpdate(key=key, value=value, description=description)
|
|
3094
|
+
return self.server.sandbox_config_manager.update_sandbox_env_var(
|
|
3095
|
+
env_var_id=env_var_id, env_var_update=env_var_update, actor=self.user
|
|
3096
|
+
)
|
|
3097
|
+
|
|
3098
|
+
def delete_sandbox_env_var(self, env_var_id: str) -> None:
|
|
3099
|
+
"""
|
|
3100
|
+
Delete an environment variable by its ID.
|
|
3101
|
+
"""
|
|
3102
|
+
return self.server.sandbox_config_manager.delete_sandbox_env_var(env_var_id=env_var_id, actor=self.user)
|
|
3103
|
+
|
|
3104
|
+
def list_sandbox_env_vars(
|
|
3105
|
+
self, sandbox_config_id: str, limit: int = 50, cursor: Optional[str] = None
|
|
3106
|
+
) -> List[SandboxEnvironmentVariable]:
|
|
3107
|
+
"""
|
|
3108
|
+
List all environment variables associated with a sandbox configuration.
|
|
3109
|
+
"""
|
|
3110
|
+
return self.server.sandbox_config_manager.list_sandbox_env_vars(
|
|
3111
|
+
sandbox_config_id=sandbox_config_id, actor=self.user, limit=limit, cursor=cursor
|
|
3112
|
+
)
|
|
3113
|
+
|
|
2824
3114
|
def update_agent_memory_label(self, agent_id: str, current_label: str, new_label: str) -> Memory:
|
|
2825
3115
|
return self.server.update_agent_memory_label(
|
|
2826
3116
|
user_id=self.user_id, agent_id=agent_id, current_block_label=current_label, new_block_label=new_label
|
letta/constants.py
CHANGED
|
@@ -36,14 +36,8 @@ DEFAULT_PERSONA = "sam_pov"
|
|
|
36
36
|
DEFAULT_HUMAN = "basic"
|
|
37
37
|
DEFAULT_PRESET = "memgpt_chat"
|
|
38
38
|
|
|
39
|
-
#
|
|
40
|
-
BASE_TOOLS = [
|
|
41
|
-
"send_message",
|
|
42
|
-
"conversation_search",
|
|
43
|
-
"conversation_search_date",
|
|
44
|
-
"archival_memory_insert",
|
|
45
|
-
"archival_memory_search",
|
|
46
|
-
]
|
|
39
|
+
# Base tools that cannot be edited, as they access agent state directly
|
|
40
|
+
BASE_TOOLS = ["send_message", "conversation_search", "conversation_search_date", "archival_memory_insert", "archival_memory_search"]
|
|
47
41
|
|
|
48
42
|
# The name of the tool used to send message to the user
|
|
49
43
|
# May not be relevant in cases where the agent has multiple ways to message to user (send_imessage, send_discord_mesasge, ...)
|
|
@@ -134,8 +128,9 @@ MESSAGE_SUMMARY_REQUEST_ACK = "Understood, I will respond with a summary of the
|
|
|
134
128
|
MESSAGE_SUMMARY_TRUNC_KEEP_N_LAST = 3
|
|
135
129
|
|
|
136
130
|
# Default memory limits
|
|
137
|
-
CORE_MEMORY_PERSONA_CHAR_LIMIT =
|
|
138
|
-
CORE_MEMORY_HUMAN_CHAR_LIMIT =
|
|
131
|
+
CORE_MEMORY_PERSONA_CHAR_LIMIT: int = 5000
|
|
132
|
+
CORE_MEMORY_HUMAN_CHAR_LIMIT: int = 5000
|
|
133
|
+
CORE_MEMORY_BLOCK_CHAR_LIMIT: int = 5000
|
|
139
134
|
|
|
140
135
|
# Function return limits
|
|
141
136
|
FUNCTION_RETURN_CHAR_LIMIT = 6000 # ~300 words
|
|
@@ -155,9 +150,5 @@ FUNC_FAILED_HEARTBEAT_MESSAGE = f"{NON_USER_MSG_PREFIX}Function call failed, ret
|
|
|
155
150
|
|
|
156
151
|
RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE = 5
|
|
157
152
|
|
|
158
|
-
# TODO Is this config or constant?
|
|
159
|
-
CORE_MEMORY_PERSONA_CHAR_LIMIT: int = 2000
|
|
160
|
-
CORE_MEMORY_HUMAN_CHAR_LIMIT: int = 2000
|
|
161
|
-
|
|
162
153
|
MAX_FILENAME_LENGTH = 255
|
|
163
154
|
RESERVED_FILENAMES = {"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "LPT1", "LPT2"}
|
letta/functions/helpers.py
CHANGED
|
@@ -13,8 +13,6 @@ def generate_composio_tool_wrapper(action: "ActionType") -> tuple[str, str]:
|
|
|
13
13
|
|
|
14
14
|
wrapper_function_str = f"""
|
|
15
15
|
def {func_name}(**kwargs):
|
|
16
|
-
if 'self' in kwargs:
|
|
17
|
-
del kwargs['self']
|
|
18
16
|
from composio import Action, App, Tag
|
|
19
17
|
from composio_langchain import ComposioToolSet
|
|
20
18
|
|
|
@@ -46,8 +44,6 @@ def generate_langchain_tool_wrapper(
|
|
|
46
44
|
# Combine all parts into the wrapper function
|
|
47
45
|
wrapper_function_str = f"""
|
|
48
46
|
def {func_name}(**kwargs):
|
|
49
|
-
if 'self' in kwargs:
|
|
50
|
-
del kwargs['self']
|
|
51
47
|
import importlib
|
|
52
48
|
{import_statement}
|
|
53
49
|
{extra_module_imports}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import inspect
|
|
2
|
-
from typing import Any, Dict, Optional, Type, Union, get_args, get_origin
|
|
2
|
+
from typing import Any, Dict, List, Optional, Type, Union, get_args, get_origin
|
|
3
3
|
|
|
4
4
|
from docstring_parser import parse
|
|
5
5
|
from pydantic import BaseModel
|
|
@@ -38,15 +38,29 @@ def type_to_json_schema_type(py_type):
|
|
|
38
38
|
|
|
39
39
|
# Mapping of Python types to JSON schema types
|
|
40
40
|
type_map = {
|
|
41
|
+
# Basic types
|
|
41
42
|
int: "integer",
|
|
42
43
|
str: "string",
|
|
43
44
|
bool: "boolean",
|
|
44
45
|
float: "number",
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
# Collections
|
|
47
|
+
List[str]: "array",
|
|
48
|
+
List[int]: "array",
|
|
49
|
+
list: "array",
|
|
50
|
+
tuple: "array",
|
|
51
|
+
set: "array",
|
|
52
|
+
# Dictionaries
|
|
53
|
+
dict: "object",
|
|
54
|
+
Dict[str, Any]: "object",
|
|
55
|
+
# Special types
|
|
56
|
+
None: "null",
|
|
57
|
+
type(None): "null",
|
|
58
|
+
# Optional types
|
|
59
|
+
# Optional[str]: "string", # NOTE: caught above ^
|
|
60
|
+
Union[str, None]: "string",
|
|
47
61
|
}
|
|
48
62
|
if py_type not in type_map:
|
|
49
|
-
raise ValueError(f"Python type {py_type} has no corresponding JSON schema type")
|
|
63
|
+
raise ValueError(f"Python type {py_type} has no corresponding JSON schema type - full map: {type_map}")
|
|
50
64
|
|
|
51
65
|
return type_map.get(py_type, "string") # Default to "string" if type not in map
|
|
52
66
|
|
|
@@ -93,9 +107,14 @@ def generate_schema(function, name: Optional[str] = None, description: Optional[
|
|
|
93
107
|
|
|
94
108
|
for param in sig.parameters.values():
|
|
95
109
|
# Exclude 'self' parameter
|
|
110
|
+
# TODO: eventually remove this (only applies to BASE_TOOLS)
|
|
96
111
|
if param.name == "self":
|
|
97
112
|
continue
|
|
98
113
|
|
|
114
|
+
# exclude 'agent_state' parameter
|
|
115
|
+
if param.name == "agent_state":
|
|
116
|
+
continue
|
|
117
|
+
|
|
99
118
|
# Assert that the parameter has a type annotation
|
|
100
119
|
if param.annotation == inspect.Parameter.empty:
|
|
101
120
|
raise TypeError(f"Parameter '{param.name}' in function '{function.__name__}' lacks a type annotation")
|
|
@@ -129,6 +148,7 @@ def generate_schema(function, name: Optional[str] = None, description: Optional[
|
|
|
129
148
|
|
|
130
149
|
# append the heartbeat
|
|
131
150
|
# TODO: don't hard-code
|
|
151
|
+
# TODO: if terminal, don't include this
|
|
132
152
|
if function.__name__ not in ["send_message", "pause_heartbeats"]:
|
|
133
153
|
schema["parameters"]["properties"]["request_heartbeat"] = {
|
|
134
154
|
"type": "boolean",
|
letta/local_llm/utils.py
CHANGED
|
@@ -88,16 +88,19 @@ def num_tokens_from_functions(functions: List[dict], model: str = "gpt-4"):
|
|
|
88
88
|
try:
|
|
89
89
|
encoding = tiktoken.encoding_for_model(model)
|
|
90
90
|
except KeyError:
|
|
91
|
-
|
|
91
|
+
warnings.warn("Warning: model not found. Using cl100k_base encoding.")
|
|
92
92
|
encoding = tiktoken.get_encoding("cl100k_base")
|
|
93
93
|
|
|
94
94
|
num_tokens = 0
|
|
95
95
|
for function in functions:
|
|
96
96
|
function_tokens = len(encoding.encode(function["name"]))
|
|
97
97
|
if function["description"]:
|
|
98
|
-
|
|
98
|
+
if not isinstance(function["description"], str):
|
|
99
|
+
warnings.warn(f"Function {function['name']} has non-string description: {function['description']}")
|
|
100
|
+
else:
|
|
101
|
+
function_tokens += len(encoding.encode(function["description"]))
|
|
99
102
|
else:
|
|
100
|
-
|
|
103
|
+
warnings.warn(f"Function {function['name']} has no description, function: {function}")
|
|
101
104
|
|
|
102
105
|
if "parameters" in function:
|
|
103
106
|
parameters = function["parameters"]
|
letta/log.py
CHANGED
|
@@ -23,12 +23,10 @@ def _setup_logfile() -> "Path":
|
|
|
23
23
|
# TODO: production logging should be much less invasive
|
|
24
24
|
DEVELOPMENT_LOGGING = {
|
|
25
25
|
"version": 1,
|
|
26
|
-
"disable_existing_loggers":
|
|
26
|
+
"disable_existing_loggers": False, # Allow capturing from all loggers
|
|
27
27
|
"formatters": {
|
|
28
28
|
"standard": {"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"},
|
|
29
|
-
"no_datetime": {
|
|
30
|
-
"format": "%(name)s - %(levelname)s - %(message)s",
|
|
31
|
-
},
|
|
29
|
+
"no_datetime": {"format": "%(name)s - %(levelname)s - %(message)s"},
|
|
32
30
|
},
|
|
33
31
|
"handlers": {
|
|
34
32
|
"console": {
|
|
@@ -46,14 +44,14 @@ DEVELOPMENT_LOGGING = {
|
|
|
46
44
|
"formatter": "standard",
|
|
47
45
|
},
|
|
48
46
|
},
|
|
47
|
+
"root": { # Root logger handles all logs
|
|
48
|
+
"level": logging.DEBUG if settings.debug else logging.INFO,
|
|
49
|
+
"handlers": ["console", "file"],
|
|
50
|
+
},
|
|
49
51
|
"loggers": {
|
|
50
52
|
"Letta": {
|
|
51
53
|
"level": logging.DEBUG if settings.debug else logging.INFO,
|
|
52
|
-
"
|
|
53
|
-
"console",
|
|
54
|
-
"file",
|
|
55
|
-
],
|
|
56
|
-
"propagate": False,
|
|
54
|
+
"propagate": True, # Let logs bubble up to root
|
|
57
55
|
},
|
|
58
56
|
"uvicorn": {
|
|
59
57
|
"level": "CRITICAL",
|
letta/orm/__init__.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from letta.orm.base import Base
|
|
2
2
|
from letta.orm.block import Block
|
|
3
|
+
from letta.orm.blocks_agents import BlocksAgents
|
|
3
4
|
from letta.orm.file import FileMetadata
|
|
4
5
|
from letta.orm.organization import Organization
|
|
6
|
+
from letta.orm.sandbox_config import SandboxConfig, SandboxEnvironmentVariable
|
|
5
7
|
from letta.orm.source import Source
|
|
6
8
|
from letta.orm.tool import Tool
|
|
7
9
|
from letta.orm.user import User
|
letta/orm/block.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING, Optional, Type
|
|
2
2
|
|
|
3
|
-
from sqlalchemy import JSON, BigInteger, Integer
|
|
3
|
+
from sqlalchemy import JSON, BigInteger, Integer, UniqueConstraint
|
|
4
4
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
5
5
|
|
|
6
|
+
from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT
|
|
6
7
|
from letta.orm.mixins import OrganizationMixin
|
|
7
8
|
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
|
8
9
|
from letta.schemas.block import Block as PydanticBlock
|
|
@@ -17,6 +18,8 @@ class Block(OrganizationMixin, SqlalchemyBase):
|
|
|
17
18
|
|
|
18
19
|
__tablename__ = "block"
|
|
19
20
|
__pydantic_model__ = PydanticBlock
|
|
21
|
+
# This may seem redundant, but is necessary for the BlocksAgents composite FK relationship
|
|
22
|
+
__table_args__ = (UniqueConstraint("id", "label", name="unique_block_id_label"),)
|
|
20
23
|
|
|
21
24
|
template_name: Mapped[Optional[str]] = mapped_column(
|
|
22
25
|
nullable=True, doc="the unique name that identifies a block in a human-readable way"
|
|
@@ -27,7 +30,7 @@ class Block(OrganizationMixin, SqlalchemyBase):
|
|
|
27
30
|
doc="whether the block is a template (e.g. saved human/persona options as baselines for other templates)", default=False
|
|
28
31
|
)
|
|
29
32
|
value: Mapped[str] = mapped_column(doc="Text content of the block for the respective section of core memory.")
|
|
30
|
-
limit: Mapped[BigInteger] = mapped_column(Integer, default=
|
|
33
|
+
limit: Mapped[BigInteger] = mapped_column(Integer, default=CORE_MEMORY_BLOCK_CHAR_LIMIT, doc="Character limit of the block.")
|
|
31
34
|
metadata_: Mapped[Optional[dict]] = mapped_column(JSON, default={}, doc="arbitrary information related to the block.")
|
|
32
35
|
|
|
33
36
|
# relationships
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from sqlalchemy import ForeignKey, ForeignKeyConstraint, String, UniqueConstraint
|
|
2
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
3
|
+
|
|
4
|
+
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
|
5
|
+
from letta.schemas.blocks_agents import BlocksAgents as PydanticBlocksAgents
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BlocksAgents(SqlalchemyBase):
|
|
9
|
+
"""Agents must have one or many blocks to make up their core memory."""
|
|
10
|
+
|
|
11
|
+
__tablename__ = "blocks_agents"
|
|
12
|
+
__pydantic_model__ = PydanticBlocksAgents
|
|
13
|
+
__table_args__ = (
|
|
14
|
+
UniqueConstraint(
|
|
15
|
+
"agent_id",
|
|
16
|
+
"block_label",
|
|
17
|
+
name="unique_label_per_agent",
|
|
18
|
+
),
|
|
19
|
+
ForeignKeyConstraint(
|
|
20
|
+
["block_id", "block_label"],
|
|
21
|
+
["block.id", "block.label"],
|
|
22
|
+
name="fk_block_id_label",
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# unique agent + block label
|
|
27
|
+
agent_id: Mapped[str] = mapped_column(String, ForeignKey("agents.id"), primary_key=True)
|
|
28
|
+
block_id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
29
|
+
block_label: Mapped[str] = mapped_column(String, primary_key=True)
|
letta/orm/mixins.py
CHANGED
|
@@ -37,3 +37,11 @@ class SourceMixin(Base):
|
|
|
37
37
|
__abstract__ = True
|
|
38
38
|
|
|
39
39
|
source_id: Mapped[str] = mapped_column(String, ForeignKey("sources.id"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SandboxConfigMixin(Base):
|
|
43
|
+
"""Mixin for models that belong to a SandboxConfig."""
|
|
44
|
+
|
|
45
|
+
__abstract__ = True
|
|
46
|
+
|
|
47
|
+
sandbox_config_id: Mapped[str] = mapped_column(String, ForeignKey("sandbox_configs.id"))
|