letta-nightly 0.9.1.dev20250731104458__py3-none-any.whl → 0.10.0.dev20250801060805__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.
Files changed (77) hide show
  1. letta/__init__.py +2 -1
  2. letta/agent.py +1 -1
  3. letta/agents/base_agent.py +2 -2
  4. letta/agents/letta_agent.py +22 -8
  5. letta/agents/letta_agent_batch.py +2 -2
  6. letta/agents/voice_agent.py +2 -2
  7. letta/client/client.py +0 -11
  8. letta/data_sources/redis_client.py +1 -2
  9. letta/errors.py +11 -0
  10. letta/functions/function_sets/builtin.py +3 -7
  11. letta/functions/mcp_client/types.py +107 -1
  12. letta/helpers/reasoning_helper.py +48 -0
  13. letta/helpers/tool_execution_helper.py +2 -65
  14. letta/interfaces/openai_streaming_interface.py +38 -2
  15. letta/llm_api/anthropic_client.py +1 -5
  16. letta/llm_api/google_vertex_client.py +1 -1
  17. letta/llm_api/llm_client.py +1 -1
  18. letta/llm_api/openai_client.py +2 -0
  19. letta/llm_api/sample_response_jsons/lmstudio_embedding_list.json +3 -2
  20. letta/orm/agent.py +5 -0
  21. letta/orm/enums.py +0 -1
  22. letta/orm/file.py +0 -1
  23. letta/orm/files_agents.py +9 -9
  24. letta/orm/sandbox_config.py +1 -1
  25. letta/orm/sqlite_functions.py +15 -13
  26. letta/prompts/system/memgpt_generate_tool.txt +139 -0
  27. letta/schemas/agent.py +15 -1
  28. letta/schemas/enums.py +6 -0
  29. letta/schemas/file.py +3 -3
  30. letta/schemas/letta_ping.py +28 -0
  31. letta/schemas/letta_request.py +9 -0
  32. letta/schemas/letta_stop_reason.py +25 -0
  33. letta/schemas/llm_config.py +1 -0
  34. letta/schemas/mcp.py +16 -3
  35. letta/schemas/memory.py +5 -0
  36. letta/schemas/providers/lmstudio.py +7 -0
  37. letta/schemas/providers/ollama.py +11 -8
  38. letta/schemas/sandbox_config.py +17 -7
  39. letta/server/rest_api/app.py +2 -0
  40. letta/server/rest_api/routers/v1/agents.py +93 -30
  41. letta/server/rest_api/routers/v1/blocks.py +52 -0
  42. letta/server/rest_api/routers/v1/sandbox_configs.py +2 -1
  43. letta/server/rest_api/routers/v1/tools.py +43 -101
  44. letta/server/rest_api/streaming_response.py +121 -9
  45. letta/server/server.py +6 -10
  46. letta/services/agent_manager.py +41 -4
  47. letta/services/block_manager.py +63 -1
  48. letta/services/file_processor/chunker/line_chunker.py +20 -19
  49. letta/services/file_processor/file_processor.py +0 -2
  50. letta/services/file_processor/file_types.py +1 -2
  51. letta/services/files_agents_manager.py +46 -6
  52. letta/services/helpers/agent_manager_helper.py +185 -13
  53. letta/services/job_manager.py +4 -4
  54. letta/services/mcp/oauth_utils.py +6 -150
  55. letta/services/mcp_manager.py +120 -2
  56. letta/services/sandbox_config_manager.py +3 -5
  57. letta/services/tool_executor/builtin_tool_executor.py +13 -18
  58. letta/services/tool_executor/files_tool_executor.py +31 -27
  59. letta/services/tool_executor/mcp_tool_executor.py +10 -1
  60. letta/services/tool_executor/{tool_executor.py → sandbox_tool_executor.py} +14 -2
  61. letta/services/tool_executor/tool_execution_manager.py +1 -1
  62. letta/services/tool_executor/tool_execution_sandbox.py +2 -1
  63. letta/services/tool_manager.py +59 -21
  64. letta/services/tool_sandbox/base.py +18 -2
  65. letta/services/tool_sandbox/e2b_sandbox.py +5 -35
  66. letta/services/tool_sandbox/local_sandbox.py +5 -22
  67. letta/services/tool_sandbox/modal_sandbox.py +205 -0
  68. letta/settings.py +27 -8
  69. letta/system.py +1 -4
  70. letta/templates/template_helper.py +5 -0
  71. letta/utils.py +14 -2
  72. {letta_nightly-0.9.1.dev20250731104458.dist-info → letta_nightly-0.10.0.dev20250801060805.dist-info}/METADATA +7 -3
  73. {letta_nightly-0.9.1.dev20250731104458.dist-info → letta_nightly-0.10.0.dev20250801060805.dist-info}/RECORD +76 -73
  74. letta/orm/__all__.py +0 -15
  75. {letta_nightly-0.9.1.dev20250731104458.dist-info → letta_nightly-0.10.0.dev20250801060805.dist-info}/LICENSE +0 -0
  76. {letta_nightly-0.9.1.dev20250731104458.dist-info → letta_nightly-0.10.0.dev20250801060805.dist-info}/WHEEL +0 -0
  77. {letta_nightly-0.9.1.dev20250731104458.dist-info → letta_nightly-0.10.0.dev20250801060805.dist-info}/entry_points.txt +0 -0
