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.
- letta/__init__.py +2 -1
- letta/agent.py +1 -1
- letta/agents/base_agent.py +2 -2
- letta/agents/letta_agent.py +22 -8
- letta/agents/letta_agent_batch.py +2 -2
- letta/agents/voice_agent.py +2 -2
- letta/client/client.py +0 -11
- letta/data_sources/redis_client.py +1 -2
- letta/errors.py +11 -0
- letta/functions/function_sets/builtin.py +3 -7
- letta/functions/mcp_client/types.py +107 -1
- letta/helpers/reasoning_helper.py +48 -0
- letta/helpers/tool_execution_helper.py +2 -65
- letta/interfaces/openai_streaming_interface.py +38 -2
- letta/llm_api/anthropic_client.py +1 -5
- letta/llm_api/google_vertex_client.py +1 -1
- letta/llm_api/llm_client.py +1 -1
- letta/llm_api/openai_client.py +2 -0
- letta/llm_api/sample_response_jsons/lmstudio_embedding_list.json +3 -2
- letta/orm/agent.py +5 -0
- letta/orm/enums.py +0 -1
- letta/orm/file.py +0 -1
- letta/orm/files_agents.py +9 -9
- letta/orm/sandbox_config.py +1 -1
- letta/orm/sqlite_functions.py +15 -13
- letta/prompts/system/memgpt_generate_tool.txt +139 -0
- letta/schemas/agent.py +15 -1
- letta/schemas/enums.py +6 -0
- letta/schemas/file.py +3 -3
- letta/schemas/letta_ping.py +28 -0
- letta/schemas/letta_request.py +9 -0
- letta/schemas/letta_stop_reason.py +25 -0
- letta/schemas/llm_config.py +1 -0
- letta/schemas/mcp.py +16 -3
- letta/schemas/memory.py +5 -0
- letta/schemas/providers/lmstudio.py +7 -0
- letta/schemas/providers/ollama.py +11 -8
- letta/schemas/sandbox_config.py +17 -7
- letta/server/rest_api/app.py +2 -0
- letta/server/rest_api/routers/v1/agents.py +93 -30
- letta/server/rest_api/routers/v1/blocks.py +52 -0
- letta/server/rest_api/routers/v1/sandbox_configs.py +2 -1
- letta/server/rest_api/routers/v1/tools.py +43 -101
- letta/server/rest_api/streaming_response.py +121 -9
- letta/server/server.py +6 -10
- letta/services/agent_manager.py +41 -4
- letta/services/block_manager.py +63 -1
- letta/services/file_processor/chunker/line_chunker.py +20 -19
- letta/services/file_processor/file_processor.py +0 -2
- letta/services/file_processor/file_types.py +1 -2
- letta/services/files_agents_manager.py +46 -6
- letta/services/helpers/agent_manager_helper.py +185 -13
- letta/services/job_manager.py +4 -4
- letta/services/mcp/oauth_utils.py +6 -150
- letta/services/mcp_manager.py +120 -2
- letta/services/sandbox_config_manager.py +3 -5
- letta/services/tool_executor/builtin_tool_executor.py +13 -18
- letta/services/tool_executor/files_tool_executor.py +31 -27
- letta/services/tool_executor/mcp_tool_executor.py +10 -1
- letta/services/tool_executor/{tool_executor.py → sandbox_tool_executor.py} +14 -2
- letta/services/tool_executor/tool_execution_manager.py +1 -1
- letta/services/tool_executor/tool_execution_sandbox.py +2 -1
- letta/services/tool_manager.py +59 -21
- letta/services/tool_sandbox/base.py +18 -2
- letta/services/tool_sandbox/e2b_sandbox.py +5 -35
- letta/services/tool_sandbox/local_sandbox.py +5 -22
- letta/services/tool_sandbox/modal_sandbox.py +205 -0
- letta/settings.py +27 -8
- letta/system.py +1 -4
- letta/templates/template_helper.py +5 -0
- letta/utils.py +14 -2
- {letta_nightly-0.9.1.dev20250731104458.dist-info → letta_nightly-0.10.0.dev20250801060805.dist-info}/METADATA +7 -3
- {letta_nightly-0.9.1.dev20250731104458.dist-info → letta_nightly-0.10.0.dev20250801060805.dist-info}/RECORD +76 -73
- letta/orm/__all__.py +0 -15
- {letta_nightly-0.9.1.dev20250731104458.dist-info → letta_nightly-0.10.0.dev20250801060805.dist-info}/LICENSE +0 -0
- {letta_nightly-0.9.1.dev20250731104458.dist-info → letta_nightly-0.10.0.dev20250801060805.dist-info}/WHEEL +0 -0
- {letta_nightly-0.9.1.dev20250731104458.dist-info → letta_nightly-0.10.0.dev20250801060805.dist-info}/entry_points.txt +0 -0
letta/llm_api/openai_client.py
CHANGED
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
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
|
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,
|
letta/orm/sandbox_config.py
CHANGED
@@ -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
|
letta/orm/sqlite_functions.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
176
|
+
logger.info("Successfully registered cosine_distance for aiosqlite")
|
176
177
|
else:
|
177
178
|
dbapi_connection.create_function("cosine_distance", 2, cosine_distance)
|
178
|
-
|
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
|
-
|
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
|
-
|
187
|
-
sqlite3.
|
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.
|
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 agent
|
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
|
+
)
|
letta/schemas/letta_request.py
CHANGED
@@ -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
|
+
)
|
letta/schemas/llm_config.py
CHANGED
@@ -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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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=
|
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=
|
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),
|