letta-nightly 0.6.19.dev20250130104029__py3-none-any.whl → 0.6.20.dev20250131222205__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 CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.6.19"
1
+ __version__ = "0.6.20"
2
2
 
3
3
 
4
4
  # import clients
@@ -1,7 +1,8 @@
1
1
  import json
2
2
  import re
3
3
  import time
4
- from typing import Generator, List, Optional, Tuple, Union
4
+ import warnings
5
+ from typing import Generator, List, Optional, Union
5
6
 
6
7
  import anthropic
7
8
  from anthropic import PermissionDeniedError
@@ -36,7 +37,7 @@ from letta.schemas.openai.chat_completion_response import MessageDelta, ToolCall
36
37
  from letta.services.provider_manager import ProviderManager
37
38
  from letta.settings import model_settings
38
39
  from letta.streaming_interface import AgentChunkStreamingInterface, AgentRefreshStreamingInterface
39
- from letta.utils import get_utc_time, smart_urljoin
40
+ from letta.utils import get_utc_time
40
41
 
41
42
  BASE_URL = "https://api.anthropic.com/v1"
42
43
 
@@ -567,30 +568,6 @@ def _prepare_anthropic_request(
567
568
  return data
568
569
 
569
570
 
570
- def get_anthropic_endpoint_and_headers(
571
- base_url: str,
572
- api_key: str,
573
- version: str = "2023-06-01",
574
- beta: Optional[str] = "tools-2024-04-04",
575
- ) -> Tuple[str, dict]:
576
- """
577
- Dynamically generate the Anthropic endpoint and headers.
578
- """
579
- url = smart_urljoin(base_url, "messages")
580
-
581
- headers = {
582
- "Content-Type": "application/json",
583
- "x-api-key": api_key,
584
- "anthropic-version": version,
585
- }
586
-
587
- # Add beta header if specified
588
- if beta:
589
- headers["anthropic-beta"] = beta
590
-
591
- return url, headers
592
-
593
-
594
571
  def anthropic_chat_completions_request(
595
572
  data: ChatCompletionRequest,
596
573
  inner_thoughts_xml_tag: Optional[str] = "thinking",
@@ -6,5 +6,5 @@ AZURE_MODEL_TO_CONTEXT_LENGTH = {
6
6
  "gpt-35-turbo-0125": 16385,
7
7
  "gpt-4-0613": 8192,
8
8
  "gpt-4o-mini-2024-07-18": 128000,
9
- "gpt-4o-2024-08-06": 128000,
9
+ "gpt-4o": 128000,
10
10
  }
@@ -29,7 +29,6 @@ from letta.schemas.openai.chat_completion_request import ChatCompletionRequest,
29
29
  from letta.schemas.openai.chat_completion_response import ChatCompletionResponse
30
30
  from letta.settings import ModelSettings
31
31
  from letta.streaming_interface import AgentChunkStreamingInterface, AgentRefreshStreamingInterface
32
- from letta.utils import run_async_task
33
32
 
34
33
  LLM_API_PROVIDER_OPTIONS = ["openai", "azure", "anthropic", "google_ai", "cohere", "local", "groq"]
35
34
 
@@ -57,7 +56,9 @@ def retry_with_exponential_backoff(
57
56
  while True:
58
57
  try:
59
58
  return func(*args, **kwargs)
60
-
59
+ except KeyboardInterrupt:
60
+ # Stop retrying if user hits Ctrl-C
61
+ raise KeyboardInterrupt("User intentionally stopped thread. Stopping...")
61
62
  except requests.exceptions.HTTPError as http_err:
62
63
 
63
64
  if not hasattr(http_err, "response") or not http_err.response:
@@ -142,6 +143,11 @@ def create(
142
143
  if model_settings.openai_api_key is None and llm_config.model_endpoint == "https://api.openai.com/v1":
143
144
  # only is a problem if we are *not* using an openai proxy
144
145
  raise LettaConfigurationError(message="OpenAI key is missing from letta config file", missing_fields=["openai_api_key"])
146
+ elif model_settings.openai_api_key is None:
147
+ # the openai python client requires a dummy API key
148
+ api_key = "DUMMY_API_KEY"
149
+ else:
150
+ api_key = model_settings.openai_api_key
145
151
 
146
152
  if function_call is None and functions is not None and len(functions) > 0:
147
153
  # force function calling for reliability, see https://platform.openai.com/docs/api-reference/chat/create#chat-create-tool_choice
@@ -157,25 +163,21 @@ def create(
157
163
  assert isinstance(stream_interface, AgentChunkStreamingInterface) or isinstance(
158
164
  stream_interface, AgentRefreshStreamingInterface
159
165
  ), type(stream_interface)
160
- response = run_async_task(
161
- openai_chat_completions_process_stream(
162
- url=llm_config.model_endpoint,
163
- api_key=model_settings.openai_api_key,
164
- chat_completion_request=data,
165
- stream_interface=stream_interface,
166
- )
166
+ response = openai_chat_completions_process_stream(
167
+ url=llm_config.model_endpoint,
168
+ api_key=api_key,
169
+ chat_completion_request=data,
170
+ stream_interface=stream_interface,
167
171
  )
168
172
  else: # Client did not request token streaming (expect a blocking backend response)
169
173
  data.stream = False
170
174
  if isinstance(stream_interface, AgentChunkStreamingInterface):
171
175
  stream_interface.stream_start()
172
176
  try:
173
- response = run_async_task(
174
- openai_chat_completions_request(
175
- url=llm_config.model_endpoint,
176
- api_key=model_settings.openai_api_key,
177
- chat_completion_request=data,
178
- )
177
+ response = openai_chat_completions_request(
178
+ url=llm_config.model_endpoint,
179
+ api_key=api_key,
180
+ chat_completion_request=data,
179
181
  )
180
182
  finally:
181
183
  if isinstance(stream_interface, AgentChunkStreamingInterface):
@@ -349,12 +351,10 @@ def create(
349
351
  stream_interface.stream_start()
350
352
  try:
351
353
  # groq uses the openai chat completions API, so this component should be reusable
352
- response = run_async_task(
353
- openai_chat_completions_request(
354
- url=llm_config.model_endpoint,
355
- api_key=model_settings.groq_api_key,
356
- chat_completion_request=data,
357
- )
354
+ response = openai_chat_completions_request(
355
+ url=llm_config.model_endpoint,
356
+ api_key=model_settings.groq_api_key,
357
+ chat_completion_request=data,
358
358
  )
359
359
  finally:
360
360
  if isinstance(stream_interface, AgentChunkStreamingInterface):
letta/llm_api/openai.py CHANGED
@@ -1,8 +1,8 @@
1
1
  import warnings
2
- from typing import AsyncGenerator, List, Optional, Union
2
+ from typing import Generator, List, Optional, Union
3
3
 
4
4
  import requests
5
- from openai import AsyncOpenAI
5
+ from openai import OpenAI
6
6
 
7
7
  from letta.llm_api.helpers import add_inner_thoughts_to_functions, convert_to_structured_output, make_post_request
8
8
  from letta.local_llm.constants import INNER_THOUGHTS_KWARG, INNER_THOUGHTS_KWARG_DESCRIPTION, INNER_THOUGHTS_KWARG_DESCRIPTION_GO_FIRST
@@ -158,7 +158,7 @@ def build_openai_chat_completions_request(
158
158
  return data
159
159
 
160
160
 
161
- async def openai_chat_completions_process_stream(
161
+ def openai_chat_completions_process_stream(
162
162
  url: str,
163
163
  api_key: str,
164
164
  chat_completion_request: ChatCompletionRequest,
@@ -231,7 +231,7 @@ async def openai_chat_completions_process_stream(
231
231
  n_chunks = 0 # approx == n_tokens
232
232
  chunk_idx = 0
233
233
  try:
234
- async for chat_completion_chunk in openai_chat_completions_request_stream(
234
+ for chat_completion_chunk in openai_chat_completions_request_stream(
235
235
  url=url, api_key=api_key, chat_completion_request=chat_completion_request
236
236
  ):
237
237
  assert isinstance(chat_completion_chunk, ChatCompletionChunkResponse), type(chat_completion_chunk)
@@ -382,24 +382,21 @@ async def openai_chat_completions_process_stream(
382
382
  return chat_completion_response
383
383
 
384
384
 
385
- async def openai_chat_completions_request_stream(
385
+ def openai_chat_completions_request_stream(
386
386
  url: str,
387
387
  api_key: str,
388
388
  chat_completion_request: ChatCompletionRequest,
389
- ) -> AsyncGenerator[ChatCompletionChunkResponse, None]:
389
+ ) -> Generator[ChatCompletionChunkResponse, None, None]:
390
390
  data = prepare_openai_payload(chat_completion_request)
391
391
  data["stream"] = True
392
- client = AsyncOpenAI(
393
- api_key=api_key,
394
- base_url=url,
395
- )
396
- stream = await client.chat.completions.create(**data)
397
- async for chunk in stream:
392
+ client = OpenAI(api_key=api_key, base_url=url, max_retries=0)
393
+ stream = client.chat.completions.create(**data)
394
+ for chunk in stream:
398
395
  # TODO: Use the native OpenAI objects here?
399
396
  yield ChatCompletionChunkResponse(**chunk.model_dump(exclude_none=True))
400
397
 
401
398
 
402
- async def openai_chat_completions_request(
399
+ def openai_chat_completions_request(
403
400
  url: str,
404
401
  api_key: str,
405
402
  chat_completion_request: ChatCompletionRequest,
@@ -412,8 +409,8 @@ async def openai_chat_completions_request(
412
409
  https://platform.openai.com/docs/guides/text-generation?lang=curl
413
410
  """
414
411
  data = prepare_openai_payload(chat_completion_request)
415
- client = AsyncOpenAI(api_key=api_key, base_url=url)
416
- chat_completion = await client.chat.completions.create(**data)
412
+ client = OpenAI(api_key=api_key, base_url=url, max_retries=0)
413
+ chat_completion = client.chat.completions.create(**data)
417
414
  return ChatCompletionResponse(**chat_completion.model_dump())
418
415
 
419
416
 
letta/orm/agent.py CHANGED
@@ -56,6 +56,9 @@ class Agent(SqlalchemyBase, OrganizationMixin):
56
56
  embedding_config: Mapped[Optional[EmbeddingConfig]] = mapped_column(
57
57
  EmbeddingConfigColumn, doc="the embedding configuration object for this agent."
58
58
  )
59
+ project_id: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The id of the project the agent belongs to.")
60
+ template_id: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The id of the template the agent belongs to.")
61
+ base_template_id: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The base template id of the agent.")
59
62
 
60
63
  # Tool rules
61
64
  tool_rules: Mapped[Optional[List[ToolRule]]] = mapped_column(ToolRulesColumn, doc="the tool rules for this agent.")
@@ -146,6 +149,9 @@ class Agent(SqlalchemyBase, OrganizationMixin):
146
149
  "created_at": self.created_at,
147
150
  "updated_at": self.updated_at,
148
151
  "tool_exec_environment_variables": self.tool_exec_environment_variables,
152
+ "project_id": self.project_id,
153
+ "template_id": self.template_id,
154
+ "base_template_id": self.base_template_id,
149
155
  }
150
156
 
151
157
  return self.__pydantic_model__(**state)
@@ -85,10 +85,13 @@ class ToolRulesColumn(TypeDecorator):
85
85
  """Deserialize a dictionary to the appropriate ToolRule subclass based on the 'type'."""
86
86
  rule_type = ToolRuleType(data.get("type")) # Remove 'type' field if it exists since it is a class var
87
87
  if rule_type == ToolRuleType.run_first or rule_type == "InitToolRule":
88
+ data["type"] = ToolRuleType.run_first
88
89
  return InitToolRule(**data)
89
90
  elif rule_type == ToolRuleType.exit_loop or rule_type == "TerminalToolRule":
91
+ data["type"] = ToolRuleType.exit_loop
90
92
  return TerminalToolRule(**data)
91
93
  elif rule_type == ToolRuleType.constrain_child_tools or rule_type == "ToolRule":
94
+ data["type"] = ToolRuleType.constrain_child_tools
92
95
  rule = ChildToolRule(**data)
93
96
  return rule
94
97
  elif rule_type == ToolRuleType.conditional:
letta/schemas/agent.py CHANGED
@@ -81,6 +81,9 @@ class AgentState(OrmMetadataBase, validate_assignment=True):
81
81
  tool_exec_environment_variables: List[AgentEnvironmentVariable] = Field(
82
82
  default_factory=list, description="The environment variables for tool execution specific to this agent."
83
83
  )
84
+ project_id: Optional[str] = Field(None, description="The id of the project the agent belongs to.")
85
+ template_id: Optional[str] = Field(None, description="The id of the template the agent belongs to.")
86
+ base_template_id: Optional[str] = Field(None, description="The base template id of the agent.")
84
87
 
85
88
  def get_agent_env_vars_as_dict(self) -> Dict[str, str]:
86
89
  # Get environment variables for this agent specifically
@@ -1,5 +1,6 @@
1
1
  import hashlib
2
2
  import json
3
+ import re
3
4
  from enum import Enum
4
5
  from typing import Any, Dict, List, Literal, Optional, Union
5
6
 
@@ -25,18 +26,55 @@ class SandboxRunResult(BaseModel):
25
26
  sandbox_config_fingerprint: str = Field(None, description="The fingerprint of the config for the sandbox")
26
27
 
27
28
 
29
+ class PipRequirement(BaseModel):
30
+ name: str = Field(..., min_length=1, description="Name of the pip package.")
31
+ version: Optional[str] = Field(None, description="Optional version of the package, following semantic versioning.")
32
+
33
+ @classmethod
34
+ def validate_version(cls, version: Optional[str]) -> Optional[str]:
35
+ if version is None:
36
+ return None
37
+ semver_pattern = re.compile(r"^\d+(\.\d+){0,2}(-[a-zA-Z0-9.]+)?$")
38
+ if not semver_pattern.match(version):
39
+ raise ValueError(f"Invalid version format: {version}. Must follow semantic versioning (e.g., 1.2.3, 2.0, 1.5.0-alpha).")
40
+ return version
41
+
42
+ def __init__(self, **data):
43
+ super().__init__(**data)
44
+ self.version = self.validate_version(self.version)
45
+
46
+
28
47
  class LocalSandboxConfig(BaseModel):
29
- sandbox_dir: str = Field(..., description="Directory for the sandbox environment.")
48
+ sandbox_dir: Optional[str] = Field(None, description="Directory for the sandbox environment.")
30
49
  use_venv: bool = Field(False, description="Whether or not to use the venv, or run directly in the same run loop.")
31
50
  venv_name: str = Field(
32
51
  "venv",
33
52
  description="The name for the venv in the sandbox directory. We first search for an existing venv with this name, otherwise, we make it from the requirements.txt.",
34
53
  )
54
+ pip_requirements: List[PipRequirement] = Field(
55
+ default_factory=list,
56
+ description="List of pip packages to install with mandatory name and optional version following semantic versioning. This only is considered when use_venv is True.",
57
+ )
35
58
 
36
59
  @property
37
60
  def type(self) -> "SandboxType":
38
61
  return SandboxType.LOCAL
39
62
 
63
+ @model_validator(mode="before")
64
+ @classmethod
65
+ def set_default_sandbox_dir(cls, data):
66
+ # If `data` is not a dict (e.g., it's another Pydantic model), just return it
67
+ if not isinstance(data, dict):
68
+ return data
69
+
70
+ if data.get("sandbox_dir") is None:
71
+ if tool_settings.local_sandbox_dir:
72
+ data["sandbox_dir"] = tool_settings.local_sandbox_dir
73
+ else:
74
+ data["sandbox_dir"] = "~/.letta"
75
+
76
+ return data
77
+
40
78
 
41
79
  class E2BSandboxConfig(BaseModel):
42
80
  timeout: int = Field(5 * 60, description="Time limit for the sandbox (in seconds).")
@@ -53,6 +91,10 @@ class E2BSandboxConfig(BaseModel):
53
91
  """
54
92
  Assign a default template value if the template field is not provided.
55
93
  """
94
+ # If `data` is not a dict (e.g., it's another Pydantic model), just return it
95
+ if not isinstance(data, dict):
96
+ return data
97
+
56
98
  if data.get("template") is None:
57
99
  data["template"] = tool_settings.e2b_sandbox_template_id
58
100
  return data
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict, List, Optional, Union
1
+ from typing import Annotated, Any, Dict, List, Literal, Optional, Union
2
2
 
3
3
  from pydantic import Field
4
4
 
@@ -17,7 +17,7 @@ class ChildToolRule(BaseToolRule):
17
17
  A ToolRule represents a tool that can be invoked by the agent.
18
18
  """
19
19
 
20
- type: ToolRuleType = ToolRuleType.constrain_child_tools
20
+ type: Literal[ToolRuleType.constrain_child_tools] = ToolRuleType.constrain_child_tools
21
21
  children: List[str] = Field(..., description="The children tools that can be invoked.")
22
22
 
23
23
 
@@ -26,7 +26,7 @@ class ConditionalToolRule(BaseToolRule):
26
26
  A ToolRule that conditionally maps to different child tools based on the output.
27
27
  """
28
28
 
29
- type: ToolRuleType = ToolRuleType.conditional
29
+ type: Literal[ToolRuleType.conditional] = ToolRuleType.conditional
30
30
  default_child: Optional[str] = Field(None, description="The default child tool to be called. If None, any tool can be called.")
31
31
  child_output_mapping: Dict[Any, str] = Field(..., description="The output case to check for mapping")
32
32
  require_output_mapping: bool = Field(default=False, description="Whether to throw an error when output doesn't match any case")
@@ -37,7 +37,7 @@ class InitToolRule(BaseToolRule):
37
37
  Represents the initial tool rule configuration.
38
38
  """
39
39
 
40
- type: ToolRuleType = ToolRuleType.run_first
40
+ type: Literal[ToolRuleType.run_first] = ToolRuleType.run_first
41
41
 
42
42
 
43
43
  class TerminalToolRule(BaseToolRule):
@@ -45,7 +45,10 @@ class TerminalToolRule(BaseToolRule):
45
45
  Represents a terminal tool rule configuration where if this tool gets called, it must end the agent loop.
46
46
  """
47
47
 
48
- type: ToolRuleType = ToolRuleType.exit_loop
48
+ type: Literal[ToolRuleType.exit_loop] = ToolRuleType.exit_loop
49
49
 
50
50
 
51
- ToolRule = Union[ChildToolRule, InitToolRule, TerminalToolRule, ConditionalToolRule]
51
+ ToolRule = Annotated[
52
+ Union[ChildToolRule, InitToolRule, TerminalToolRule, ConditionalToolRule],
53
+ Field(discriminator="type"),
54
+ ]
@@ -97,7 +97,10 @@ class CheckPasswordMiddleware(BaseHTTPMiddleware):
97
97
  if request.url.path == "/v1/health/" or request.url.path == "/latest/health/":
98
98
  return await call_next(request)
99
99
 
100
- if request.headers.get("X-BARE-PASSWORD") == f"password {random_password}":
100
+ if (
101
+ request.headers.get("X-BARE-PASSWORD") == f"password {random_password}"
102
+ or request.headers.get("Authorization") == f"Bearer {random_password}"
103
+ ):
101
104
  return await call_next(request)
102
105
 
103
106
  return JSONResponse(
@@ -7,6 +7,7 @@ from letta.server.rest_api.routers.v1.providers import router as providers_route
7
7
  from letta.server.rest_api.routers.v1.runs import router as runs_router
8
8
  from letta.server.rest_api.routers.v1.sandbox_configs import router as sandbox_configs_router
9
9
  from letta.server.rest_api.routers.v1.sources import router as sources_router
10
+ from letta.server.rest_api.routers.v1.steps import router as steps_router
10
11
  from letta.server.rest_api.routers.v1.tags import router as tags_router
11
12
  from letta.server.rest_api.routers.v1.tools import router as tools_router
12
13
 
@@ -21,5 +22,6 @@ ROUTERS = [
21
22
  sandbox_configs_router,
22
23
  providers_router,
23
24
  runs_router,
25
+ steps_router,
24
26
  tags_router,
25
27
  ]
@@ -1,16 +1,22 @@
1
+ import os
2
+ import shutil
1
3
  from typing import List, Optional
2
4
 
3
- from fastapi import APIRouter, Depends, Query
5
+ from fastapi import APIRouter, Depends, HTTPException, Query
4
6
 
7
+ from letta.log import get_logger
5
8
  from letta.schemas.environment_variables import SandboxEnvironmentVariable as PydanticEnvVar
6
9
  from letta.schemas.environment_variables import SandboxEnvironmentVariableCreate, SandboxEnvironmentVariableUpdate
10
+ from letta.schemas.sandbox_config import LocalSandboxConfig
7
11
  from letta.schemas.sandbox_config import SandboxConfig as PydanticSandboxConfig
8
12
  from letta.schemas.sandbox_config import SandboxConfigCreate, SandboxConfigUpdate, SandboxType
9
13
  from letta.server.rest_api.utils import get_letta_server, get_user_id
10
14
  from letta.server.server import SyncServer
15
+ from letta.services.helpers.tool_execution_helper import create_venv_for_local_sandbox, install_pip_requirements_for_sandbox
11
16
 
12
17
  router = APIRouter(prefix="/sandbox-config", tags=["sandbox-config"])
13
18
 
19
+ logger = get_logger(__name__)
14
20
 
15
21
  ### Sandbox Config Routes
16
22
 
@@ -44,6 +50,34 @@ def create_default_local_sandbox_config(
44
50
  return server.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.LOCAL, actor=actor)
45
51
 
46
52
 
53
+ @router.post("/local", response_model=PydanticSandboxConfig)
54
+ def create_custom_local_sandbox_config(
55
+ local_sandbox_config: LocalSandboxConfig,
56
+ server: SyncServer = Depends(get_letta_server),
57
+ user_id: str = Depends(get_user_id),
58
+ ):
59
+ """
60
+ Create or update a custom LocalSandboxConfig, including pip_requirements.
61
+ """
62
+ # Ensure the incoming config is of type LOCAL
63
+ if local_sandbox_config.type != SandboxType.LOCAL:
64
+ raise HTTPException(
65
+ status_code=400,
66
+ detail=f"Provided config must be of type '{SandboxType.LOCAL.value}'.",
67
+ )
68
+
69
+ # Retrieve the user (actor)
70
+ actor = server.user_manager.get_user_or_default(user_id=user_id)
71
+
72
+ # Wrap the LocalSandboxConfig into a SandboxConfigCreate
73
+ sandbox_config_create = SandboxConfigCreate(config=local_sandbox_config)
74
+
75
+ # Use the manager to create or update the sandbox config
76
+ sandbox_config = server.sandbox_config_manager.create_or_update_sandbox_config(sandbox_config_create, actor=actor)
77
+
78
+ return sandbox_config
79
+
80
+
47
81
  @router.patch("/{sandbox_config_id}", response_model=PydanticSandboxConfig)
48
82
  def update_sandbox_config(
49
83
  sandbox_config_id: str,
@@ -77,6 +111,49 @@ def list_sandbox_configs(
77
111
  return server.sandbox_config_manager.list_sandbox_configs(actor, limit=limit, after=after, sandbox_type=sandbox_type)
78
112
 
79
113
 
114
+ @router.post("/local/recreate-venv", response_model=PydanticSandboxConfig)
115
+ def force_recreate_local_sandbox_venv(
116
+ server: SyncServer = Depends(get_letta_server),
117
+ user_id: str = Depends(get_user_id),
118
+ ):
119
+ """
120
+ Forcefully recreate the virtual environment for the local sandbox.
121
+ Deletes and recreates the venv, then reinstalls required dependencies.
122
+ """
123
+ actor = server.user_manager.get_user_or_default(user_id=user_id)
124
+
125
+ # Retrieve the local sandbox config
126
+ sbx_config = server.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.LOCAL, actor=actor)
127
+
128
+ local_configs = sbx_config.get_local_config()
129
+ sandbox_dir = os.path.expanduser(local_configs.sandbox_dir) # Expand tilde
130
+ venv_path = os.path.join(sandbox_dir, local_configs.venv_name)
131
+
132
+ # Check if venv exists, and delete if necessary
133
+ if os.path.isdir(venv_path):
134
+ try:
135
+ shutil.rmtree(venv_path)
136
+ logger.info(f"Deleted existing virtual environment at: {venv_path}")
137
+ except Exception as e:
138
+ raise HTTPException(status_code=500, detail=f"Failed to delete existing venv: {e}")
139
+
140
+ # Recreate the virtual environment
141
+ try:
142
+ create_venv_for_local_sandbox(sandbox_dir_path=sandbox_dir, venv_path=str(venv_path), env=os.environ.copy(), force_recreate=True)
143
+ logger.info(f"Successfully recreated virtual environment at: {venv_path}")
144
+ except Exception as e:
145
+ raise HTTPException(status_code=500, detail=f"Failed to recreate venv: {e}")
146
+
147
+ # Install pip requirements
148
+ try:
149
+ install_pip_requirements_for_sandbox(local_configs=local_configs, env=os.environ.copy())
150
+ logger.info(f"Successfully installed pip requirements for venv at: {venv_path}")
151
+ except Exception as e:
152
+ raise HTTPException(status_code=500, detail=f"Failed to install pip requirements: {e}")
153
+
154
+ return sbx_config
155
+
156
+
80
157
  ### Sandbox Environment Variable Routes
81
158
 
82
159
 
@@ -0,0 +1,78 @@
1
+ from datetime import datetime
2
+ from typing import List, Optional
3
+
4
+ from fastapi import APIRouter, Depends, Header, HTTPException, Query
5
+
6
+ from letta.orm.errors import NoResultFound
7
+ from letta.schemas.step import Step
8
+ from letta.server.rest_api.utils import get_letta_server
9
+ from letta.server.server import SyncServer
10
+
11
+ router = APIRouter(prefix="/steps", tags=["steps"])
12
+
13
+
14
+ @router.get("", response_model=List[Step], operation_id="list_steps")
15
+ def list_steps(
16
+ before: Optional[str] = Query(None, description="Return steps before this step ID"),
17
+ after: Optional[str] = Query(None, description="Return steps after this step ID"),
18
+ limit: Optional[int] = Query(50, description="Maximum number of steps to return"),
19
+ order: Optional[str] = Query("desc", description="Sort order (asc or desc)"),
20
+ start_date: Optional[str] = Query(None, description='Return steps after this ISO datetime (e.g. "2025-01-29T15:01:19-08:00")'),
21
+ end_date: Optional[str] = Query(None, description='Return steps before this ISO datetime (e.g. "2025-01-29T15:01:19-08:00")'),
22
+ model: Optional[str] = Query(None, description="Filter by the name of the model used for the step"),
23
+ server: SyncServer = Depends(get_letta_server),
24
+ user_id: Optional[str] = Header(None, alias="user_id"),
25
+ ):
26
+ """
27
+ List steps with optional pagination and date filters.
28
+ Dates should be provided in ISO 8601 format (e.g. 2025-01-29T15:01:19-08:00)
29
+ """
30
+ actor = server.user_manager.get_user_or_default(user_id=user_id)
31
+
32
+ # Convert ISO strings to datetime objects if provided
33
+ start_dt = datetime.fromisoformat(start_date) if start_date else None
34
+ end_dt = datetime.fromisoformat(end_date) if end_date else None
35
+
36
+ return server.step_manager.list_steps(
37
+ actor=actor,
38
+ before=before,
39
+ after=after,
40
+ start_date=start_dt,
41
+ end_date=end_dt,
42
+ limit=limit,
43
+ order=order,
44
+ model=model,
45
+ )
46
+
47
+
48
+ @router.get("/{step_id}", response_model=Step, operation_id="retrieve_step")
49
+ def retrieve_step(
50
+ step_id: str,
51
+ user_id: Optional[str] = Header(None, alias="user_id"),
52
+ server: SyncServer = Depends(get_letta_server),
53
+ ):
54
+ """
55
+ Get a step by ID.
56
+ """
57
+ try:
58
+ return server.step_manager.get_step(step_id=step_id)
59
+ except NoResultFound:
60
+ raise HTTPException(status_code=404, detail="Step not found")
61
+
62
+
63
+ @router.patch("/{step_id}/transaction/{transaction_id}", response_model=Step, operation_id="update_step_transaction_id")
64
+ def update_step_transaction_id(
65
+ step_id: str,
66
+ transaction_id: str,
67
+ user_id: Optional[str] = Header(None, alias="user_id"),
68
+ server: SyncServer = Depends(get_letta_server),
69
+ ):
70
+ """
71
+ Update the transaction ID for a step.
72
+ """
73
+ actor = server.user_manager.get_user_or_default(user_id=user_id)
74
+
75
+ try:
76
+ return server.step_manager.update_step_transaction_id(actor, step_id=step_id, transaction_id=transaction_id)
77
+ except NoResultFound:
78
+ raise HTTPException(status_code=404, detail="Step not found")
@@ -8,15 +8,19 @@ from composio.tools.base.abs import InvalidClassDefinition
8
8
  from fastapi import APIRouter, Body, Depends, Header, HTTPException
9
9
 
10
10
  from letta.errors import LettaToolCreateError
11
+ from letta.log import get_logger
11
12
  from letta.orm.errors import UniqueConstraintViolationError
12
13
  from letta.schemas.letta_message import ToolReturnMessage
13
14
  from letta.schemas.tool import Tool, ToolCreate, ToolRunFromSource, ToolUpdate
14
15
  from letta.schemas.user import User
15
16
  from letta.server.rest_api.utils import get_letta_server
16
17
  from letta.server.server import SyncServer
18
+ from letta.settings import tool_settings
17
19
 
18
20
  router = APIRouter(prefix="/tools", tags=["tools"])
19
21
 
22
+ logger = get_logger(__name__)
23
+
20
24
 
21
25
  @router.delete("/{tool_id}", operation_id="delete_tool")
22
26
  def delete_tool(
@@ -52,6 +56,7 @@ def retrieve_tool(
52
56
  def list_tools(
53
57
  after: Optional[str] = None,
54
58
  limit: Optional[int] = 50,
59
+ name: Optional[str] = None,
55
60
  server: SyncServer = Depends(get_letta_server),
56
61
  user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
57
62
  ):
@@ -60,6 +65,9 @@ def list_tools(
60
65
  """
61
66
  try:
62
67
  actor = server.user_manager.get_user_or_default(user_id=user_id)
68
+ if name is not None:
69
+ tool = server.tool_manager.get_tool_by_name(name=name, actor=actor)
70
+ return [tool] if tool else []
63
71
  return server.tool_manager.list_tools(actor=actor, after=after, limit=limit)
64
72
  except Exception as e:
65
73
  # Log or print the full exception here for debugging
@@ -293,12 +301,18 @@ def add_composio_tool(
293
301
  def get_composio_key(server: SyncServer, actor: User):
294
302
  api_keys = server.sandbox_config_manager.list_sandbox_env_vars_by_key(key="COMPOSIO_API_KEY", actor=actor)
295
303
  if not api_keys:
296
- raise HTTPException(
297
- status_code=400, # Bad Request
298
- detail=f"No API keys found for Composio. Please add your Composio API Key as an environment variable for your sandbox configuration.",
299
- )
300
-
301
- # TODO: Add more protections around this
302
- # Ideally, not tied to a specific sandbox, but for now we just get the first one
303
- # Theoretically possible for someone to have different composio api keys per sandbox
304
- return api_keys[0].value
304
+ logger.warning(f"No API keys found for Composio. Defaulting to the environment variable...")
305
+
306
+ if tool_settings.composio_api_key:
307
+ return tool_settings.composio_api_key
308
+ else:
309
+ # Nothing, raise fatal warning
310
+ raise HTTPException(
311
+ status_code=400, # Bad Request
312
+ detail=f"No API keys found for Composio. Please add your Composio API Key as an environment variable for your sandbox configuration, or set it as environment variable COMPOSIO_API_KEY.",
313
+ )
314
+ else:
315
+ # TODO: Add more protections around this
316
+ # Ideally, not tied to a specific sandbox, but for now we just get the first one
317
+ # Theoretically possible for someone to have different composio api keys per sandbox
318
+ return api_keys[0].value
letta/server/server.py CHANGED
@@ -404,9 +404,6 @@ class SyncServer(Server):
404
404
  if model_settings.lmstudio_base_url.endswith("/v1")
405
405
  else model_settings.lmstudio_base_url + "/v1"
406
406
  )
407
- # Set the OpenAI API key to something non-empty
408
- if model_settings.openai_api_key is None:
409
- model_settings.openai_api_key = "DUMMY"
410
407
  self._enabled_providers.append(LMStudioOpenAIProvider(base_url=lmstudio_url))
411
408
 
412
409
  def load_agent(self, agent_id: str, actor: User, interface: Union[AgentInterface, None] = None) -> Agent:
@@ -0,0 +1,155 @@
1
+ import os
2
+ import platform
3
+ import subprocess
4
+ import venv
5
+ from typing import Dict, Optional
6
+
7
+ from letta.log import get_logger
8
+ from letta.schemas.sandbox_config import LocalSandboxConfig
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ def find_python_executable(local_configs: LocalSandboxConfig) -> str:
14
+ """
15
+ Determines the Python executable path based on sandbox configuration and platform.
16
+ Resolves any '~' (tilde) paths to absolute paths.
17
+
18
+ Returns:
19
+ str: Full path to the Python binary.
20
+ """
21
+ sandbox_dir = os.path.expanduser(local_configs.sandbox_dir) # Expand tilde
22
+
23
+ if not local_configs.use_venv:
24
+ return "python.exe" if platform.system().lower().startswith("win") else "python3"
25
+
26
+ venv_path = os.path.join(sandbox_dir, local_configs.venv_name)
27
+ python_exec = (
28
+ os.path.join(venv_path, "Scripts", "python.exe")
29
+ if platform.system().startswith("Win")
30
+ else os.path.join(venv_path, "bin", "python3")
31
+ )
32
+
33
+ if not os.path.isfile(python_exec):
34
+ raise FileNotFoundError(f"Python executable not found: {python_exec}. Ensure the virtual environment exists.")
35
+
36
+ return python_exec
37
+
38
+
39
+ def run_subprocess(command: list, env: Optional[Dict[str, str]] = None, fail_msg: str = "Command failed"):
40
+ """
41
+ Helper to execute a subprocess with logging and error handling.
42
+
43
+ Args:
44
+ command (list): The command to run as a list of arguments.
45
+ env (dict, optional): The environment variables to use for the process.
46
+ fail_msg (str): The error message to log in case of failure.
47
+
48
+ Raises:
49
+ RuntimeError: If the subprocess execution fails.
50
+ """
51
+ logger.info(f"Running command: {' '.join(command)}")
52
+ try:
53
+ result = subprocess.run(command, check=True, capture_output=True, text=True, env=env)
54
+ logger.info(f"Command successful. Output:\n{result.stdout}")
55
+ return result.stdout
56
+ except subprocess.CalledProcessError as e:
57
+ logger.error(f"{fail_msg}\nSTDOUT:\n{e.stdout}\nSTDERR:\n{e.stderr}")
58
+ raise RuntimeError(f"{fail_msg}: {e.stderr.strip()}") from e
59
+
60
+
61
+ def ensure_pip_is_up_to_date(python_exec: str, env: Optional[Dict[str, str]] = None):
62
+ """
63
+ Ensures pip, setuptools, and wheel are up to date before installing any other dependencies.
64
+
65
+ Args:
66
+ python_exec (str): Path to the Python executable to use.
67
+ env (dict, optional): Environment variables to pass to subprocess.
68
+ """
69
+ run_subprocess(
70
+ [python_exec, "-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel"],
71
+ env=env,
72
+ fail_msg="Failed to upgrade pip, setuptools, and wheel.",
73
+ )
74
+
75
+
76
+ def install_pip_requirements_for_sandbox(
77
+ local_configs: LocalSandboxConfig,
78
+ upgrade: bool = True,
79
+ user_install_if_no_venv: bool = False,
80
+ env: Optional[Dict[str, str]] = None,
81
+ ):
82
+ """
83
+ Installs the specified pip requirements inside the correct environment (venv or system).
84
+ """
85
+ if not local_configs.pip_requirements:
86
+ logger.debug("No pip requirements specified; skipping installation.")
87
+ return
88
+
89
+ sandbox_dir = os.path.expanduser(local_configs.sandbox_dir) # Expand tilde
90
+ local_configs.sandbox_dir = sandbox_dir # Update the object to store the absolute path
91
+
92
+ python_exec = find_python_executable(local_configs)
93
+
94
+ # If using a virtual environment, upgrade pip before installing dependencies.
95
+ if local_configs.use_venv:
96
+ ensure_pip_is_up_to_date(python_exec, env=env)
97
+
98
+ # Construct package list
99
+ packages = [f"{req.name}=={req.version}" if req.version else req.name for req in local_configs.pip_requirements]
100
+
101
+ # Construct pip install command
102
+ pip_cmd = [python_exec, "-m", "pip", "install"]
103
+ if upgrade:
104
+ pip_cmd.append("--upgrade")
105
+ pip_cmd += packages
106
+
107
+ if user_install_if_no_venv and not local_configs.use_venv:
108
+ pip_cmd.append("--user")
109
+
110
+ run_subprocess(pip_cmd, env=env, fail_msg=f"Failed to install packages: {', '.join(packages)}")
111
+
112
+
113
+ def create_venv_for_local_sandbox(sandbox_dir_path: str, venv_path: str, env: Dict[str, str], force_recreate: bool):
114
+ """
115
+ Creates a virtual environment for the sandbox. If force_recreate is True, deletes and recreates the venv.
116
+
117
+ Args:
118
+ sandbox_dir_path (str): Path to the sandbox directory.
119
+ venv_path (str): Path to the virtual environment directory.
120
+ env (dict): Environment variables to use.
121
+ force_recreate (bool): If True, delete and recreate the virtual environment.
122
+ """
123
+ sandbox_dir_path = os.path.expanduser(sandbox_dir_path)
124
+ venv_path = os.path.expanduser(venv_path)
125
+
126
+ # If venv exists and force_recreate is True, delete it
127
+ if force_recreate and os.path.isdir(venv_path):
128
+ logger.warning(f"Force recreating virtual environment at: {venv_path}")
129
+ import shutil
130
+
131
+ shutil.rmtree(venv_path)
132
+
133
+ # Create venv if it does not exist
134
+ if not os.path.isdir(venv_path):
135
+ logger.info(f"Creating new virtual environment at {venv_path}")
136
+ venv.create(venv_path, with_pip=True)
137
+
138
+ pip_path = os.path.join(venv_path, "bin", "pip")
139
+ try:
140
+ # Step 2: Upgrade pip
141
+ logger.info("Upgrading pip in the virtual environment...")
142
+ subprocess.run([pip_path, "install", "--upgrade", "pip"], env=env, check=True)
143
+
144
+ # Step 3: Install packages from requirements.txt if available
145
+ requirements_txt_path = os.path.join(sandbox_dir_path, "requirements.txt")
146
+ if os.path.isfile(requirements_txt_path):
147
+ logger.info(f"Installing packages from requirements file: {requirements_txt_path}")
148
+ subprocess.run([pip_path, "install", "-r", requirements_txt_path], env=env, check=True)
149
+ logger.info("Successfully installed packages from requirements.txt")
150
+ else:
151
+ logger.warning("No requirements.txt file found. Skipping package installation.")
152
+
153
+ except subprocess.CalledProcessError as e:
154
+ logger.error(f"Error while setting up the virtual environment: {e}")
155
+ raise RuntimeError(f"Failed to set up the virtual environment: {e}")
@@ -1,3 +1,4 @@
1
+ import datetime
1
2
  from typing import List, Literal, Optional
2
3
 
3
4
  from sqlalchemy import select
@@ -20,6 +21,34 @@ class StepManager:
20
21
 
21
22
  self.session_maker = db_context
22
23
 
24
+ @enforce_types
25
+ def list_steps(
26
+ self,
27
+ actor: PydanticUser,
28
+ before: Optional[str] = None,
29
+ after: Optional[str] = None,
30
+ start_date: Optional[datetime] = None,
31
+ end_date: Optional[datetime] = None,
32
+ limit: Optional[int] = 50,
33
+ order: Optional[str] = None,
34
+ model: Optional[str] = None,
35
+ ) -> List[PydanticStep]:
36
+ """List all jobs with optional pagination and status filter."""
37
+ with self.session_maker() as session:
38
+ filter_kwargs = {"organization_id": actor.organization_id, "model": model}
39
+
40
+ steps = StepModel.list(
41
+ db_session=session,
42
+ before=before,
43
+ after=after,
44
+ start_date=start_date,
45
+ end_date=end_date,
46
+ limit=limit,
47
+ ascending=True if order == "asc" else False,
48
+ **filter_kwargs,
49
+ )
50
+ return [step.to_pydantic() for step in steps]
51
+
23
52
  @enforce_types
24
53
  def log_step(
25
54
  self,
@@ -58,6 +87,32 @@ class StepManager:
58
87
  step = StepModel.read(db_session=session, identifier=step_id)
59
88
  return step.to_pydantic()
60
89
 
90
+ @enforce_types
91
+ def update_step_transaction_id(self, actor: PydanticUser, step_id: str, transaction_id: str) -> PydanticStep:
92
+ """Update the transaction ID for a step.
93
+
94
+ Args:
95
+ actor: The user making the request
96
+ step_id: The ID of the step to update
97
+ transaction_id: The new transaction ID to set
98
+
99
+ Returns:
100
+ The updated step
101
+
102
+ Raises:
103
+ NoResultFound: If the step does not exist
104
+ """
105
+ with self.session_maker() as session:
106
+ step = session.get(StepModel, step_id)
107
+ if not step:
108
+ raise NoResultFound(f"Step with id {step_id} does not exist")
109
+ if step.organization_id != actor.organization_id:
110
+ raise Exception("Unauthorized")
111
+
112
+ step.tid = transaction_id
113
+ session.commit()
114
+ return step.to_pydantic()
115
+
61
116
  def _verify_job_access(
62
117
  self,
63
118
  session: Session,
@@ -9,7 +9,6 @@ import sys
9
9
  import tempfile
10
10
  import traceback
11
11
  import uuid
12
- import venv
13
12
  from typing import Any, Dict, Optional
14
13
 
15
14
  from letta.log import get_logger
@@ -17,6 +16,11 @@ from letta.schemas.agent import AgentState
17
16
  from letta.schemas.sandbox_config import SandboxConfig, SandboxRunResult, SandboxType
18
17
  from letta.schemas.tool import Tool
19
18
  from letta.schemas.user import User
19
+ from letta.services.helpers.tool_execution_helper import (
20
+ create_venv_for_local_sandbox,
21
+ find_python_executable,
22
+ install_pip_requirements_for_sandbox,
23
+ )
20
24
  from letta.services.sandbox_config_manager import SandboxConfigManager
21
25
  from letta.services.tool_manager import ToolManager
22
26
  from letta.settings import tool_settings
@@ -38,7 +42,9 @@ class ToolExecutionSandbox:
38
42
  # We make this a long random string to avoid collisions with any variables in the user's code
39
43
  LOCAL_SANDBOX_RESULT_VAR_NAME = "result_ZQqiequkcFwRwwGQMqkt"
40
44
 
41
- def __init__(self, tool_name: str, args: dict, user: User, force_recreate=True, tool_object: Optional[Tool] = None):
45
+ def __init__(
46
+ self, tool_name: str, args: dict, user: User, force_recreate=True, force_recreate_venv=False, tool_object: Optional[Tool] = None
47
+ ):
42
48
  self.tool_name = tool_name
43
49
  self.args = args
44
50
  self.user = user
@@ -58,6 +64,7 @@ class ToolExecutionSandbox:
58
64
 
59
65
  self.sandbox_config_manager = SandboxConfigManager(tool_settings)
60
66
  self.force_recreate = force_recreate
67
+ self.force_recreate_venv = force_recreate_venv
61
68
 
62
69
  def run(self, agent_state: Optional[AgentState] = None, additional_env_vars: Optional[Dict] = None) -> SandboxRunResult:
63
70
  """
@@ -150,36 +157,41 @@ class ToolExecutionSandbox:
150
157
 
151
158
  def run_local_dir_sandbox_venv(self, sbx_config: SandboxConfig, env: Dict[str, str], temp_file_path: str) -> SandboxRunResult:
152
159
  local_configs = sbx_config.get_local_config()
153
- venv_path = os.path.join(local_configs.sandbox_dir, local_configs.venv_name)
160
+ sandbox_dir = os.path.expanduser(local_configs.sandbox_dir) # Expand tilde
161
+ venv_path = os.path.join(sandbox_dir, local_configs.venv_name)
154
162
 
155
- # Safety checks for the venv: verify that the venv path exists and is a directory
156
- if not os.path.isdir(venv_path):
163
+ # Recreate venv if required
164
+ if self.force_recreate_venv or not os.path.isdir(venv_path):
157
165
  logger.warning(f"Virtual environment directory does not exist at: {venv_path}, creating one now...")
158
- self.create_venv_for_local_sandbox(sandbox_dir_path=local_configs.sandbox_dir, venv_path=venv_path, env=env)
166
+ create_venv_for_local_sandbox(
167
+ sandbox_dir_path=sandbox_dir, venv_path=venv_path, env=env, force_recreate=self.force_recreate_venv
168
+ )
169
+
170
+ install_pip_requirements_for_sandbox(local_configs, env=env)
159
171
 
160
- # Ensure the python interpreter exists in the virtual environment
161
- python_executable = os.path.join(venv_path, "bin", "python3")
172
+ # Ensure Python executable exists
173
+ python_executable = find_python_executable(local_configs)
162
174
  if not os.path.isfile(python_executable):
163
175
  raise FileNotFoundError(f"Python executable not found in virtual environment: {python_executable}")
164
176
 
165
- # Set up env for venv
177
+ # Set up environment variables
166
178
  env["VIRTUAL_ENV"] = venv_path
167
179
  env["PATH"] = os.path.join(venv_path, "bin") + ":" + env["PATH"]
168
- # Suppress all warnings
169
180
  env["PYTHONWARNINGS"] = "ignore"
170
181
 
171
- # Execute the code in a restricted subprocess
182
+ # Execute the code
172
183
  try:
173
184
  result = subprocess.run(
174
- [os.path.join(venv_path, "bin", "python3"), temp_file_path],
185
+ [python_executable, temp_file_path],
175
186
  env=env,
176
- cwd=local_configs.sandbox_dir, # Restrict execution to sandbox_dir
187
+ cwd=sandbox_dir,
177
188
  timeout=60,
178
189
  capture_output=True,
179
190
  text=True,
180
191
  )
181
192
  func_result, stdout = self.parse_out_function_results_markers(result.stdout)
182
193
  func_return, agent_state = self.parse_best_effort(func_result)
194
+
183
195
  return SandboxRunResult(
184
196
  func_return=func_return,
185
197
  agent_state=agent_state,
@@ -260,29 +272,6 @@ class ToolExecutionSandbox:
260
272
  end_index = text.index(self.LOCAL_SANDBOX_RESULT_END_MARKER)
261
273
  return text[start_index:end_index], text[: start_index - marker_len] + text[end_index + +marker_len :]
262
274
 
263
- def create_venv_for_local_sandbox(self, sandbox_dir_path: str, venv_path: str, env: Dict[str, str]):
264
- # Step 1: Create the virtual environment
265
- venv.create(venv_path, with_pip=True)
266
-
267
- pip_path = os.path.join(venv_path, "bin", "pip")
268
- try:
269
- # Step 2: Upgrade pip
270
- logger.info("Upgrading pip in the virtual environment...")
271
- subprocess.run([pip_path, "install", "--upgrade", "pip"], env=env, check=True)
272
-
273
- # Step 3: Install packages from requirements.txt if provided
274
- requirements_txt_path = os.path.join(sandbox_dir_path, self.REQUIREMENT_TXT_NAME)
275
- if os.path.isfile(requirements_txt_path):
276
- logger.info(f"Installing packages from requirements file: {requirements_txt_path}")
277
- subprocess.run([pip_path, "install", "-r", requirements_txt_path], env=env, check=True)
278
- logger.info("Successfully installed packages from requirements.txt")
279
- else:
280
- logger.warning("No requirements.txt file provided or the file does not exist. Skipping package installation.")
281
-
282
- except subprocess.CalledProcessError as e:
283
- logger.error(f"Error while setting up the virtual environment: {e}")
284
- raise RuntimeError(f"Failed to set up the virtual environment: {e}")
285
-
286
275
  # e2b sandbox specific functions
287
276
 
288
277
  def run_e2b_sandbox(self, agent_state: Optional[AgentState] = None, additional_env_vars: Optional[Dict] = None) -> SandboxRunResult:
letta/settings.py CHANGED
@@ -121,7 +121,7 @@ if "--use-file-pg-uri" in sys.argv:
121
121
  try:
122
122
  with open(Path.home() / ".letta/pg_uri", "r") as f:
123
123
  default_pg_uri = f.read()
124
- print("Read pg_uri from ~/.letta/pg_uri")
124
+ print(f"Read pg_uri from ~/.letta/pg_uri: {default_pg_uri}")
125
125
  except FileNotFoundError:
126
126
  pass
127
127
 
letta/system.py CHANGED
@@ -152,6 +152,15 @@ def package_function_response(was_success, response_string, timestamp=None):
152
152
 
153
153
 
154
154
  def package_system_message(system_message, message_type="system_alert", time=None):
155
+ # error handling for recursive packaging
156
+ try:
157
+ message_json = json.loads(system_message)
158
+ if "type" in message_json and message_json["type"] == message_type:
159
+ warnings.warn(f"Attempted to pack a system message that is already packed. Not packing: '{system_message}'")
160
+ return system_message
161
+ except:
162
+ pass # do nothing, expected behavior that the message is not JSON
163
+
155
164
  formatted_time = time if time else get_local_time()
156
165
  packaged_message = {
157
166
  "type": message_type,
@@ -214,7 +223,7 @@ def unpack_message(packed_message) -> str:
214
223
  try:
215
224
  message_json = json.loads(packed_message)
216
225
  except:
217
- warnings.warn(f"Was unable to load message as JSON to unpack: ''{packed_message}")
226
+ warnings.warn(f"Was unable to load message as JSON to unpack: '{packed_message}'")
218
227
  return packed_message
219
228
 
220
229
  if "message" not in message_json:
@@ -224,4 +233,8 @@ def unpack_message(packed_message) -> str:
224
233
  warnings.warn(f"Was unable to find 'message' field in packed message object: '{packed_message}'")
225
234
  return packed_message
226
235
  else:
236
+ message_type = message_json["type"]
237
+ if message_type != "user_message":
238
+ warnings.warn(f"Expected type to be 'user_message', but was '{message_type}', so not unpacking: '{packed_message}'")
239
+ return packed_message
227
240
  return message_json.get("message")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: letta-nightly
3
- Version: 0.6.19.dev20250130104029
3
+ Version: 0.6.20.dev20250131222205
4
4
  Summary: Create LLM agents with long-term memory and custom tools
5
5
  License: Apache License
6
6
  Author: Letta Team
@@ -1,4 +1,4 @@
1
- letta/__init__.py,sha256=mpGFNaHH0eZV-lM9FibGipWc-jltrJW033d7eqp66Dw,919
1
+ letta/__init__.py,sha256=EApmTks3-qbEB16_xaPs03NX3OVfVYtxNA3MTpE-oRs,919
2
2
  letta/__main__.py,sha256=6Hs2PV7EYc5Tid4g4OtcLXhqVHiNYTGzSBdoOnW2HXA,29
3
3
  letta/agent.py,sha256=3-YDxRLMKPrXnmvZ1qstG2MmH9FU9cUQ0cDYZbFQ9eM,56575
4
4
  letta/benchmark/benchmark.py,sha256=ebvnwfp3yezaXOQyGXkYCDYpsmre-b9hvNtnyx4xkG0,3701
@@ -32,16 +32,16 @@ letta/humans/examples/basic.txt,sha256=Lcp8YESTWvOJgO4Yf_yyQmgo5bKakeB1nIVrwEGG6
32
32
  letta/humans/examples/cs_phd.txt,sha256=9C9ZAV_VuG7GB31ksy3-_NAyk8rjE6YtVOkhp08k1xw,297
33
33
  letta/interface.py,sha256=JszHyhIK34dpV0h5KL0CD1W4svh4eijaHGgfOYyZOhg,12755
34
34
  letta/llm_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
- letta/llm_api/anthropic.py,sha256=5prpeZWHtjors9zT5e_6ozq5HG3BP2FVWAmoPqAW8zA,34162
35
+ letta/llm_api/anthropic.py,sha256=ZPPjpYZh8hleSjHkbRF27EEwSp-pg23JlSo-b1wbWBY,33602
36
36
  letta/llm_api/aws_bedrock.py,sha256=-ms9tdROu8DLrEZJ9XgL-IyIOU_0UJKuhfRbjLs0_Gc,3838
37
37
  letta/llm_api/azure_openai.py,sha256=Y1HKPog1XzM_f7ujUK_Gv2zQkoy5pU-1bKiUnvSxSrs,6297
38
- letta/llm_api/azure_openai_constants.py,sha256=oXtKrgBFHf744gyt5l1thILXgyi8NDNUrKEa2GGGpjw,278
38
+ letta/llm_api/azure_openai_constants.py,sha256=_f7NKjKBPxGPFQPfP1e0umHk4Jmf56qNjyecI0PqWqU,267
39
39
  letta/llm_api/cohere.py,sha256=H5kzYH_aQAnGNq7lip7XyKGLEOKC318Iw0_tiTP6kc4,14772
40
40
  letta/llm_api/google_ai.py,sha256=MIX4nmyC6448AvyPPSE8JZ_tzSpKJTArkZSfQGGoy0M,17920
41
41
  letta/llm_api/helpers.py,sha256=ov9WHsLSvkceIpSNJ3PUgCvufD862Bcrum-bWrUVJko,16193
42
- letta/llm_api/llm_api_tools.py,sha256=Ur3Wr5FnrcnRpJi5EpSbVe0yVB4iij81VOS278faYwY,20174
42
+ letta/llm_api/llm_api_tools.py,sha256=UXm1t_DPyJVhBtzBGP8wv1LPTKyfsng31X0yfIAEusI,20292
43
43
  letta/llm_api/mistral.py,sha256=fHdfD9ug-rQIk2qn8tRKay1U6w9maF11ryhKi91FfXM,1593
44
- letta/llm_api/openai.py,sha256=FRGtfE10fyv2QxePKjHUKBfzG1954E1BKyrbHy74kV4,20331
44
+ letta/llm_api/openai.py,sha256=gE2RTYsyATYjicgE4VwATUAwTD38B74ZVqy8oVemzdQ,20277
45
45
  letta/local_llm/README.md,sha256=hFJyw5B0TU2jrh9nb0zGZMgdH-Ei1dSRfhvPQG_NSoU,168
46
46
  letta/local_llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
47
  letta/local_llm/chat_completion_proxy.py,sha256=ElYR0M5SY2zL4NQzInye21MxqtiP3AUXX9Ia0KbkD4Y,12948
@@ -87,12 +87,12 @@ letta/openai_backcompat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
87
87
  letta/openai_backcompat/openai_object.py,sha256=Y1ZS1sATP60qxJiOsjOP3NbwSzuzvkNAvb3DeuhM5Uk,13490
88
88
  letta/orm/__all__.py,sha256=2gh2MZTkA3Hw67VWVKK3JIStJOqTeLdpCvYSVYNeEDA,692
89
89
  letta/orm/__init__.py,sha256=wPokP-EvOri2LhKLjmYVtI_FWchaxgFvJeF_NAfqJIo,842
90
- letta/orm/agent.py,sha256=CdcpVFfuFkGeaInVTaUTCjavUOhOZ3HsguCXs6fYBY4,6859
90
+ letta/orm/agent.py,sha256=ucVnPMP7S9YuWcMPxG_CWtT5QH8P2TWSU-Cp1HLuXIU,7385
91
91
  letta/orm/agents_tags.py,sha256=dYSnHz4IWBjyOiQ4RJomX3P0QN76JTlEZEw5eJM6Emg,925
92
92
  letta/orm/base.py,sha256=VjvxF9TwKF9Trf8BJkDgf7D6KrWWopOkUiF18J3IElk,3071
93
93
  letta/orm/block.py,sha256=EjH8lXexHtFIHJ8G-RjNo2oAH834x0Hbn4CER9S4U74,3880
94
94
  letta/orm/blocks_agents.py,sha256=W0dykl9OchAofSuAYZD5zNmhyMabPr9LTJrz-I3A0m4,983
95
- letta/orm/custom_columns.py,sha256=grgUo4kIAd-xsFsWSsOzv3Bw51SA8XZHEJJstLcRI1I,5285
95
+ letta/orm/custom_columns.py,sha256=APR3ylle9hUutQoy8m-toTV53F1mpcQhEcnf32XTmQA,5447
96
96
  letta/orm/enums.py,sha256=HzX3eXhBH-PnpxhBWtWbkV4J6wrStlJaX_OVdZgAdLU,428
97
97
  letta/orm/errors.py,sha256=Se0Guz-gqi-D36NUWSh7AP9zTVCSph9KgZh_trwng4o,734
98
98
  letta/orm/file.py,sha256=7_p7LxityP3NQZVURQYG0kgcZhEkVuMN0Fj4h9YOe1w,1780
@@ -139,7 +139,7 @@ letta/prompts/system/memgpt_modified_o1.txt,sha256=objnDgnxpF3-MmU28ZqZ7-TOG8UlH
139
139
  letta/prompts/system/memgpt_offline_memory.txt,sha256=rWEJeF-6aiinjkJM9hgLUYCmlEcC_HekYB1bjEUYq6M,2460
140
140
  letta/prompts/system/memgpt_offline_memory_chat.txt,sha256=ituh7gDuio7nC2UKFB7GpBq6crxb8bYedQfJ0ADoPgg,3949
141
141
  letta/pytest.ini,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
142
- letta/schemas/agent.py,sha256=VmzzaRb4HA20fahzUxU2JOdcIxt2ywLFdTRjbR6Ogyk,11783
142
+ letta/schemas/agent.py,sha256=M09-BY37tqxRkQ8sh16oq7Ay_TOJWoP63Cm6aJxp-SY,12091
143
143
  letta/schemas/block.py,sha256=FYYmRWjH4d4QHMUx_nmIXjv_qJna_l-Ip_4i51wDBPA,5942
144
144
  letta/schemas/embedding_config.py,sha256=RkLbUorFkMWr1tPkn6c2aHrnICjWTbhPY86tIncwXyA,3373
145
145
  letta/schemas/embedding_config_overrides.py,sha256=lkTa4y-EQ2RnaEKtKDM0sEAk7EwNa67REw8DGNNtGQY,84
@@ -165,18 +165,18 @@ letta/schemas/organization.py,sha256=WWbUWVSp_VQRFwWN4fdHg1yObiV6x9rZnvIY8x5BPs0
165
165
  letta/schemas/passage.py,sha256=pdCLZgOn0gWK1gB6aFHLS0gfdWCBqLaiHDA0iQ12Zd8,3704
166
166
  letta/schemas/providers.py,sha256=Wd0d0jgv6z3X5t7cT1ZVoX_Qa85ecsm1gQzkOPgQFUo,34890
167
167
  letta/schemas/run.py,sha256=SRqPRziINIiPunjOhE_NlbnQYgxTvqmbauni_yfBQRA,2085
168
- letta/schemas/sandbox_config.py,sha256=v32V5T73X-VxhDk0g_1RGniK985KMvg2xyLVi1dvMQY,4215
168
+ letta/schemas/sandbox_config.py,sha256=Nz8K5brqe6jpf66KnTJ0-E7ZeFdPoBFGN-XOI35OeaY,5926
169
169
  letta/schemas/source.py,sha256=-BQVolcXA2ziCu2ztR6cbTdGUc8G7vGJy7rvpdf1hpg,2880
170
170
  letta/schemas/step.py,sha256=cCmDChQMndy7aMJGH0Z19VbzJkAeFTYuA0cJpzjW2g0,1928
171
171
  letta/schemas/tool.py,sha256=uv3WxTt9SzaoXzwTLNHT2wegWTcaBFQBnBvNJrxeYvs,11022
172
- letta/schemas/tool_rule.py,sha256=SaMHAO5-hlhceQJjz2mDpb7rAN0eDm5yIItDEE7op8o,1732
172
+ letta/schemas/tool_rule.py,sha256=tS7ily6NJD8E4n7Hla38jMUe6OIdhdc1ckq0AiRpu5Y,1893
173
173
  letta/schemas/usage.py,sha256=8oYRH-JX0PfjIu2zkT5Uu3UWQ7Unnz_uHiO8hRGI4m0,912
174
174
  letta/schemas/user.py,sha256=V32Tgl6oqB3KznkxUz12y7agkQicjzW7VocSpj78i6Q,1526
175
175
  letta/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
176
176
  letta/server/constants.py,sha256=yAdGbLkzlOU_dLTx0lKDmAnj0ZgRXCEaIcPJWO69eaE,92
177
177
  letta/server/generate_openapi_schema.sh,sha256=0OtBhkC1g6CobVmNEd_m2B6sTdppjbJLXaM95icejvE,371
178
178
  letta/server/rest_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
179
- letta/server/rest_api/app.py,sha256=PapV6EQHCGy0pxEuCj6MpawU_u3vXUb2-czyzYIBfS0,11698
179
+ letta/server/rest_api/app.py,sha256=9cf9H6vZhN-iBJqkqjBdFWjA3PlKfok-q48ltI71qls,11805
180
180
  letta/server/rest_api/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
181
181
  letta/server/rest_api/auth/index.py,sha256=fQBGyVylGSRfEMLQ17cZzrHd5Y1xiVylvPqH5Rl-lXQ,1378
182
182
  letta/server/rest_api/auth_token.py,sha256=725EFEIiNj4dh70hrSd94UysmFD8vcJLrTRfNHkzxDo,774
@@ -186,7 +186,7 @@ letta/server/rest_api/optimistic_json_parser.py,sha256=1z4d9unmxMb0ou7owJ62uUQoN
186
186
  letta/server/rest_api/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
187
187
  letta/server/rest_api/routers/openai/chat_completions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
188
188
  letta/server/rest_api/routers/openai/chat_completions/chat_completions.py,sha256=TjeiyEMkpWR3PBazUWs4W2XVwGVEW5NRTOFXWag62oU,6657
189
- letta/server/rest_api/routers/v1/__init__.py,sha256=bXEZzmvHNX7N11NDwsxyajjci7yxP-2dYIvbeJi33vA,1070
189
+ letta/server/rest_api/routers/v1/__init__.py,sha256=tzD8Oh6ynPkg8ULcITWcwalLL81SIh6eztPqV9l7VGk,1162
190
190
  letta/server/rest_api/routers/v1/agents.py,sha256=-eRRQDyWyBnP6aj6z9rJTiaidqPls1TQB1xSqGdIYGA,25041
191
191
  letta/server/rest_api/routers/v1/blocks.py,sha256=oJYOpGUTd4AhKwVolVlZPIXO2EoOrBHkyi2PdrmbtmA,3888
192
192
  letta/server/rest_api/routers/v1/health.py,sha256=pKCuVESlVOhGIb4VC4K-H82eZqfghmT6kvj2iOkkKuc,401
@@ -195,14 +195,15 @@ letta/server/rest_api/routers/v1/llms.py,sha256=lYp5URXtZk1yu_Pe-p1Wq1uQ0qeb6aWt
195
195
  letta/server/rest_api/routers/v1/organizations.py,sha256=8n-kA9LHtKImdY2xL-v7m6nYAbFWqH1vjBCJhQbv7Is,2077
196
196
  letta/server/rest_api/routers/v1/providers.py,sha256=EOwSy4KsU63RY_NjzjjR4K6uaAmewXYTgbNOL4aO-X8,2444
197
197
  letta/server/rest_api/routers/v1/runs.py,sha256=4w06j5CYfRzVf5Jf9Fzh7zQyVxC1R6q9gpglERYKeHU,6062
198
- letta/server/rest_api/routers/v1/sandbox_configs.py,sha256=RR7u3Yj2d9llopbUYyXxgJnV-UXY0LvUMoL41a1yXCk,5260
198
+ letta/server/rest_api/routers/v1/sandbox_configs.py,sha256=_6WcwgKrgRfnTEC_EgoN-gt8yTkLPgMdM56g1SwX_eo,8502
199
199
  letta/server/rest_api/routers/v1/sources.py,sha256=g7NKgbZkS7y1vlukJHZ_yoWrk3AxyoWKTVGszp0Ky18,8414
200
+ letta/server/rest_api/routers/v1/steps.py,sha256=SuZmneaeSSAHjOMatMt_QILLzkNpkbwuVgKiMYr52cE,3038
200
201
  letta/server/rest_api/routers/v1/tags.py,sha256=45G0cmcP-ER0OO5OanT_fGtGQfl9ZjRKU97mFwtwyfo,878
201
- letta/server/rest_api/routers/v1/tools.py,sha256=Rp2_YH7KHeP_-WHjlwqu0wxtR27wvaO0wZToZtENB_w,12000
202
+ letta/server/rest_api/routers/v1/tools.py,sha256=BS4HsHZLv2n4MauLSrVNlch2TuijzlyEfqKc3fP2b94,12633
202
203
  letta/server/rest_api/routers/v1/users.py,sha256=G5DBHSkPfBgVHN2Wkm-rVYiLQAudwQczIq2Z3YLdbVo,2277
203
204
  letta/server/rest_api/static_files.py,sha256=NG8sN4Z5EJ8JVQdj19tkFa9iQ1kBPTab9f_CUxd_u4Q,3143
204
205
  letta/server/rest_api/utils.py,sha256=dsjkZzgo9Rk3fjUf1ajjiiql1eeO5DAzmXprttI7bJU,3993
205
- letta/server/server.py,sha256=Des6W1ebnorwTB6hJtalYAltmkJWUlT7ioZB3dg-1lA,59997
206
+ letta/server/server.py,sha256=GensF4fGmRP5-3hGxm6Zr6rQea2XyX-T2dIK277Shjk,59827
206
207
  letta/server/startup.sh,sha256=722uKJWB2C4q3vjn39De2zzPacaZNw_1fN1SpLGjKIo,1569
207
208
  letta/server/static_files/assets/index-048c9598.js,sha256=mR16XppvselwKCcNgONs4L7kZEVa4OEERm4lNZYtLSk,146819
208
209
  letta/server/static_files/assets/index-0e31b727.css,sha256=SBbja96uiQVLDhDOroHgM6NSl7tS4lpJRCREgSS_hA8,7672
@@ -219,6 +220,7 @@ letta/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
219
220
  letta/services/agent_manager.py,sha256=I6F-nED9Q6O_FY2-O1gHDY-OaQIxbt3FsL88DENVTfE,50287
220
221
  letta/services/block_manager.py,sha256=u56TXG46QDMbQZadDGCO7fY1vreJ69Xr_0MUF53xw4k,5519
221
222
  letta/services/helpers/agent_manager_helper.py,sha256=RH0MXLZASkP2LVbVNUfSYHrcBYZnVxFd9ejGjRK90Hw,11283
223
+ letta/services/helpers/tool_execution_helper.py,sha256=q8uSiQcX6VH_iNg5VNloZgC2JkH9lIOXBKCXYPx2Yac,6097
222
224
  letta/services/job_manager.py,sha256=7awEHraoEmqlDQvcQTEkb_dcZsG9eIYGWYct8exzm2E,13117
223
225
  letta/services/message_manager.py,sha256=w6-B9Zz5z9UXcd6mKhinsaCINTsmxDsH9JJsV2_qlH4,8878
224
226
  letta/services/organization_manager.py,sha256=h3hrknBhA3YQt90OeBzFnqjYM9NWKnk8jDKzXGm4AUg,3392
@@ -227,17 +229,17 @@ letta/services/per_agent_lock_manager.py,sha256=porM0cKKANQ1FvcGXOO_qM7ARk5Fgi1H
227
229
  letta/services/provider_manager.py,sha256=jEal0A0XWobWH5CVfmzPtcFhsflI-sanqyg26FqpDKk,3575
228
230
  letta/services/sandbox_config_manager.py,sha256=eWDNTscRG9Gt_Ixho3-daOOno_9KcebxeA9v_CbzYu0,13371
229
231
  letta/services/source_manager.py,sha256=0JLKIv405oS5wc6bY5k2bxxZpS9O-VwUGHVdGPbJ3e4,7676
230
- letta/services/step_manager.py,sha256=RngrVs2Sd_oDZv_UoQ1ToLY0RnH-6wS1DqIBPRm-Imc,2961
231
- letta/services/tool_execution_sandbox.py,sha256=Tjufm58V9XzeYr8oF6g5b3OV5zZ7oPWUTqcC8GsBi9k,23162
232
+ letta/services/step_manager.py,sha256=Zpbz6o-KgvGePiQkgd6lHagtpYo3H906vjDE0_hpZVo,4871
233
+ letta/services/tool_execution_sandbox.py,sha256=4XBYkCEBLG6GqijxgqeLIQQJ9zRbsJa8vZ4dZG04Pq8,22080
232
234
  letta/services/tool_manager.py,sha256=0zc7bFxGvl_wjs7CC4cyeUWFZAUQK_yVKsUjhrW3Vao,9181
233
235
  letta/services/user_manager.py,sha256=1U8BQ_-MBkEW2wnSFV_OsTwBmRAZLN8uHLFjnDjK3hA,4308
234
- letta/settings.py,sha256=sHJEaLPtEnWNZDew69F9tC8YTrkBh5BWXXtzOtAneFY,6071
236
+ letta/settings.py,sha256=r9KUE1YU2VfLPsdjTRvv5OfyEhV6CqXwqkeeT6NMznI,6090
235
237
  letta/streaming_interface.py,sha256=lo2VAQRUJOdWTijwnXuKOC9uejqr2siUAEmZiQUXkj8,15710
236
238
  letta/streaming_utils.py,sha256=jLqFTVhUL76FeOuYk8TaRQHmPTf3HSRc2EoJwxJNK6U,11946
237
- letta/system.py,sha256=3jevN1QOScahsuEW-cavI3GsmiNDTkWryEukb1WTRTo,7752
239
+ letta/system.py,sha256=S_0cod77iEttkFd1bSh2wenLCKA8YL487AuVenIDUng,8425
238
240
  letta/utils.py,sha256=lgBDWKmrQrmJGPxcgamFC2aJyi6I0dX7bzLBt3YC6j0,34051
239
- letta_nightly-0.6.19.dev20250130104029.dist-info/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
240
- letta_nightly-0.6.19.dev20250130104029.dist-info/METADATA,sha256=0lkgLQ6npsu_hZ6TheMhqmYq98LGkxwms8xn3M-n8Dw,22156
241
- letta_nightly-0.6.19.dev20250130104029.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
242
- letta_nightly-0.6.19.dev20250130104029.dist-info/entry_points.txt,sha256=2zdiyGNEZGV5oYBuS-y2nAAgjDgcC9yM_mHJBFSRt5U,40
243
- letta_nightly-0.6.19.dev20250130104029.dist-info/RECORD,,
241
+ letta_nightly-0.6.20.dev20250131222205.dist-info/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
242
+ letta_nightly-0.6.20.dev20250131222205.dist-info/METADATA,sha256=8ZADA-ivhmqJvEe_nRcwlYJR9OADcnZkkZQkjJmuXic,22156
243
+ letta_nightly-0.6.20.dev20250131222205.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
244
+ letta_nightly-0.6.20.dev20250131222205.dist-info/entry_points.txt,sha256=2zdiyGNEZGV5oYBuS-y2nAAgjDgcC9yM_mHJBFSRt5U,40
245
+ letta_nightly-0.6.20.dev20250131222205.dist-info/RECORD,,