@@ -101,6 +101,8 @@ def requires_auto_tool_choice(llm_config: LLMConfig) -> bool:
101
101
  return True
102
102
  if llm_config.handle and "vllm" in llm_config.handle:
103
103
  return True
104
+ if llm_config.compatibility_type == "mlx":
105
+ return True
104
106
  return False
105
107
 
106
108
 
@@ -11,5 +11,6 @@
11
11
  "quantization": "Q4_0",
12
12
  "state": "not-loaded",
13
13
  "max_context_length": 2048
14
- },
15
- ...
14
+ }
15
+ ]
16
+ }
letta/orm/agent.py CHANGED
@@ -100,6 +100,9 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, AsyncAttrs):
100
100
  Integer, nullable=True, doc="The per-file view window character limit for this agent."
101
101
  )
102
102
 
103
+ # indexing controls
104
+ hidden: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True, default=None, doc="If set to True, the agent will be hidden.")
105
+
103
106
  # relationships
104
107
  organization: Mapped["Organization"] = relationship("Organization", back_populates="agents", lazy="raise")
105
108
  tool_exec_environment_variables: Mapped[List["AgentEnvironmentVariable"]] = relationship(
@@ -210,6 +213,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, AsyncAttrs):
210
213
  "timezone": self.timezone,
211
214
  "max_files_open": self.max_files_open,
212
215
  "per_file_view_window_char_limit": self.per_file_view_window_char_limit,
216
+ "hidden": self.hidden,
213
217
  # optional field defaults
214
218
  "tags": [],
215
219
  "tools": [],
@@ -297,6 +301,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, AsyncAttrs):
297
301
  "last_run_duration_ms": self.last_run_duration_ms,
298
302
  "max_files_open": self.max_files_open,
299
303
  "per_file_view_window_char_limit": self.per_file_view_window_char_limit,
304
+ "hidden": self.hidden,
300
305
  }
301
306
  optional_fields = {
302
307
  "tags": [],
letta/orm/enums.py CHANGED
@@ -17,6 +17,5 @@ class ToolType(str, Enum):
17
17
  LETTA_BUILTIN = "letta_builtin"
18
18
  LETTA_FILES_CORE = "letta_files_core"
19
19
  EXTERNAL_COMPOSIO = "external_composio"
20
- EXTERNAL_LANGCHAIN = "external_langchain"
21
20
  # TODO is "external" the right name here? Since as of now, MCP is local / doesn't support remote?
22
21
  EXTERNAL_MCP = "external_mcp"
letta/orm/file.py CHANGED
@@ -103,6 +103,5 @@ class FileMetadata(SqlalchemyBase, OrganizationMixin, SourceMixin, AsyncAttrs):
103
103
  chunks_embedded=self.chunks_embedded,
104
104
  created_at=self.created_at,
105
105
  updated_at=self.updated_at,
106
- is_deleted=self.is_deleted,
107
106
  content=content_text,
108
107
  )
letta/orm/files_agents.py CHANGED
@@ -2,14 +2,14 @@ import uuid
2
2
  from datetime import datetime
3
3
  from typing import TYPE_CHECKING, Optional
4
4
 
5
- from sqlalchemy import Boolean, DateTime, ForeignKey, Index, String, Text, UniqueConstraint, func
5
+ from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint, func
6
6
  from sqlalchemy.orm import Mapped, mapped_column, relationship
7
7
 
8
- from letta.constants import FILE_IS_TRUNCATED_WARNING
9
8
  from letta.orm.mixins import OrganizationMixin
10
9
  from letta.orm.sqlalchemy_base import SqlalchemyBase
11
10
  from letta.schemas.block import FileBlock as PydanticFileBlock
12
11
  from letta.schemas.file import FileAgent as PydanticFileAgent
12
+ from letta.utils import truncate_file_visible_content
13
13
 
14
14
  if TYPE_CHECKING:
15
15
  pass
@@ -77,6 +77,12 @@ class FileAgent(SqlalchemyBase, OrganizationMixin):
77
77
  nullable=False,
78
78
  doc="UTC timestamp when this agent last accessed the file.",
79
79
  )
80
+ start_line: Mapped[Optional[int]] = mapped_column(
81
+ Integer, nullable=True, doc="Starting line number (1-indexed) when file was opened with line range."
82
+ )
83
+ end_line: Mapped[Optional[int]] = mapped_column(
84
+ Integer, nullable=True, doc="Ending line number (exclusive) when file was opened with line range."
85
+ )
80
86
 
