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.

Files changed (36) hide show
  1. letta/agent.py +23 -3
  2. letta/agent_store/db.py +1 -1
  3. letta/client/client.py +290 -0
  4. letta/constants.py +5 -14
  5. letta/functions/helpers.py +0 -4
  6. letta/functions/schema_generator.py +24 -4
  7. letta/local_llm/utils.py +6 -3
  8. letta/log.py +7 -9
  9. letta/orm/__init__.py +2 -0
  10. letta/orm/block.py +5 -2
  11. letta/orm/blocks_agents.py +29 -0
  12. letta/orm/mixins.py +8 -0
  13. letta/orm/organization.py +8 -1
  14. letta/orm/sandbox_config.py +56 -0
  15. letta/orm/sqlalchemy_base.py +9 -3
  16. letta/schemas/blocks_agents.py +32 -0
  17. letta/schemas/letta_base.py +9 -0
  18. letta/schemas/memory.py +9 -8
  19. letta/schemas/sandbox_config.py +114 -0
  20. letta/server/rest_api/routers/v1/__init__.py +4 -9
  21. letta/server/rest_api/routers/v1/sandbox_configs.py +108 -0
  22. letta/server/rest_api/routers/v1/tools.py +3 -5
  23. letta/server/rest_api/utils.py +6 -0
  24. letta/server/server.py +10 -5
  25. letta/services/block_manager.py +4 -2
  26. letta/services/blocks_agents_manager.py +84 -0
  27. letta/services/sandbox_config_manager.py +256 -0
  28. letta/services/tool_execution_sandbox.py +326 -0
  29. letta/services/tool_manager.py +10 -10
  30. letta/services/tool_sandbox_env/.gitkeep +0 -0
  31. letta/settings.py +4 -0
  32. {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241124104049.dist-info}/METADATA +3 -1
  33. {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241124104049.dist-info}/RECORD +36 -27
  34. {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241124104049.dist-info}/LICENSE +0 -0
  35. {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241124104049.dist-info}/WHEEL +0 -0
  36. {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
- function_args["self"] = self # need to attach self to arg since it's dynamically linked
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
- # Tools
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 = 2000
138
- CORE_MEMORY_HUMAN_CHAR_LIMIT = 2000
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"}
@@ -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
- list[str]: "array",
46
- # Add more mappings as needed
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
- print("Warning: model not found. Using cl100k_base encoding.")
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
- function_tokens += len(encoding.encode(function["description"]))
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
- raise ValueError(f"Function {function['name']} has no description, function: {function}")
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": True,
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
- "handlers": [
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=2000, doc="Character limit of the block.")
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"))