letta-nightly 0.7.30.dev20250603104343__py3-none-any.whl → 0.8.0.dev20250604104349__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 +7 -1
- letta/agent.py +14 -7
- letta/agents/base_agent.py +1 -0
- letta/agents/ephemeral_summary_agent.py +104 -0
- letta/agents/helpers.py +35 -3
- letta/agents/letta_agent.py +492 -176
- letta/agents/letta_agent_batch.py +22 -16
- letta/agents/prompts/summary_system_prompt.txt +62 -0
- letta/agents/voice_agent.py +22 -7
- letta/agents/voice_sleeptime_agent.py +13 -8
- letta/constants.py +33 -1
- letta/data_sources/connectors.py +52 -36
- letta/errors.py +4 -0
- letta/functions/ast_parsers.py +13 -30
- letta/functions/function_sets/base.py +3 -1
- letta/functions/functions.py +2 -0
- letta/functions/mcp_client/base_client.py +151 -97
- letta/functions/mcp_client/sse_client.py +49 -31
- letta/functions/mcp_client/stdio_client.py +107 -106
- letta/functions/schema_generator.py +22 -22
- letta/groups/helpers.py +3 -4
- letta/groups/sleeptime_multi_agent.py +4 -4
- letta/groups/sleeptime_multi_agent_v2.py +22 -0
- letta/helpers/composio_helpers.py +16 -0
- letta/helpers/converters.py +20 -0
- letta/helpers/datetime_helpers.py +1 -6
- letta/helpers/tool_rule_solver.py +2 -1
- letta/interfaces/anthropic_streaming_interface.py +17 -2
- letta/interfaces/openai_chat_completions_streaming_interface.py +1 -0
- letta/interfaces/openai_streaming_interface.py +18 -2
- letta/llm_api/anthropic_client.py +24 -3
- letta/llm_api/google_ai_client.py +0 -15
- letta/llm_api/google_vertex_client.py +6 -5
- letta/llm_api/llm_client_base.py +15 -0
- letta/llm_api/openai.py +2 -2
- letta/llm_api/openai_client.py +60 -8
- letta/orm/__init__.py +2 -0
- letta/orm/agent.py +45 -43
- letta/orm/base.py +0 -2
- letta/orm/block.py +1 -0
- letta/orm/custom_columns.py +13 -0
- letta/orm/enums.py +5 -0
- letta/orm/file.py +3 -1
- letta/orm/files_agents.py +68 -0
- letta/orm/mcp_server.py +48 -0
- letta/orm/message.py +1 -0
- letta/orm/organization.py +11 -2
- letta/orm/passage.py +25 -10
- letta/orm/sandbox_config.py +5 -2
- letta/orm/sqlalchemy_base.py +171 -110
- letta/prompts/system/memgpt_base.txt +6 -1
- letta/prompts/system/memgpt_v2_chat.txt +57 -0
- letta/prompts/system/sleeptime.txt +2 -0
- letta/prompts/system/sleeptime_v2.txt +28 -0
- letta/schemas/agent.py +87 -20
- letta/schemas/block.py +7 -1
- letta/schemas/file.py +57 -0
- letta/schemas/mcp.py +74 -0
- letta/schemas/memory.py +5 -2
- letta/schemas/message.py +9 -0
- letta/schemas/openai/openai.py +0 -6
- letta/schemas/providers.py +33 -4
- letta/schemas/tool.py +26 -21
- letta/schemas/tool_execution_result.py +5 -0
- letta/server/db.py +23 -8
- letta/server/rest_api/app.py +73 -56
- letta/server/rest_api/interface.py +4 -4
- letta/server/rest_api/routers/v1/agents.py +132 -47
- letta/server/rest_api/routers/v1/blocks.py +3 -2
- letta/server/rest_api/routers/v1/embeddings.py +3 -3
- letta/server/rest_api/routers/v1/groups.py +3 -3
- letta/server/rest_api/routers/v1/jobs.py +14 -17
- letta/server/rest_api/routers/v1/organizations.py +10 -10
- letta/server/rest_api/routers/v1/providers.py +12 -10
- letta/server/rest_api/routers/v1/runs.py +3 -3
- letta/server/rest_api/routers/v1/sandbox_configs.py +12 -12
- letta/server/rest_api/routers/v1/sources.py +108 -43
- letta/server/rest_api/routers/v1/steps.py +8 -6
- letta/server/rest_api/routers/v1/tools.py +134 -95
- letta/server/rest_api/utils.py +12 -1
- letta/server/server.py +272 -73
- letta/services/agent_manager.py +246 -313
- letta/services/block_manager.py +30 -9
- letta/services/context_window_calculator/__init__.py +0 -0
- letta/services/context_window_calculator/context_window_calculator.py +150 -0
- letta/services/context_window_calculator/token_counter.py +82 -0
- letta/services/file_processor/__init__.py +0 -0
- letta/services/file_processor/chunker/__init__.py +0 -0
- letta/services/file_processor/chunker/llama_index_chunker.py +29 -0
- letta/services/file_processor/embedder/__init__.py +0 -0
- letta/services/file_processor/embedder/openai_embedder.py +84 -0
- letta/services/file_processor/file_processor.py +123 -0
- letta/services/file_processor/parser/__init__.py +0 -0
- letta/services/file_processor/parser/base_parser.py +9 -0
- letta/services/file_processor/parser/mistral_parser.py +54 -0
- letta/services/file_processor/types.py +0 -0
- letta/services/files_agents_manager.py +184 -0
- letta/services/group_manager.py +118 -0
- letta/services/helpers/agent_manager_helper.py +76 -21
- letta/services/helpers/tool_execution_helper.py +3 -0
- letta/services/helpers/tool_parser_helper.py +100 -0
- letta/services/identity_manager.py +44 -42
- letta/services/job_manager.py +21 -10
- letta/services/mcp/base_client.py +5 -2
- letta/services/mcp/sse_client.py +3 -5
- letta/services/mcp/stdio_client.py +3 -5
- letta/services/mcp_manager.py +281 -0
- letta/services/message_manager.py +40 -26
- letta/services/organization_manager.py +55 -19
- letta/services/passage_manager.py +211 -13
- letta/services/provider_manager.py +48 -2
- letta/services/sandbox_config_manager.py +105 -0
- letta/services/source_manager.py +4 -5
- letta/services/step_manager.py +9 -6
- letta/services/summarizer/summarizer.py +50 -23
- letta/services/telemetry_manager.py +7 -0
- letta/services/tool_executor/tool_execution_manager.py +11 -52
- letta/services/tool_executor/tool_execution_sandbox.py +4 -34
- letta/services/tool_executor/tool_executor.py +107 -105
- letta/services/tool_manager.py +56 -17
- letta/services/tool_sandbox/base.py +39 -92
- letta/services/tool_sandbox/e2b_sandbox.py +16 -11
- letta/services/tool_sandbox/local_sandbox.py +51 -23
- letta/services/user_manager.py +36 -3
- letta/settings.py +10 -3
- letta/templates/__init__.py +0 -0
- letta/templates/sandbox_code_file.py.j2 +47 -0
- letta/templates/template_helper.py +16 -0
- letta/tracing.py +30 -1
- letta/types/__init__.py +7 -0
- letta/utils.py +25 -1
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/METADATA +7 -2
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/RECORD +136 -110
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/entry_points.txt +0 -0
@@ -53,13 +53,13 @@ class AnthropicClient(LLMClientBase):
|
|
53
53
|
|
54
54
|
@trace_method
|
55
55
|
async def request_async(self, request_data: dict, llm_config: LLMConfig) -> dict:
|
56
|
-
client = self.
|
56
|
+
client = await self._get_anthropic_client_async(llm_config, async_client=True)
|
57
57
|
response = await client.beta.messages.create(**request_data, betas=["tools-2024-04-04"])
|
58
58
|
return response.model_dump()
|
59
59
|
|
60
60
|
@trace_method
|
61
61
|
async def stream_async(self, request_data: dict, llm_config: LLMConfig) -> AsyncStream[BetaRawMessageStreamEvent]:
|
62
|
-
client = self.
|
62
|
+
client = await self._get_anthropic_client_async(llm_config, async_client=True)
|
63
63
|
request_data["stream"] = True
|
64
64
|
return await client.beta.messages.create(**request_data, betas=["tools-2024-04-04"])
|
65
65
|
|
@@ -99,7 +99,7 @@ class AnthropicClient(LLMClientBase):
|
|
99
99
|
for agent_id in agent_messages_mapping
|
100
100
|
}
|
101
101
|
|
102
|
-
client = self.
|
102
|
+
client = await self._get_anthropic_client_async(list(agent_llm_config_mapping.values())[0], async_client=True)
|
103
103
|
|
104
104
|
anthropic_requests = [
|
105
105
|
Request(custom_id=agent_id, params=MessageCreateParamsNonStreaming(**params)) for agent_id, params in requests.items()
|
@@ -134,6 +134,26 @@ class AnthropicClient(LLMClientBase):
|
|
134
134
|
else anthropic.Anthropic(max_retries=model_settings.anthropic_max_retries)
|
135
135
|
)
|
136
136
|
|
137
|
+
@trace_method
|
138
|
+
async def _get_anthropic_client_async(
|
139
|
+
self, llm_config: LLMConfig, async_client: bool = False
|
140
|
+
) -> Union[anthropic.AsyncAnthropic, anthropic.Anthropic]:
|
141
|
+
override_key = None
|
142
|
+
if llm_config.provider_category == ProviderCategory.byok:
|
143
|
+
override_key = await ProviderManager().get_override_key_async(llm_config.provider_name, actor=self.actor)
|
144
|
+
|
145
|
+
if async_client:
|
146
|
+
return (
|
147
|
+
anthropic.AsyncAnthropic(api_key=override_key, max_retries=model_settings.anthropic_max_retries)
|
148
|
+
if override_key
|
149
|
+
else anthropic.AsyncAnthropic(max_retries=model_settings.anthropic_max_retries)
|
150
|
+
)
|
151
|
+
return (
|
152
|
+
anthropic.Anthropic(api_key=override_key, max_retries=model_settings.anthropic_max_retries)
|
153
|
+
if override_key
|
154
|
+
else anthropic.Anthropic(max_retries=model_settings.anthropic_max_retries)
|
155
|
+
)
|
156
|
+
|
137
157
|
@trace_method
|
138
158
|
def build_request_data(
|
139
159
|
self,
|
@@ -268,6 +288,7 @@ class AnthropicClient(LLMClientBase):
|
|
268
288
|
token_count -= 8
|
269
289
|
return token_count
|
270
290
|
|
291
|
+
@trace_method
|
271
292
|
def handle_llm_error(self, e: Exception) -> Exception:
|
272
293
|
if isinstance(e, anthropic.APIConnectionError):
|
273
294
|
logger.warning(f"[Anthropic] API connection error: {e.__cause__}")
|
@@ -7,10 +7,7 @@ from letta.errors import ErrorCode, LLMAuthenticationError, LLMError
|
|
7
7
|
from letta.llm_api.google_constants import GOOGLE_MODEL_FOR_API_KEY_CHECK
|
8
8
|
from letta.llm_api.google_vertex_client import GoogleVertexClient
|
9
9
|
from letta.log import get_logger
|
10
|
-
from letta.schemas.llm_config import LLMConfig
|
11
|
-
from letta.schemas.message import Message as PydanticMessage
|
12
10
|
from letta.settings import model_settings
|
13
|
-
from letta.tracing import trace_method
|
14
11
|
|
15
12
|
logger = get_logger(__name__)
|
16
13
|
|
@@ -20,18 +17,6 @@ class GoogleAIClient(GoogleVertexClient):
|
|
20
17
|
def _get_client(self):
|
21
18
|
return genai.Client(api_key=model_settings.gemini_api_key)
|
22
19
|
|
23
|
-
@trace_method
|
24
|
-
def build_request_data(
|
25
|
-
self,
|
26
|
-
messages: List[PydanticMessage],
|
27
|
-
llm_config: LLMConfig,
|
28
|
-
tools: List[dict],
|
29
|
-
force_tool_call: Optional[str] = None,
|
30
|
-
) -> dict:
|
31
|
-
request = super().build_request_data(messages, llm_config, tools, force_tool_call)
|
32
|
-
del request["config"]["thinking_config"]
|
33
|
-
return request
|
34
|
-
|
35
20
|
|
36
21
|
def get_gemini_endpoint_and_headers(
|
37
22
|
base_url: str, model: Optional[str], api_key: str, key_in_header: bool = True, generate_content: bool = False
|
@@ -241,13 +241,14 @@ class GoogleVertexClient(LLMClientBase):
|
|
241
241
|
)
|
242
242
|
request_data["config"]["tool_config"] = tool_config.model_dump()
|
243
243
|
|
244
|
-
# Add thinking_config
|
244
|
+
# Add thinking_config for flash
|
245
245
|
# If enable_reasoner is False, set thinking_budget to 0
|
246
246
|
# Otherwise, use the value from max_reasoning_tokens
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
247
|
+
if "flash" in llm_config.model:
|
248
|
+
thinking_config = ThinkingConfig(
|
249
|
+
thinking_budget=llm_config.max_reasoning_tokens if llm_config.enable_reasoner else 0,
|
250
|
+
)
|
251
|
+
request_data["config"]["thinking_config"] = thinking_config.model_dump()
|
251
252
|
|
252
253
|
return request_data
|
253
254
|
|
letta/llm_api/llm_client_base.py
CHANGED
@@ -6,6 +6,7 @@ from openai import AsyncStream, Stream
|
|
6
6
|
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
|
7
7
|
|
8
8
|
from letta.errors import LLMError
|
9
|
+
from letta.schemas.embedding_config import EmbeddingConfig
|
9
10
|
from letta.schemas.llm_config import LLMConfig
|
10
11
|
from letta.schemas.message import Message
|
11
12
|
from letta.schemas.openai.chat_completion_response import ChatCompletionResponse
|
@@ -138,6 +139,20 @@ class LLMClientBase:
|
|
138
139
|
"""
|
139
140
|
raise NotImplementedError
|
140
141
|
|
142
|
+
@abstractmethod
|
143
|
+
async def request_embeddings(self, texts: List[str], embedding_config: EmbeddingConfig) -> List[List[float]]:
|
144
|
+
"""
|
145
|
+
Generate embeddings for a batch of texts.
|
146
|
+
|
147
|
+
Args:
|
148
|
+
texts (List[str]): List of texts to generate embeddings for.
|
149
|
+
embedding_config (EmbeddingConfig): Configuration for the embedding model.
|
150
|
+
|
151
|
+
Returns:
|
152
|
+
embeddings (List[List[float]]): List of embeddings for the input texts.
|
153
|
+
"""
|
154
|
+
raise NotImplementedError
|
155
|
+
|
141
156
|
@abstractmethod
|
142
157
|
def convert_response_to_chat_completion(
|
143
158
|
self,
|
letta/llm_api/openai.py
CHANGED
@@ -226,7 +226,7 @@ def build_openai_chat_completions_request(
|
|
226
226
|
tool_choice=tool_choice,
|
227
227
|
user=str(user_id),
|
228
228
|
max_completion_tokens=llm_config.max_tokens,
|
229
|
-
temperature=llm_config.temperature if supports_temperature_param(model) else
|
229
|
+
temperature=llm_config.temperature if supports_temperature_param(model) else 1.0,
|
230
230
|
reasoning_effort=llm_config.reasoning_effort,
|
231
231
|
)
|
232
232
|
else:
|
@@ -237,7 +237,7 @@ def build_openai_chat_completions_request(
|
|
237
237
|
function_call=function_call,
|
238
238
|
user=str(user_id),
|
239
239
|
max_completion_tokens=llm_config.max_tokens,
|
240
|
-
temperature=
|
240
|
+
temperature=llm_config.temperature if supports_temperature_param(model) else 1.0,
|
241
241
|
reasoning_effort=llm_config.reasoning_effort,
|
242
242
|
)
|
243
243
|
# https://platform.openai.com/docs/guides/text-generation/json-mode
|
letta/llm_api/openai_client.py
CHANGED
@@ -12,6 +12,7 @@ from letta.errors import (
|
|
12
12
|
LLMAuthenticationError,
|
13
13
|
LLMBadRequestError,
|
14
14
|
LLMConnectionError,
|
15
|
+
LLMContextWindowExceededError,
|
15
16
|
LLMNotFoundError,
|
16
17
|
LLMPermissionDeniedError,
|
17
18
|
LLMRateLimitError,
|
@@ -22,6 +23,7 @@ from letta.llm_api.helpers import add_inner_thoughts_to_functions, convert_to_st
|
|
22
23
|
from letta.llm_api.llm_client_base import LLMClientBase
|
23
24
|
from letta.local_llm.constants import INNER_THOUGHTS_KWARG, INNER_THOUGHTS_KWARG_DESCRIPTION, INNER_THOUGHTS_KWARG_DESCRIPTION_GO_FIRST
|
24
25
|
from letta.log import get_logger
|
26
|
+
from letta.schemas.embedding_config import EmbeddingConfig
|
25
27
|
from letta.schemas.enums import ProviderCategory, ProviderType
|
26
28
|
from letta.schemas.llm_config import LLMConfig
|
27
29
|
from letta.schemas.message import Message as PydanticMessage
|
@@ -125,6 +127,35 @@ class OpenAIClient(LLMClientBase):
|
|
125
127
|
|
126
128
|
return kwargs
|
127
129
|
|
130
|
+
def _prepare_client_kwargs_embedding(self, embedding_config: EmbeddingConfig) -> dict:
|
131
|
+
api_key = None
|
132
|
+
if embedding_config.embedding_endpoint_type == ProviderType.together:
|
133
|
+
api_key = model_settings.together_api_key or os.environ.get("TOGETHER_API_KEY")
|
134
|
+
|
135
|
+
if not api_key:
|
136
|
+
api_key = model_settings.openai_api_key or os.environ.get("OPENAI_API_KEY")
|
137
|
+
# supposedly the openai python client requires a dummy API key
|
138
|
+
api_key = api_key or "DUMMY_API_KEY"
|
139
|
+
kwargs = {"api_key": api_key, "base_url": embedding_config.embedding_endpoint}
|
140
|
+
return kwargs
|
141
|
+
|
142
|
+
async def _prepare_client_kwargs_async(self, llm_config: LLMConfig) -> dict:
|
143
|
+
api_key = None
|
144
|
+
if llm_config.provider_category == ProviderCategory.byok:
|
145
|
+
from letta.services.provider_manager import ProviderManager
|
146
|
+
|
147
|
+
api_key = await ProviderManager().get_override_key_async(llm_config.provider_name, actor=self.actor)
|
148
|
+
if llm_config.model_endpoint_type == ProviderType.together:
|
149
|
+
api_key = model_settings.together_api_key or os.environ.get("TOGETHER_API_KEY")
|
150
|
+
|
151
|
+
if not api_key:
|
152
|
+
api_key = model_settings.openai_api_key or os.environ.get("OPENAI_API_KEY")
|
153
|
+
# supposedly the openai python client requires a dummy API key
|
154
|
+
api_key = api_key or "DUMMY_API_KEY"
|
155
|
+
kwargs = {"api_key": api_key, "base_url": llm_config.model_endpoint}
|
156
|
+
|
157
|
+
return kwargs
|
158
|
+
|
128
159
|
@trace_method
|
129
160
|
def build_request_data(
|
130
161
|
self,
|
@@ -190,7 +221,6 @@ class OpenAIClient(LLMClientBase):
|
|
190
221
|
# NOTE: the reasoners that don't support temperature require 1.0, not None
|
191
222
|
temperature=llm_config.temperature if supports_temperature_param(model) else 1.0,
|
192
223
|
)
|
193
|
-
|
194
224
|
# always set user id for openai requests
|
195
225
|
if self.actor:
|
196
226
|
data.user = self.actor.id
|
@@ -231,7 +261,8 @@ class OpenAIClient(LLMClientBase):
|
|
231
261
|
"""
|
232
262
|
Performs underlying asynchronous request to OpenAI API and returns raw response dict.
|
233
263
|
"""
|
234
|
-
|
264
|
+
kwargs = await self._prepare_client_kwargs_async(llm_config)
|
265
|
+
client = AsyncOpenAI(**kwargs)
|
235
266
|
response: ChatCompletion = await client.chat.completions.create(**request_data)
|
236
267
|
return response.model_dump()
|
237
268
|
|
@@ -262,16 +293,29 @@ class OpenAIClient(LLMClientBase):
|
|
262
293
|
|
263
294
|
return chat_completion_response
|
264
295
|
|
296
|
+
@trace_method
|
265
297
|
async def stream_async(self, request_data: dict, llm_config: LLMConfig) -> AsyncStream[ChatCompletionChunk]:
|
266
298
|
"""
|
267
299
|
Performs underlying asynchronous streaming request to OpenAI and returns the async stream iterator.
|
268
300
|
"""
|
269
|
-
|
301
|
+
kwargs = await self._prepare_client_kwargs_async(llm_config)
|
302
|
+
client = AsyncOpenAI(**kwargs)
|
270
303
|
response_stream: AsyncStream[ChatCompletionChunk] = await client.chat.completions.create(
|
271
304
|
**request_data, stream=True, stream_options={"include_usage": True}
|
272
305
|
)
|
273
306
|
return response_stream
|
274
307
|
|
308
|
+
@trace_method
|
309
|
+
async def request_embeddings(self, inputs: List[str], embedding_config: EmbeddingConfig) -> List[dict]:
|
310
|
+
"""Request embeddings given texts and embedding config"""
|
311
|
+
kwargs = self._prepare_client_kwargs_embedding(embedding_config)
|
312
|
+
client = AsyncOpenAI(**kwargs)
|
313
|
+
response = await client.embeddings.create(model=embedding_config.embedding_model, input=inputs)
|
314
|
+
|
315
|
+
# TODO: add total usage
|
316
|
+
return [r.embedding for r in response.data]
|
317
|
+
|
318
|
+
@trace_method
|
275
319
|
def handle_llm_error(self, e: Exception) -> Exception:
|
276
320
|
"""
|
277
321
|
Maps OpenAI-specific errors to common LLMError types.
|
@@ -297,11 +341,19 @@ class OpenAIClient(LLMClientBase):
|
|
297
341
|
# BadRequestError can signify different issues (e.g., invalid args, context length)
|
298
342
|
# Check message content if finer-grained errors are needed
|
299
343
|
# Example: if "context_length_exceeded" in str(e): return LLMContextLengthExceededError(...)
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
344
|
+
# TODO: This is a super soft check. Not sure if we can do better, needs more investigation.
|
345
|
+
if "context" in str(e):
|
346
|
+
return LLMContextWindowExceededError(
|
347
|
+
message=f"Bad request to OpenAI (context length exceeded): {str(e)}",
|
348
|
+
code=ErrorCode.INVALID_ARGUMENT, # Or more specific if detectable
|
349
|
+
details=e.body,
|
350
|
+
)
|
351
|
+
else:
|
352
|
+
return LLMBadRequestError(
|
353
|
+
message=f"Bad request to OpenAI: {str(e)}",
|
354
|
+
code=ErrorCode.INVALID_ARGUMENT, # Or more specific if detectable
|
355
|
+
details=e.body,
|
356
|
+
)
|
305
357
|
|
306
358
|
if isinstance(e, openai.AuthenticationError):
|
307
359
|
logger.error(f"[OpenAI] Authentication error (401): {str(e)}") # More severe log level
|
letta/orm/__init__.py
CHANGED
@@ -5,6 +5,7 @@ from letta.orm.block import Block
|
|
5
5
|
from letta.orm.block_history import BlockHistory
|
6
6
|
from letta.orm.blocks_agents import BlocksAgents
|
7
7
|
from letta.orm.file import FileMetadata
|
8
|
+
from letta.orm.files_agents import FileAgent
|
8
9
|
from letta.orm.group import Group
|
9
10
|
from letta.orm.groups_agents import GroupsAgents
|
10
11
|
from letta.orm.groups_blocks import GroupsBlocks
|
@@ -15,6 +16,7 @@ from letta.orm.job import Job
|
|
15
16
|
from letta.orm.job_messages import JobMessage
|
16
17
|
from letta.orm.llm_batch_items import LLMBatchItem
|
17
18
|
from letta.orm.llm_batch_job import LLMBatchJob
|
19
|
+
from letta.orm.mcp_server import MCPServer
|
18
20
|
from letta.orm.message import Message
|
19
21
|
from letta.orm.organization import Organization
|
20
22
|
from letta.orm.passage import AgentPassage, BasePassage, SourcePassage
|
letta/orm/agent.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import asyncio
|
1
2
|
import uuid
|
2
3
|
from typing import TYPE_CHECKING, List, Optional, Set
|
3
4
|
|
@@ -21,6 +22,7 @@ from letta.schemas.tool_rule import ToolRule
|
|
21
22
|
|
22
23
|
if TYPE_CHECKING:
|
23
24
|
from letta.orm.agents_tags import AgentsTags
|
25
|
+
from letta.orm.files_agents import FileAgent
|
24
26
|
from letta.orm.identity import Identity
|
25
27
|
from letta.orm.organization import Organization
|
26
28
|
from letta.orm.source import Source
|
@@ -125,6 +127,12 @@ class Agent(SqlalchemyBase, OrganizationMixin, AsyncAttrs):
|
|
125
127
|
back_populates="manager_agent",
|
126
128
|
)
|
127
129
|
batch_items: Mapped[List["LLMBatchItem"]] = relationship("LLMBatchItem", back_populates="agent", lazy="selectin")
|
130
|
+
file_agents: Mapped[List["FileAgent"]] = relationship(
|
131
|
+
"FileAgent",
|
132
|
+
back_populates="agent",
|
133
|
+
cascade="all, delete-orphan",
|
134
|
+
lazy="selectin",
|
135
|
+
)
|
128
136
|
|
129
137
|
def to_pydantic(self, include_relationships: Optional[Set[str]] = None) -> PydanticAgentState:
|
130
138
|
"""
|
@@ -166,6 +174,8 @@ class Agent(SqlalchemyBase, OrganizationMixin, AsyncAttrs):
|
|
166
174
|
"last_updated_by_id": self.last_updated_by_id,
|
167
175
|
"created_at": self.created_at,
|
168
176
|
"updated_at": self.updated_at,
|
177
|
+
"enable_sleeptime": self.enable_sleeptime,
|
178
|
+
"response_format": self.response_format,
|
169
179
|
# optional field defaults
|
170
180
|
"tags": [],
|
171
181
|
"tools": [],
|
@@ -174,8 +184,6 @@ class Agent(SqlalchemyBase, OrganizationMixin, AsyncAttrs):
|
|
174
184
|
"identity_ids": [],
|
175
185
|
"multi_agent_group": None,
|
176
186
|
"tool_exec_environment_variables": [],
|
177
|
-
"enable_sleeptime": None,
|
178
|
-
"response_format": self.response_format,
|
179
187
|
}
|
180
188
|
|
181
189
|
# Optional fields: only included if requested
|
@@ -185,12 +193,12 @@ class Agent(SqlalchemyBase, OrganizationMixin, AsyncAttrs):
|
|
185
193
|
"sources": lambda: [s.to_pydantic() for s in self.sources],
|
186
194
|
"memory": lambda: Memory(
|
187
195
|
blocks=[b.to_pydantic() for b in self.core_memory],
|
196
|
+
file_blocks=[block for b in self.file_agents if (block := b.to_pydantic_block()) is not None],
|
188
197
|
prompt_template=get_prompt_template_for_agent_type(self.agent_type),
|
189
198
|
),
|
190
199
|
"identity_ids": lambda: [i.id for i in self.identities],
|
191
200
|
"multi_agent_group": lambda: self.multi_agent_group,
|
192
201
|
"tool_exec_environment_variables": lambda: self.tool_exec_environment_variables,
|
193
|
-
"enable_sleeptime": lambda: self.enable_sleeptime,
|
194
202
|
}
|
195
203
|
|
196
204
|
include_relationships = set(optional_fields.keys() if include_relationships is None else include_relationships)
|
@@ -242,15 +250,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, AsyncAttrs):
|
|
242
250
|
"last_updated_by_id": self.last_updated_by_id,
|
243
251
|
"created_at": self.created_at,
|
244
252
|
"updated_at": self.updated_at,
|
245
|
-
|
246
|
-
"tags": [],
|
247
|
-
"tools": [],
|
248
|
-
"sources": [],
|
249
|
-
"memory": Memory(blocks=[]),
|
250
|
-
"identity_ids": [],
|
251
|
-
"multi_agent_group": None,
|
252
|
-
"tool_exec_environment_variables": [],
|
253
|
-
"enable_sleeptime": None,
|
253
|
+
"enable_sleeptime": self.enable_sleeptime,
|
254
254
|
"response_format": self.response_format,
|
255
255
|
}
|
256
256
|
optional_fields = {
|
@@ -261,43 +261,45 @@ class Agent(SqlalchemyBase, OrganizationMixin, AsyncAttrs):
|
|
261
261
|
"identity_ids": [],
|
262
262
|
"multi_agent_group": None,
|
263
263
|
"tool_exec_environment_variables": [],
|
264
|
-
"enable_sleeptime": None,
|
265
|
-
"response_format": self.response_format,
|
266
264
|
}
|
267
265
|
|
268
266
|
# Initialize include_relationships to an empty set if it's None
|
269
267
|
include_relationships = set(optional_fields.keys() if include_relationships is None else include_relationships)
|
270
268
|
|
271
|
-
|
272
|
-
|
273
|
-
tags = await self.awaitable_attrs.tags
|
274
|
-
state["tags"] = [t.tag for t in tags]
|
275
|
-
|
276
|
-
if "tools" in include_relationships:
|
277
|
-
state["tools"] = await self.awaitable_attrs.tools
|
269
|
+
async def empty_list_async():
|
270
|
+
return []
|
278
271
|
|
279
|
-
|
280
|
-
|
281
|
-
state["sources"] = [s.to_pydantic() for s in sources]
|
272
|
+
async def none_async():
|
273
|
+
return None
|
282
274
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
275
|
+
# Only load requested relationships
|
276
|
+
tags = self.awaitable_attrs.tags if "tags" in include_relationships else empty_list_async()
|
277
|
+
tools = self.awaitable_attrs.tools if "tools" in include_relationships else empty_list_async()
|
278
|
+
sources = self.awaitable_attrs.sources if "sources" in include_relationships else empty_list_async()
|
279
|
+
memory = self.awaitable_attrs.core_memory if "memory" in include_relationships else empty_list_async()
|
280
|
+
identities = self.awaitable_attrs.identities if "identity_ids" in include_relationships else empty_list_async()
|
281
|
+
multi_agent_group = self.awaitable_attrs.multi_agent_group if "multi_agent_group" in include_relationships else none_async()
|
282
|
+
tool_exec_environment_variables = (
|
283
|
+
self.awaitable_attrs.tool_exec_environment_variables
|
284
|
+
if "tool_exec_environment_variables" in include_relationships
|
285
|
+
else empty_list_async()
|
286
|
+
)
|
287
|
+
file_agents = self.awaitable_attrs.file_agents if "memory" in include_relationships else empty_list_async()
|
288
|
+
|
289
|
+
(tags, tools, sources, memory, identities, multi_agent_group, tool_exec_environment_variables, file_agents) = await asyncio.gather(
|
290
|
+
tags, tools, sources, memory, identities, multi_agent_group, tool_exec_environment_variables, file_agents
|
291
|
+
)
|
292
|
+
|
293
|
+
state["tags"] = [t.tag for t in tags]
|
294
|
+
state["tools"] = [t.to_pydantic() for t in tools]
|
295
|
+
state["sources"] = [s.to_pydantic() for s in sources]
|
296
|
+
state["memory"] = Memory(
|
297
|
+
blocks=[m.to_pydantic() for m in memory],
|
298
|
+
file_blocks=[block for b in self.file_agents if (block := b.to_pydantic_block()) is not None],
|
299
|
+
prompt_template=get_prompt_template_for_agent_type(self.agent_type),
|
300
|
+
)
|
301
|
+
state["identity_ids"] = [i.id for i in identities]
|
302
|
+
state["multi_agent_group"] = multi_agent_group
|
303
|
+
state["tool_exec_environment_variables"] = tool_exec_environment_variables
|
302
304
|
|
303
305
|
return self.__pydantic_model__(**state)
|
letta/orm/base.py
CHANGED
@@ -69,8 +69,6 @@ class CommonSqlalchemyMetaMixins(Base):
|
|
69
69
|
"""returns the user id for the specified property"""
|
70
70
|
full_prop = f"_{prop}_by_id"
|
71
71
|
prop_value = getattr(self, full_prop, None)
|
72
|
-
if not prop_value:
|
73
|
-
return
|
74
72
|
return prop_value
|
75
73
|
|
76
74
|
def _user_id_setter(self, prop: str, value: str) -> None:
|
letta/orm/block.py
CHANGED
@@ -35,6 +35,7 @@ class Block(OrganizationMixin, SqlalchemyBase):
|
|
35
35
|
is_template: Mapped[bool] = mapped_column(
|
36
36
|
doc="whether the block is a template (e.g. saved human/persona options as baselines for other templates)", default=False
|
37
37
|
)
|
38
|
+
preserve_on_migration: Mapped[Optional[bool]] = mapped_column(doc="preserve the block on template migration", default=False)
|
38
39
|
value: Mapped[str] = mapped_column(doc="Text content of the block for the respective section of core memory.")
|
39
40
|
limit: Mapped[BigInteger] = mapped_column(Integer, default=CORE_MEMORY_BLOCK_CHAR_LIMIT, doc="Character limit of the block.")
|
40
41
|
metadata_: Mapped[Optional[dict]] = mapped_column(JSON, default={}, doc="arbitrary information related to the block.")
|
letta/orm/custom_columns.py
CHANGED
@@ -7,6 +7,7 @@ from letta.helpers.converters import (
|
|
7
7
|
deserialize_create_batch_response,
|
8
8
|
deserialize_embedding_config,
|
9
9
|
deserialize_llm_config,
|
10
|
+
deserialize_mcp_stdio_config,
|
10
11
|
deserialize_message_content,
|
11
12
|
deserialize_poll_batch_response,
|
12
13
|
deserialize_response_format,
|
@@ -19,6 +20,7 @@ from letta.helpers.converters import (
|
|
19
20
|
serialize_create_batch_response,
|
20
21
|
serialize_embedding_config,
|
21
22
|
serialize_llm_config,
|
23
|
+
serialize_mcp_stdio_config,
|
22
24
|
serialize_message_content,
|
23
25
|
serialize_poll_batch_response,
|
24
26
|
serialize_response_format,
|
@@ -183,3 +185,14 @@ class ResponseFormatColumn(TypeDecorator):
|
|
183
185
|
|
184
186
|
def process_result_value(self, value, dialect):
|
185
187
|
return deserialize_response_format(value)
|
188
|
+
|
189
|
+
|
190
|
+
class MCPStdioServerConfigColumn(TypeDecorator):
|
191
|
+
impl = JSON
|
192
|
+
cache_ok = True
|
193
|
+
|
194
|
+
def process_bind_param(self, value, dialect):
|
195
|
+
return serialize_mcp_stdio_config(value)
|
196
|
+
|
197
|
+
def process_result_value(self, value, dialect):
|
198
|
+
return deserialize_mcp_stdio_config(value)
|
letta/orm/enums.py
CHANGED
letta/orm/file.py
CHANGED
@@ -8,13 +8,14 @@ from letta.orm.sqlalchemy_base import SqlalchemyBase
|
|
8
8
|
from letta.schemas.file import FileMetadata as PydanticFileMetadata
|
9
9
|
|
10
10
|
if TYPE_CHECKING:
|
11
|
+
from letta.orm.files_agents import FileAgent
|
11
12
|
from letta.orm.organization import Organization
|
12
13
|
from letta.orm.passage import SourcePassage
|
13
14
|
from letta.orm.source import Source
|
14
15
|
|
15
16
|
|
16
17
|
class FileMetadata(SqlalchemyBase, OrganizationMixin, SourceMixin):
|
17
|
-
"""Represents
|
18
|
+
"""Represents an uploaded file."""
|
18
19
|
|
19
20
|
__tablename__ = "files"
|
20
21
|
__pydantic_model__ = PydanticFileMetadata
|
@@ -32,3 +33,4 @@ class FileMetadata(SqlalchemyBase, OrganizationMixin, SourceMixin):
|
|
32
33
|
source_passages: Mapped[List["SourcePassage"]] = relationship(
|
33
34
|
"SourcePassage", back_populates="file", lazy="selectin", cascade="all, delete-orphan"
|
34
35
|
)
|
36
|
+
file_agents: Mapped[List["FileAgent"]] = relationship("FileAgent", back_populates="file", lazy="selectin")
|
@@ -0,0 +1,68 @@
|
|
1
|
+
import uuid
|
2
|
+
from datetime import datetime
|
3
|
+
from typing import TYPE_CHECKING, Optional
|
4
|
+
|
5
|
+
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, String, Text, UniqueConstraint, func
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
7
|
+
|
8
|
+
from letta.orm.mixins import OrganizationMixin
|
9
|
+
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
10
|
+
from letta.schemas.block import Block as PydanticBlock
|
11
|
+
from letta.schemas.file import FileAgent as PydanticFileAgent
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from letta.orm.file import FileMetadata
|
15
|
+
|
16
|
+
|
17
|
+
class FileAgent(SqlalchemyBase, OrganizationMixin):
|
18
|
+
"""
|
19
|
+
Join table between File and Agent.
|
20
|
+
|
21
|
+
Tracks whether a file is currently “open” for the agent and
|
22
|
+
the specific excerpt (grepped section) the agent is looking at.
|
23
|
+
"""
|
24
|
+
|
25
|
+
__tablename__ = "files_agents"
|
26
|
+
__table_args__ = (
|
27
|
+
Index("ix_files_agents_file_id_agent_id", "file_id", "agent_id"),
|
28
|
+
UniqueConstraint("file_id", "agent_id", name="uq_files_agents_file_agent"),
|
29
|
+
)
|
30
|
+
__pydantic_model__ = PydanticFileAgent
|
31
|
+
|
32
|
+
# TODO: We want to migrate all the ORM models to do this, so we will need to move this to the SqlalchemyBase
|
33
|
+
# TODO: Some still rely on the Pydantic object to do this
|
34
|
+
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"file_agent-{uuid.uuid4()}")
|
35
|
+
file_id: Mapped[str] = mapped_column(String, ForeignKey("files.id", ondelete="CASCADE"), primary_key=True, doc="ID of the file.")
|
36
|
+
agent_id: Mapped[str] = mapped_column(String, ForeignKey("agents.id", ondelete="CASCADE"), primary_key=True, doc="ID of the agent.")
|
37
|
+
|
38
|
+
is_open: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, doc="True if the agent currently has the file open.")
|
39
|
+
visible_content: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Portion of the file the agent is focused on.")
|
40
|
+
last_accessed_at: Mapped[datetime] = mapped_column(
|
41
|
+
DateTime(timezone=True),
|
42
|
+
server_default=func.now(),
|
43
|
+
onupdate=func.now(),
|
44
|
+
nullable=False,
|
45
|
+
doc="UTC timestamp when this agent last accessed the file.",
|
46
|
+
)
|
47
|
+
|
48
|
+
# relationships
|
49
|
+
agent: Mapped["Agent"] = relationship(
|
50
|
+
"Agent",
|
51
|
+
back_populates="file_agents",
|
52
|
+
lazy="selectin",
|
53
|
+
)
|
54
|
+
file: Mapped["FileMetadata"] = relationship(
|
55
|
+
"FileMetadata",
|
56
|
+
foreign_keys=[file_id],
|
57
|
+
lazy="selectin",
|
58
|
+
)
|
59
|
+
|
60
|
+
# TODO: This is temporary as we figure out if we want FileBlock as a first class citizen
|
61
|
+
def to_pydantic_block(self) -> PydanticBlock:
|
62
|
+
visible_content = self.visible_content if self.visible_content and self.is_open else ""
|
63
|
+
return PydanticBlock(
|
64
|
+
organization_id=self.organization_id,
|
65
|
+
value=visible_content,
|
66
|
+
label=self.file.file_name,
|
67
|
+
read_only=True,
|
68
|
+
)
|
letta/orm/mcp_server.py
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
from typing import TYPE_CHECKING, Optional
|
2
|
+
|
3
|
+
from sqlalchemy import JSON, String, UniqueConstraint
|
4
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
5
|
+
|
6
|
+
from letta.functions.mcp_client.types import StdioServerConfig
|
7
|
+
from letta.orm.custom_columns import MCPStdioServerConfigColumn
|
8
|
+
|
9
|
+
# TODO everything in functions should live in this model
|
10
|
+
from letta.orm.enums import MCPServerType
|
11
|
+
from letta.orm.mixins import OrganizationMixin
|
12
|
+
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
13
|
+
from letta.schemas.mcp import MCPServer
|
14
|
+
|
15
|
+
if TYPE_CHECKING:
|
16
|
+
pass
|
17
|
+
|
18
|
+
|
19
|
+
class MCPServer(SqlalchemyBase, OrganizationMixin):
|
20
|
+
"""Represents a registered MCP server"""
|
21
|
+
|
22
|
+
__tablename__ = "mcp_server"
|
23
|
+
__pydantic_model__ = MCPServer
|
24
|
+
|
25
|
+
# Add unique constraint on (name, _organization_id)
|
26
|
+
# An organization should not have multiple tools with the same name
|
27
|
+
__table_args__ = (UniqueConstraint("server_name", "organization_id", name="uix_name_organization_mcp_server"),)
|
28
|
+
|
29
|
+
server_name: Mapped[str] = mapped_column(doc="The display name of the MCP server")
|
30
|
+
server_type: Mapped[MCPServerType] = mapped_column(
|
31
|
+
String, default=MCPServerType.SSE, doc="The type of the MCP server. Only SSE is supported for remote servers."
|
32
|
+
)
|
33
|
+
|
34
|
+
# sse server
|
35
|
+
server_url: Mapped[Optional[str]] = mapped_column(
|
36
|
+
String, nullable=True, doc="The URL of the server (MCP SSE client will connect to this URL)"
|
37
|
+
)
|
38
|
+
|
39
|
+
# stdio server
|
40
|
+
stdio_config: Mapped[Optional[StdioServerConfig]] = mapped_column(
|
41
|
+
MCPStdioServerConfigColumn, nullable=True, doc="The configuration for the stdio server"
|
42
|
+
)
|
43
|
+
|
44
|
+
metadata_: Mapped[Optional[dict]] = mapped_column(
|
45
|
+
JSON, default=lambda: {}, doc="A dictionary of additional metadata for the MCP server."
|
46
|
+
)
|
47
|
+
# relationships
|
48
|
+
# organization: Mapped["Organization"] = relationship("Organization", back_populates="mcp_server", lazy="selectin")
|
letta/orm/message.py
CHANGED
@@ -22,6 +22,7 @@ class Message(SqlalchemyBase, OrganizationMixin, AgentMixin):
|
|
22
22
|
Index("ix_messages_agent_created_at", "agent_id", "created_at"),
|
23
23
|
Index("ix_messages_created_at", "created_at", "id"),
|
24
24
|
Index("ix_messages_agent_sequence", "agent_id", "sequence_id"),
|
25
|
+
Index("ix_messages_org_agent", "organization_id", "agent_id"),
|
25
26
|
)
|
26
27
|
__pydantic_model__ = PydanticMessage
|
27
28
|
|