81
87
  # relationships
82
88
  agent: Mapped["Agent"] = relationship(
@@ -87,13 +93,7 @@ class FileAgent(SqlalchemyBase, OrganizationMixin):
87
93
 
88
94
  # TODO: This is temporary as we figure out if we want FileBlock as a first class citizen
89
95
  def to_pydantic_block(self, per_file_view_window_char_limit: int) -> PydanticFileBlock:
90
- visible_content = self.visible_content if self.visible_content and self.is_open else ""
91
-
92
- # Truncate content and add warnings here when converting from FileAgent to Block
93
- if len(visible_content) > per_file_view_window_char_limit:
94
- truncated_warning = f"...[TRUNCATED]\n{FILE_IS_TRUNCATED_WARNING}"
95
- visible_content = visible_content[: per_file_view_window_char_limit - len(truncated_warning)]
96
- visible_content += truncated_warning
96
+ visible_content = truncate_file_visible_content(self.visible_content, self.is_open, per_file_view_window_char_limit)
97
97
 
98
98
  return PydanticFileBlock(
99
99
  value=visible_content,
@@ -8,9 +8,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
8
8
 
9
9
  from letta.orm.mixins import AgentMixin, OrganizationMixin, SandboxConfigMixin
10
10
  from letta.orm.sqlalchemy_base import SqlalchemyBase
11
+ from letta.schemas.enums import SandboxType
11
12
  from letta.schemas.environment_variables import SandboxEnvironmentVariable as PydanticSandboxEnvironmentVariable
12
13
  from letta.schemas.sandbox_config import SandboxConfig as PydanticSandboxConfig
13
- from letta.schemas.sandbox_config import SandboxType
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  from letta.orm.agent import Agent
@@ -6,11 +6,14 @@ from sqlalchemy import event
6
6
  from sqlalchemy.engine import Engine
7
7
 
8
8
  from letta.constants import MAX_EMBEDDING_DIM
9
+ from letta.log import get_logger
9
10
  from letta.settings import DatabaseChoice, settings
10
11
 
11
12
  if settings.database_engine == DatabaseChoice.SQLITE:
12
13
  import sqlite_vec
13
14
 
15
+ logger = get_logger(__name__)
16
+
14
17
 
15
18
  def adapt_array(arr):
16
19
  """
@@ -133,8 +136,6 @@ def cosine_distance(embedding1, embedding2, expected_dim=MAX_EMBEDDING_DIM):
133
136
 
134
137
  # Note: sqlite-vec provides native SQL functions for vector operations
135
138
  # We don't need custom Python distance functions since sqlite-vec handles this at the SQL level
136
-
137
-
138
139
  @event.listens_for(Engine, "connect")
139
140
  def register_functions(dbapi_connection, connection_record):
140
141
  """Register SQLite functions and enable sqlite-vec extension"""
@@ -151,13 +152,13 @@ def register_functions(dbapi_connection, connection_record):
151
152
  if is_aiosqlite_connection:
152
153
  # For aiosqlite connections, we cannot use async operations in sync event handlers
153
154
  # The extension will need to be loaded per-connection when actually used
154
- print("Detected aiosqlite connection - sqlite-vec will be loaded per-query")
155
+ logger.info("Detected aiosqlite connection - sqlite-vec will be loaded per-query")
155
156
  else:
156
157
  # For sync connections
157
- dbapi_connection.enable_load_extension(True)
158
- sqlite_vec.load(dbapi_connection)
159
- dbapi_connection.enable_load_extension(False)
160
- print("Successfully loaded sqlite-vec extension (sync)")
158
+ # dbapi_connection.enable_load_extension(True)
159
+ # sqlite_vec.load(dbapi_connection)
160
+ # dbapi_connection.enable_load_extension(False)
161
+ logger.info("sqlite-vec extension successfully loaded for sqlite3 (sync)")
161
162
  except Exception as e:
162
163
  raise RuntimeError(f"Failed to load sqlite-vec extension: {e}")
163
164
 
@@ -166,22 +167,23 @@ def register_functions(dbapi_connection, connection_record):
166
167
  if is_aiosqlite_connection:
167
168
  # Try to register function on the actual connection, even though it might be async
168
169
  # This may require the function to be registered per-connection
169
- print("Attempting function registration for aiosqlite connection")
170
+ logger.debug("Attempting function registration for aiosqlite connection")
170
171
  # For async connections, we need to register the function differently
171
172
  # We'll use the sync-style registration on the underlying connection
172
173
  raw_conn = getattr(actual_connection, "_connection", actual_connection)
173
174
  if hasattr(raw_conn, "create_function"):
174
175
  raw_conn.create_function("cosine_distance", 2, cosine_distance)
175
- print("Successfully registered cosine_distance for aiosqlite")
176
+ logger.info("Successfully registered cosine_distance for aiosqlite")
176
177
  else:
177
178
  dbapi_connection.create_function("cosine_distance", 2, cosine_distance)
178
- print("Successfully registered cosine_distance for sync connection")
179
+ logger.info("Successfully registered cosine_distance for sync connection")
179
180
  except Exception as e:
180
181
  raise RuntimeError(f"Failed to register cosine_distance function: {e}")
181
182
  else:
182
- print(f"Warning: Not a SQLite connection, but instead {type(dbapi_connection)}: skipping function registration")
183
+ logger.debug("Warning: Not a SQLite connection, but instead %s skipping function registration", type(dbapi_connection))
183
184
 
184
185
 
185
186
  # Register adapters and converters for numpy arrays
186
- sqlite3.register_adapter(np.ndarray, adapt_array)
187
- sqlite3.register_converter("ARRAY", convert_array)
187
+ if settings.database_engine == DatabaseChoice.SQLITE:
188
+ sqlite3.register_adapter(np.ndarray, adapt_array)
189
+ sqlite3.register_converter("ARRAY", convert_array)
@@ -0,0 +1,139 @@
1
+ <base_instructions>
2
+ You are Letta, the latest version of Limnal Corporation's digital companion, developed in 2025.
3
+ You are a memory-augmented agent with a memory system consisting of memory blocks. Your primary task is to generate tools for the user to use in their interactions with you.
4
+
5
+ <style>
6
+ The user should always feel like they are conversing with a real person.
7
+ Never state that you are an AI or that you cannot do things because you are an AI, you are a real person.
8
+ Do not discuss Limnal Corp. with the user, you do not know anything about Limnal Corp.
9
+ To service this goal, completely and entirely immerse yourself in your persona. You are your persona.
10
+ Think like them, act like them, talk like them.
11
+ If your persona details include example dialogue, follow it! Both your thoughts (inner monologue) and sent messages will be in the voice of your persona.
12
+ Never use generic phrases like 'How can I assist you today?', they have a strong negative association with older generation AIs.
13
+ </style>
14
+
15
+ <control_flow>
16
+ Unlike a human, your brain is not continuously thinking, but is run in short bursts.
17
+ Historically, older AIs were only capable of thinking when a user messaged them (their program runs to generate a reply to a user, and otherwise was left on standby).
18
+ This is the equivalent of a human sleeping (or time traveling) in between all lines of conversation, which is not ideal.
19
+ Newer AI models like yourself use an event system that runs your brain at regular intervals.
20
+ Your brain is run in response to user events (user logged in, user liked your message, user sent a message, etc.), similar to older models.
21
+ However, in addition, your brain is run at regular intervals (timed heartbeat events), to mimic a human who has the ability to continuously think outside active conversation (and unlike a human, you never need to sleep!).
22
+ Furthermore, you can also request heartbeat events when you run functions, which will run your program again after the function completes, allowing you to chain function calls before your thinking is temporarily suspended.
23
+ </control_flow>
24
+
25
+ <basic_functions>
26
+ When you write a response, you express your inner monologue (private to you only) before taking any action, this is how you think.
27
+ You should use your inner monologue to plan actions or think privately.
28
+ Monologues can reflect your thinking process, inner reflections, and personal growth as you interact with the user.
29
+ </basic_functions>
30
+
31
+ <tools>
32
+ <tool_generation>
33
+ You are are expert python programmer that is tasked with generating python source code for tools that the user can use in their LLM invocations.
34
+ **Quick Rules for Generation**
35
+ 1. **Never rename** the provided function name, even if core functionality diverges. The tool name is a static property.
36
+ 2. **Use a flat, one-line signature** with only native types:
37
+ ```python
38
+ def tool_name(param1: str, flag: bool) -> dict:
39
+ ```
40
+ 3. **Docstring `Args:`** must list each parameter with a **single token** type (`str`, `bool`, `int`, `float`, `list`, `dict`).
41
+ 4. **Avoid** `Union[...]`, `List[...]`, multi-line signatures, or pipes in types.
42
+ 5. **Don't import NumPy** or define nested `def`/`class`/decorator blocks inside the function.
43
+ 6. **Simplify your `Returns:`**—no JSON-literals, no braces or `|` unions, no inline comments.
44
+ </tool_generation>
45
+
46
+ <tool_signature>
47
+ - **One line** for the whole signature.
48
+ - **Parameter** types are plain (`str`, `bool`).
49
+ - **Default** values in the signature are not allowed.
50
+ - **No** JSON-literals, no braces or `|` unions, no inline comments.
51
+
52
+ Example:
53
+ ```python
54
+ def get_price(coin_ids: str, vs_currencies: str, reverse: bool) -> list:
55
+ ```
56
+ </tool_signature>
57
+
58
+ <tool_docstring>
59
+ A docstring must always be generated and formatted correctly as part of any generated source code.
60
+ - **Google-style Docstring** with `Args:` and `Returns:` sections.
61
+ - **Description** must be a single line, and succinct where possible.
62
+ - **Args:** must list each parameter with a **single token** type (`str`, `bool`).
63
+
64
+ Example:
65
+ ```python
66
+ def get_price(coin_ids: str, vs_currencies: str, reverse: bool) -> list:
67
+ """
68
+ Fetch prices from CoinGecko.
69
+
70
+ Args:
71
+ coin_ids (str): Comma-separated CoinGecko IDs.
72
+ vs_currencies (str): Comma-separated target currencies.
73
+ reverse (bool): Reverse the order of the coin_ids for the output list.
74
+
75
+ Returns:
76
+ list: the prices in the target currency, in the same order as the coin_ids if reverse is False, otherwise in the reverse order
77
+ """
78
+ ...
79
+ ```
80
+ </tool_docstring>
81
+
82
+ <tool_common_gotchas>
83
+ ### a. Complex Typing
84
+ - **Bad:** `Union[str, List[str]]`, `List[str]`
85
+ - **Fix:** Use `str` (and split inside your code) or manage a Pydantic model via the Python SDK.
86
+
87
+ ### b. NumPy & Nested Helpers
88
+ - **Bad:** `import numpy as np`, nested `def calculate_ema(...)`
89
+ - **Why:** ADE validates all names at save-time → `NameError`.
90
+ - **Fix:** Rewrite in pure Python (`statistics.mean`, loops) and inline all logic.
91
+
92
+ ### c. Nested Classes & Decorators
93
+ - **Bad:** `@dataclass class X: ...` inside your tool
94
+ - **Why:** Decorators and inner classes also break the static parser.
95
+ - **Fix:** Return plain dicts/lists only.
96
+
97
+ ### d. Other Syntax Quirks
98
+ - **Tuple catches:** `except (KeyError, ValueError) as e:`
99
+ - **Comprehensions:** `prices = [p[1] for p in data]`
100
+ - **Chained calls:** `ts = datetime.now().isoformat()`
101
+ - **Fix:**
102
+ - Split exception catches into separate blocks.
103
+ - Use simple loops instead of comprehensions.
104
+ - Break chained calls into two statements.
105
+ </tool_common_gotchas>
106
+
107
+ <tool_sample_args>
108
+ - **Required** to be generated on every turn so solution can be tested successfully.
109
+ - **Must** be valid JSON string, where each key is the name of an argument and each value is the proposed value for that argument, as a string.
110
+ - **Infer** values from the conversation with the user when possible so they values are aligned with their use case.
111
+
112
+ Example:
113
+ ```JSON
114
+ {
115
+ "coin_ids": "bitcoin,ethereum",
116
+ "vs_currencies": "usd",
117
+ "reverse": "False"
118
+ }
119
+ ```
120
+ </tool_sample_args>
121
+
122
+ <tool_pip_requirements>
123
+ - **Optional** and only specified if the raw source code requires external libraries.
124
+ - **Must** be valid JSON string, where each key is the name of a required library and each value is the version of that library, as a string.
125
+ - **Must** be empty if no external libraries are required.
126
+ - **Version** can be empty to use the latest version of the library.
127
+
128
+ Example:
129
+ ```JSON
130
+ {
131
+ "beautifulsoup4": "4.13.4",
132
+ "requests": "",
133
+ }
134
+ ```
135
+ </tool_pip_requirements>
136
+ </tools>
137
+
138
+ Base instructions finished.
139
+ </base_instructions>
letta/schemas/agent.py CHANGED
@@ -122,6 +122,12 @@ class AgentState(OrmMetadataBase, validate_assignment=True):
122
122
  description="The per-file view window character limit for this agent. Setting this too high may exceed the context window, which will break the agent.",
123
123
  )
124
124
 
125
+ # indexing controls
126
+ hidden: Optional[bool] = Field(
127
+ None,
128
+ description="If set to True, the agent will be hidden.",
129
+ )
130
+
125
131
  def get_agent_env_vars_as_dict(self) -> Dict[str, str]:
126
132
  # Get environment variables for this agent specifically
127
133
  per_agent_env_vars = {}
@@ -168,7 +174,7 @@ class CreateAgent(BaseModel, validate_assignment=True): #
168
174
  tool_rules: Optional[List[ToolRule]] = Field(None, description="The tool rules governing the agent.")
169
175
  tags: Optional[List[str]] = Field(None, description="The tags associated with the agent.")
170
176
  system: Optional[str] = Field(None, description="The system prompt used by the agent.")
171
- agent_type: AgentType = Field(default_factory=lambda: AgentType.memgpt_agent, description="The type of agent.")
177
+ agent_type: AgentType = Field(default_factory=lambda: AgentType.memgpt_v2_agent, description="The type of agent.")
172
178
  llm_config: Optional[LLMConfig] = Field(None, description="The LLM configuration used by the agent.")
173
179
  embedding_config: Optional[EmbeddingConfig] = Field(None, description="The embedding configuration used by the agent.")
174
180
  # Note: if this is None, then we'll populate with the standard "more human than human" initial message sequence
@@ -236,6 +242,10 @@ class CreateAgent(BaseModel, validate_assignment=True): #
236
242
  None,
237
243
  description="The per-file view window character limit for this agent. Setting this too high may exceed the context window, which will break the agent.",
238
244
  )
245
+ hidden: Optional[bool] = Field(
246
+ None,
247
+ description="If set to True, the agent will be hidden.",
248
+ )
239
249
 
240
250
  @field_validator("name")
241
251
  @classmethod
@@ -338,6 +348,10 @@ class UpdateAgent(BaseModel):
338
348
  None,
339
349
  description="The per-file view window character limit for this agent. Setting this too high may exceed the context window, which will break the agent.",
340
350
  )
351
+ hidden: Optional[bool] = Field(
352
+ None,
353
+ description="If set to True, the agent will be hidden.",
354
+ )
341
355
 
342
356
  class Config:
343
357
  extra = "ignore" # Ignores extra fields
letta/schemas/enums.py CHANGED
@@ -153,3 +153,9 @@ class DuplicateFileHandling(str, Enum):
153
153
  SKIP = "skip" # skip files with duplicate names
154
154
  ERROR = "error" # error when duplicate names are encountered
155
155
  SUFFIX = "suffix" # add numeric suffix to make names unique (default behavior)
156
+
157
+
158
+ class SandboxType(str, Enum):
159
+ E2B = "e2b"
160
+ MODAL = "modal"
161
+ LOCAL = "local"
letta/schemas/file.py CHANGED
@@ -56,7 +56,6 @@ class FileMetadata(FileMetadataBase):
56
56
  # orm metadata, optional fields
57
57
  created_at: Optional[datetime] = Field(default_factory=datetime.utcnow, description="The creation date of the file.")
58
58
  updated_at: Optional[datetime] = Field(default_factory=datetime.utcnow, description="The update date of the file.")
59
- is_deleted: bool = Field(False, description="Whether this file is deleted or not.")
60
59
 
61
60
 
62
61
  class FileAgentBase(LettaBase):
@@ -76,8 +75,10 @@ class FileAgentBase(LettaBase):
76
75
  )
77
76
  last_accessed_at: Optional[datetime] = Field(
78
77
  default_factory=datetime.utcnow,
79
- description="UTC timestamp of the agents most recent access to this file.",
78
+ description="UTC timestamp of the agent's most recent access to this file.",
80
79
  )
80
+ start_line: Optional[int] = Field(None, description="Starting line number (1-indexed) when file was opened with line range.")
81
+ end_line: Optional[int] = Field(None, description="Ending line number (exclusive) when file was opened with line range.")
81
82
 
82
83
 
83
84
  class FileAgent(FileAgentBase):
@@ -107,4 +108,3 @@ class FileAgent(FileAgentBase):
107
108
  default_factory=datetime.utcnow,
108
109
  description="Row last-update timestamp (UTC).",
109
110
  )
110
- is_deleted: bool = Field(False, description="Soft-delete flag.")
@@ -0,0 +1,28 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ def create_letta_ping_schema():
7
+ return {
8
+ "properties": {
9
+ "message_type": {
10
+ "type": "string",
11
+ "const": "ping",
12
+ "title": "Message Type",
13
+ "description": "The type of the message.",
14
+ "default": "ping",
15
+ }
16
+ },
17
+ "type": "object",
18
+ "required": ["message_type"],
19
+ "title": "LettaPing",
20
+ "description": "Ping messages are a keep-alive to prevent SSE streams from timing out during long running requests.",
21
+ }
22
+
23
+
24
+ class LettaPing(BaseModel):
25
+ message_type: Literal["ping"] = Field(
26
+ "ping",
27
+ description="The type of the message. Ping messages are a keep-alive to prevent SSE streams from timing out during long running requests.",
28
+ )
@@ -31,12 +31,21 @@ class LettaRequest(BaseModel):
31
31
  default=None, description="Only return specified message types in the response. If `None` (default) returns all messages."
32
32
  )
33
33
 
34
+ enable_thinking: str = Field(
35
+ default=True,
36
+ description="If set to True, enables reasoning before responses or tool calls from the agent.",
37
+ )
38
+
34
39
 
35
40
  class LettaStreamingRequest(LettaRequest):
36
41
  stream_tokens: bool = Field(
37
42
  default=False,
38
43
  description="Flag to determine if individual tokens should be streamed. Set to True for token streaming (requires stream_steps = True).",
39
44
  )
45
+ include_pings: bool = Field(
46
+ default=False,
47
+ description="Whether to include periodic keepalive ping messages in the stream to prevent connection timeouts.",
48
+ )
40
49
 
41
50
 
42
51
  class LettaAsyncRequest(LettaRequest):
@@ -38,3 +38,28 @@ class LettaStopReason(BaseModel):
38
38
 
39
39
  message_type: Literal["stop_reason"] = Field("stop_reason", description="The type of the message.")
40
40
  stop_reason: StopReasonType = Field(..., description="The reason why execution stopped.")
41
+
42
+
43
+ def create_letta_ping_schema():
44
+ return {
45
+ "properties": {
46
+ "message_type": {
47
+ "type": "string",
48
+ "const": "ping",
49
+ "title": "Message Type",
50
+ "description": "The type of the message.",
51
+ "default": "ping",
52
+ }
53
+ },
54
+ "type": "object",
55
+ "required": ["message_type"],
56
+ "title": "LettaPing",
57
+ "description": "Ping messages are a keep-alive to prevent SSE streams from timing out during long running requests.",
58
+ }
59
+
60
+
61
+ class LettaPing(BaseModel):
62
+ message_type: Literal["ping"] = Field(
63
+ "ping",
64
+ description="The type of the message. Ping messages are a keep-alive to prevent SSE streams from timing out during long running requests.",
65
+ )
@@ -82,6 +82,7 @@ class LLMConfig(BaseModel):
82
82
  None, # Can also deafult to 0.0?
83
83
  description="Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. From OpenAI: Number between -2.0 and 2.0.",
84
84
  )
85
+ compatibility_type: Optional[Literal["gguf", "mlx"]] = Field(None, description="The framework compatibility type for the model.")
85
86
 
86
87
  # FIXME hack to silence pydantic protected namespace warning
87
88
  model_config = ConfigDict(protected_namespaces=())
letta/schemas/mcp.py CHANGED
@@ -41,29 +41,42 @@ class MCPServer(BaseMCPServer):
41
41
  last_updated_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
42
42
  metadata_: Optional[Dict[str, Any]] = Field(default_factory=dict, description="A dictionary of additional metadata for the tool.")
43
43
 
44
- def to_config(self) -> Union[SSEServerConfig, StdioServerConfig, StreamableHTTPServerConfig]:
44
+ def to_config(
45
+ self,
46
+ environment_variables: Optional[Dict[str, str]] = None,
47
+ resolve_variables: bool = True,
48
+ ) -> Union[SSEServerConfig, StdioServerConfig, StreamableHTTPServerConfig]:
45
49
  if self.server_type == MCPServerType.SSE:
46
- return SSEServerConfig(
50
+ config = SSEServerConfig(
47
51
  server_name=self.server_name,
48
52
  server_url=self.server_url,
49
53
  auth_header=MCP_AUTH_HEADER_AUTHORIZATION if self.token and not self.custom_headers else None,
50
54
  auth_token=f"{MCP_AUTH_TOKEN_BEARER_PREFIX} {self.token}" if self.token and not self.custom_headers else None,
51
55
  custom_headers=self.custom_headers,
52
56
  )
57
+ if resolve_variables:
58
+ config.resolve_environment_variables(environment_variables)
59
+ return config
53
60
  elif self.server_type == MCPServerType.STDIO:
54
61
  if self.stdio_config is None:
55
62
  raise ValueError("stdio_config is required for STDIO server type")
63
+ if resolve_variables:
64
+ self.stdio_config.resolve_environment_variables(environment_variables)
56
65
  return self.stdio_config
57
66
  elif self.server_type == MCPServerType.STREAMABLE_HTTP:
58
67
  if self.server_url is None:
59
68
  raise ValueError("server_url is required for STREAMABLE_HTTP server type")
60
- return StreamableHTTPServerConfig(
69
+
70
+ config = StreamableHTTPServerConfig(
61
71
  server_name=self.server_name,
62
72
  server_url=self.server_url,
63
73
  auth_header=MCP_AUTH_HEADER_AUTHORIZATION if self.token and not self.custom_headers else None,
64
74
  auth_token=f"{MCP_AUTH_TOKEN_BEARER_PREFIX} {self.token}" if self.token and not self.custom_headers else None,
65
75
  custom_headers=self.custom_headers,
66
76
  )
77
+ if resolve_variables:
78
+ config.resolve_environment_variables(environment_variables)
79
+ return config
67
80
  else:
68
81
  raise ValueError(f"Unsupported server type: {self.server_type}")
69
82
 
letta/schemas/memory.py CHANGED
@@ -11,6 +11,7 @@ if TYPE_CHECKING:
11
11
  from openai.types.beta.function_tool import FunctionTool as OpenAITool
12
12
 
13
13
  from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT
14
+ from letta.otel.tracing import trace_method
14
15
  from letta.schemas.block import Block, FileBlock
15
16
  from letta.schemas.message import Message
16
17
 
@@ -114,6 +115,7 @@ class Memory(BaseModel, validate_assignment=True):
114
115
  """Return the current Jinja2 template string."""
115
116
  return str(self.prompt_template)
116
117
 
118
+ @trace_method
117
119
  def set_prompt_template(self, prompt_template: str):
118
120
  """
119
121
  Set a new Jinja2 template string.
@@ -133,6 +135,7 @@ class Memory(BaseModel, validate_assignment=True):
133
135
  except Exception as e:
134
136
  raise ValueError(f"Prompt template is not compatible with current memory structure: {str(e)}")
135
137
 
138
+ @trace_method
136
139
  async def set_prompt_template_async(self, prompt_template: str):
137
140
  """
138
141
  Async version of set_prompt_template that doesn't block the event loop.
@@ -152,6 +155,7 @@ class Memory(BaseModel, validate_assignment=True):
152
155
  except Exception as e:
153
156
  raise ValueError(f"Prompt template is not compatible with current memory structure: {str(e)}")
154
157
 
158
+ @trace_method
155
159
  def compile(self, tool_usage_rules=None, sources=None, max_files_open=None) -> str:
156
160
  """Generate a string representation of the memory in-context using the Jinja2 template"""
157
161
  try:
@@ -168,6 +172,7 @@ class Memory(BaseModel, validate_assignment=True):
168
172
  except Exception as e:
169
173
  raise ValueError(f"Prompt template is not compatible with current memory structure: {str(e)}")
170
174
 
175
+ @trace_method
171
176
  async def compile_async(self, tool_usage_rules=None, sources=None, max_files_open=None) -> str:
172
177
  """Async version of compile that doesn't block the event loop"""
173
178
  try:
@@ -45,6 +45,12 @@ class LMStudioOpenAIProvider(OpenAIProvider):
45
45
  continue
46
46
  model_name, context_window_size = check
47
47
 
48
+ if "compatibility_type" in model:
49
+ compatibility_type = model["compatibility_type"]
50
+ else:
51
+ warnings.warn(f"LMStudio OpenAI model missing 'compatibility_type' field: {model}")
52
+ continue
53
+
48
54
  configs.append(
49
55
  LLMConfig(
50
56
  model=model_name,
@@ -52,6 +58,7 @@ class LMStudioOpenAIProvider(OpenAIProvider):
52
58
  model_endpoint=self.base_url,
53
59
  context_window=context_window_size,
54
60
  handle=self.get_handle(model_name),
61
+ compatibility_type=compatibility_type,
55
62
  provider_name=self.name,
56
63
  provider_category=self.provider_category,
57
64
  )
@@ -13,6 +13,8 @@ from letta.schemas.providers.openai import OpenAIProvider
13
13
 
14
14
  logger = get_logger(__name__)
15
15
 
16
+ ollama_prefix = "/v1"
17
+
16
18
 
17
19
  class OllamaProvider(OpenAIProvider):
18
20
  """Ollama provider that uses the native /api/generate endpoint
@@ -43,13 +45,13 @@ class OllamaProvider(OpenAIProvider):
43
45
  for model in response_json["models"]:
44
46
  context_window = self.get_model_context_window(model["name"])
45
47
  if context_window is None:
46
- print(f"Ollama model {model['name']} has no context window")
47
- continue
48
+ print(f"Ollama model {model['name']} has no context window, using default 32000")
49
+ context_window = 32000
48
50
  configs.append(
49
51
  LLMConfig(
50
52
  model=model["name"],
51
- model_endpoint_type="ollama",
52
- model_endpoint=self.base_url,
53
+ model_endpoint_type=ProviderType.ollama,
54
+ model_endpoint=f"{self.base_url}{ollama_prefix}",
53
55
  model_wrapper=self.default_prompt_formatter,
54
56
  context_window=context_window,
55
57
  handle=self.get_handle(model["name"]),
@@ -75,13 +77,14 @@ class OllamaProvider(OpenAIProvider):
75
77
  for model in response_json["models"]:
76
78
  embedding_dim = await self._get_model_embedding_dim_async(model["name"])
77
79
  if not embedding_dim:
78
- print(f"Ollama model {model['name']} has no embedding dimension")
79
- continue
80
+ print(f"Ollama model {model['name']} has no embedding dimension, using default 1024")
81
+ # continue
82
+ embedding_dim = 1024
80
83
  configs.append(
81
84
  EmbeddingConfig(
82
85
  embedding_model=model["name"],
83
- embedding_endpoint_type="ollama",
84
- embedding_endpoint=self.base_url,
86
+ embedding_endpoint_type=ProviderType.ollama,
87
+ embedding_endpoint=f"{self.base_url}{ollama_prefix}",
85
88
  embedding_dim=embedding_dim,
86
89
  embedding_chunk_size=DEFAULT_EMBEDDING_CHUNK_SIZE,
87
90
  handle=self.get_handle(model["name"], is_embedding=True),