letta-nightly 0.8.17.dev20250722104501__py3-none-any.whl → 0.9.0.dev20250724081419__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 (96) hide show
  1. letta/__init__.py +5 -3
  2. letta/agent.py +3 -2
  3. letta/agents/base_agent.py +4 -1
  4. letta/agents/voice_agent.py +1 -0
  5. letta/constants.py +4 -2
  6. letta/functions/schema_generator.py +2 -1
  7. letta/groups/dynamic_multi_agent.py +1 -0
  8. letta/helpers/converters.py +13 -5
  9. letta/helpers/json_helpers.py +6 -1
  10. letta/llm_api/anthropic.py +2 -2
  11. letta/llm_api/aws_bedrock.py +24 -94
  12. letta/llm_api/deepseek.py +1 -1
  13. letta/llm_api/google_ai_client.py +0 -38
  14. letta/llm_api/google_constants.py +6 -3
  15. letta/llm_api/helpers.py +1 -1
  16. letta/llm_api/llm_api_tools.py +4 -7
  17. letta/llm_api/mistral.py +12 -37
  18. letta/llm_api/openai.py +17 -17
  19. letta/llm_api/sample_response_jsons/aws_bedrock.json +38 -0
  20. letta/llm_api/sample_response_jsons/lmstudio_embedding_list.json +15 -0
  21. letta/llm_api/sample_response_jsons/lmstudio_model_list.json +15 -0
  22. letta/local_llm/constants.py +2 -23
  23. letta/local_llm/json_parser.py +11 -1
  24. letta/local_llm/llm_chat_completion_wrappers/airoboros.py +9 -9
  25. letta/local_llm/llm_chat_completion_wrappers/chatml.py +7 -8
  26. letta/local_llm/llm_chat_completion_wrappers/configurable_wrapper.py +6 -6
  27. letta/local_llm/llm_chat_completion_wrappers/dolphin.py +3 -3
  28. letta/local_llm/llm_chat_completion_wrappers/simple_summary_wrapper.py +1 -1
  29. letta/local_llm/ollama/api.py +2 -2
  30. letta/orm/__init__.py +1 -0
  31. letta/orm/agent.py +33 -2
  32. letta/orm/files_agents.py +13 -10
  33. letta/orm/mixins.py +8 -0
  34. letta/orm/prompt.py +13 -0
  35. letta/orm/sqlite_functions.py +61 -17
  36. letta/otel/db_pool_monitoring.py +13 -12
  37. letta/schemas/agent.py +69 -4
  38. letta/schemas/agent_file.py +2 -0
  39. letta/schemas/block.py +11 -0
  40. letta/schemas/embedding_config.py +15 -3
  41. letta/schemas/enums.py +2 -0
  42. letta/schemas/file.py +1 -1
  43. letta/schemas/folder.py +74 -0
  44. letta/schemas/memory.py +12 -6
  45. letta/schemas/prompt.py +9 -0
  46. letta/schemas/providers/__init__.py +47 -0
  47. letta/schemas/providers/anthropic.py +78 -0
  48. letta/schemas/providers/azure.py +80 -0
  49. letta/schemas/providers/base.py +201 -0
  50. letta/schemas/providers/bedrock.py +78 -0
  51. letta/schemas/providers/cerebras.py +79 -0
  52. letta/schemas/providers/cohere.py +18 -0
  53. letta/schemas/providers/deepseek.py +63 -0
  54. letta/schemas/providers/google_gemini.py +102 -0
  55. letta/schemas/providers/google_vertex.py +54 -0
  56. letta/schemas/providers/groq.py +35 -0
  57. letta/schemas/providers/letta.py +39 -0
  58. letta/schemas/providers/lmstudio.py +97 -0
  59. letta/schemas/providers/mistral.py +41 -0
  60. letta/schemas/providers/ollama.py +151 -0
  61. letta/schemas/providers/openai.py +241 -0
  62. letta/schemas/providers/together.py +85 -0
  63. letta/schemas/providers/vllm.py +57 -0
  64. letta/schemas/providers/xai.py +66 -0
  65. letta/server/db.py +0 -5
  66. letta/server/rest_api/app.py +4 -3
  67. letta/server/rest_api/routers/v1/__init__.py +2 -0
  68. letta/server/rest_api/routers/v1/agents.py +152 -4
  69. letta/server/rest_api/routers/v1/folders.py +490 -0
  70. letta/server/rest_api/routers/v1/providers.py +2 -2
  71. letta/server/rest_api/routers/v1/sources.py +21 -26
  72. letta/server/rest_api/routers/v1/tools.py +90 -15
  73. letta/server/server.py +50 -95
  74. letta/services/agent_manager.py +420 -81
  75. letta/services/agent_serialization_manager.py +707 -0
  76. letta/services/block_manager.py +132 -11
  77. letta/services/file_manager.py +104 -29
  78. letta/services/file_processor/embedder/pinecone_embedder.py +8 -2
  79. letta/services/file_processor/file_processor.py +75 -24
  80. letta/services/file_processor/parser/markitdown_parser.py +95 -0
  81. letta/services/files_agents_manager.py +57 -17
  82. letta/services/group_manager.py +7 -0
  83. letta/services/helpers/agent_manager_helper.py +25 -15
  84. letta/services/provider_manager.py +2 -2
  85. letta/services/source_manager.py +35 -16
  86. letta/services/tool_executor/files_tool_executor.py +12 -5
  87. letta/services/tool_manager.py +12 -0
  88. letta/services/tool_sandbox/e2b_sandbox.py +52 -48
  89. letta/settings.py +9 -6
  90. letta/streaming_utils.py +2 -1
  91. letta/utils.py +34 -1
  92. {letta_nightly-0.8.17.dev20250722104501.dist-info → letta_nightly-0.9.0.dev20250724081419.dist-info}/METADATA +9 -8
  93. {letta_nightly-0.8.17.dev20250722104501.dist-info → letta_nightly-0.9.0.dev20250724081419.dist-info}/RECORD +96 -68
  94. {letta_nightly-0.8.17.dev20250722104501.dist-info → letta_nightly-0.9.0.dev20250724081419.dist-info}/LICENSE +0 -0
  95. {letta_nightly-0.8.17.dev20250722104501.dist-info → letta_nightly-0.9.0.dev20250724081419.dist-info}/WHEEL +0 -0
  96. {letta_nightly-0.8.17.dev20250722104501.dist-info → letta_nightly-0.9.0.dev20250724081419.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,3 @@
1
- import base64
2
1
  import sqlite3
3
2
  from typing import Optional, Union
4
3
 
@@ -7,11 +6,15 @@ from sqlalchemy import event
7
6
  from sqlalchemy.engine import Engine
8
7
 
9
8
  from letta.constants import MAX_EMBEDDING_DIM
9
+ from letta.settings import DatabaseChoice, settings
10
+
11
+ if settings.database_engine == DatabaseChoice.SQLITE:
12
+ import sqlite_vec
10
13
 
11
14
 
12
15
  def adapt_array(arr):
13
16
  """
14
- Converts numpy array to binary for SQLite storage
17
+ Converts numpy array to binary for SQLite storage using sqlite-vec
15
18
  """
16
19
  if arr is None:
17
20
  return None
@@ -21,15 +24,14 @@ def adapt_array(arr):
21
24
  elif not isinstance(arr, np.ndarray):
22
25
  raise ValueError(f"Unsupported type: {type(arr)}")
23
26
 
24
- # Convert to bytes and then base64 encode
25
- bytes_data = arr.tobytes()
26
- base64_data = base64.b64encode(bytes_data)
27
- return sqlite3.Binary(base64_data)
27
+ # Ensure float32 for compatibility
28
+ arr = arr.astype(np.float32)
29
+ return sqlite_vec.serialize_float32(arr.tolist())
28
30
 
29
31
 
30
32
  def convert_array(text):
31
33
  """
32
- Converts binary back to numpy array
34
+ Converts binary back to numpy array using sqlite-vec format
33
35
  """
34
36
  if text is None:
35
37
  return None
@@ -41,13 +43,11 @@ def convert_array(text):
41
43
  # Handle both bytes and sqlite3.Binary
42
44
  binary_data = bytes(text) if isinstance(text, sqlite3.Binary) else text
43
45
 
44
- try:
45
- # First decode base64
46
- decoded_data = base64.b64decode(binary_data)
47
- # Then convert to numpy array
48
- return np.frombuffer(decoded_data, dtype=np.float32)
49
- except Exception:
50
- return None
46
+ # Use sqlite-vec native format
47
+ if len(binary_data) % 4 == 0: # Must be divisible by 4 for float32
48
+ return np.frombuffer(binary_data, dtype=np.float32)
49
+ else:
50
+ raise ValueError(f"Invalid sqlite-vec binary data length: {len(binary_data)}")
51
51
 
52
52
 
53
53
  def verify_embedding_dimension(embedding: np.ndarray, expected_dim: int = MAX_EMBEDDING_DIM) -> bool:
@@ -131,11 +131,55 @@ def cosine_distance(embedding1, embedding2, expected_dim=MAX_EMBEDDING_DIM):
131
131
  return distance
132
132
 
133
133
 
134
+ # Note: sqlite-vec provides native SQL functions for vector operations
135
+ # We don't need custom Python distance functions since sqlite-vec handles this at the SQL level
136
+
137
+
134
138
  @event.listens_for(Engine, "connect")
135
139
  def register_functions(dbapi_connection, connection_record):
136
- """Register SQLite functions"""
137
- if isinstance(dbapi_connection, sqlite3.Connection):
138
- dbapi_connection.create_function("cosine_distance", 2, cosine_distance)
140
+ """Register SQLite functions and enable sqlite-vec extension"""
141
+ # Check for both sync SQLite connections and async aiosqlite connections
142
+ is_sqlite_connection = isinstance(dbapi_connection, sqlite3.Connection)
143
+ is_aiosqlite_connection = hasattr(dbapi_connection, "_connection") and str(type(dbapi_connection)).find("aiosqlite") != -1
144
+
145
+ if is_sqlite_connection or is_aiosqlite_connection:
146
+ # Get the actual SQLite connection for async connections
147
+ actual_connection = dbapi_connection._connection if is_aiosqlite_connection else dbapi_connection
148
+
149
+ # Enable sqlite-vec extension
150
+ try:
151
+ if is_aiosqlite_connection:
152
+ # For aiosqlite connections, we cannot use async operations in sync event handlers
153
+ # 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
+ else:
156
+ # 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)")
161
+ except Exception as e:
162
+ raise RuntimeError(f"Failed to load sqlite-vec extension: {e}")
163
+
164
+ # Register custom cosine_distance function for backward compatibility
165
+ try:
166
+ if is_aiosqlite_connection:
167
+ # Try to register function on the actual connection, even though it might be async
168
+ # This may require the function to be registered per-connection
169
+ print("Attempting function registration for aiosqlite connection")
170
+ # For async connections, we need to register the function differently
171
+ # We'll use the sync-style registration on the underlying connection
172
+ raw_conn = getattr(actual_connection, "_connection", actual_connection)
173
+ if hasattr(raw_conn, "create_function"):
174
+ raw_conn.create_function("cosine_distance", 2, cosine_distance)
175
+ print("Successfully registered cosine_distance for aiosqlite")
176
+ else:
177
+ dbapi_connection.create_function("cosine_distance", 2, cosine_distance)
178
+ print("Successfully registered cosine_distance for sync connection")
179
+ except Exception as e:
180
+ raise RuntimeError(f"Failed to register cosine_distance function: {e}")
181
+ else:
182
+ print(f"Warning: Not a SQLite connection, but instead {type(dbapi_connection)}: skipping function registration")
139
183
 
140
184
 
141
185
  # Register adapters and converters for numpy arrays
@@ -89,18 +89,18 @@ class DatabasePoolMonitor:
89
89
  try:
90
90
  from letta.otel.metric_registry import MetricRegistry
91
91
 
92
- # Record current pool statistics
93
- pool_stats = self._get_pool_stats(pool)
94
92
  attrs = {
95
93
  "engine_name": engine_name,
96
94
  **get_ctx_attributes(),
97
95
  }
98
-
99
- MetricRegistry().db_pool_connections_checked_out_gauge.set(pool_stats["checked_out"], attributes=attrs)
100
- MetricRegistry().db_pool_connections_available_gauge.set(pool_stats["available"], attributes=attrs)
101
- MetricRegistry().db_pool_connections_total_gauge.set(pool_stats["total"], attributes=attrs)
102
- if pool_stats["overflow"] is not None:
103
- MetricRegistry().db_pool_connections_overflow_gauge.set(pool_stats["overflow"], attributes=attrs)
96
+ # Record current pool statistics
97
+ if isinstance(pool, QueuePool):
98
+ pool_stats = self._get_pool_stats(pool)
99
+ MetricRegistry().db_pool_connections_checked_out_gauge.set(pool_stats["checked_out"], attributes=attrs)
100
+ MetricRegistry().db_pool_connections_available_gauge.set(pool_stats["available"], attributes=attrs)
101
+ MetricRegistry().db_pool_connections_total_gauge.set(pool_stats["total"], attributes=attrs)
102
+ if pool_stats["overflow"] is not None:
103
+ MetricRegistry().db_pool_connections_overflow_gauge.set(pool_stats["overflow"], attributes=attrs)
104
104
 
105
105
  # Record checkout event
106
106
  attrs["event"] = "checkout"
@@ -137,15 +137,16 @@ class DatabasePoolMonitor:
137
137
  try:
138
138
  from letta.otel.metric_registry import MetricRegistry
139
139
 
140
- # Record current pool statistics after checkin
141
- pool_stats = self._get_pool_stats(pool)
142
140
  attrs = {
143
141
  "engine_name": engine_name,
144
142
  **get_ctx_attributes(),
145
143
  }
146
144
 
147
- MetricRegistry().db_pool_connections_checked_out_gauge.set(pool_stats["checked_out"], attributes=attrs)
148
- MetricRegistry().db_pool_connections_available_gauge.set(pool_stats["available"], attributes=attrs)
145
+ # Record current pool statistics after checkin
146
+ if isinstance(pool, QueuePool):
147
+ pool_stats = self._get_pool_stats(pool)
148
+ MetricRegistry().db_pool_connections_checked_out_gauge.set(pool_stats["checked_out"], attributes=attrs)
149
+ MetricRegistry().db_pool_connections_available_gauge.set(pool_stats["available"], attributes=attrs)
149
150
 
150
151
  # Record checkin event
151
152
  attrs["event"] = "checkin"
letta/schemas/agent.py CHANGED
@@ -19,7 +19,7 @@ from letta.schemas.response_format import ResponseFormatUnion
19
19
  from letta.schemas.source import Source
20
20
  from letta.schemas.tool import Tool
21
21
  from letta.schemas.tool_rule import ToolRule
22
- from letta.utils import create_random_username
22
+ from letta.utils import calculate_file_defaults_based_on_context_window, create_random_username
23
23
 
24
24
 
25
25
  class AgentType(str, Enum):
@@ -112,6 +112,16 @@ class AgentState(OrmMetadataBase, validate_assignment=True):
112
112
  # timezone
113
113
  timezone: Optional[str] = Field(None, description="The timezone of the agent (IANA format).")
114
114
 
115
+ # file related controls
116
+ max_files_open: Optional[int] = Field(
117
+ None,
118
+ description="Maximum number of files that can be open at once for this agent. Setting this too high may exceed the context window, which will break the agent.",
119
+ )
120
+ per_file_view_window_char_limit: Optional[int] = Field(
121
+ None,
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
+ )
124
+
115
125
  def get_agent_env_vars_as_dict(self) -> Dict[str, str]:
116
126
  # Get environment variables for this agent specifically
117
127
  per_agent_env_vars = {}
@@ -119,6 +129,27 @@ class AgentState(OrmMetadataBase, validate_assignment=True):
119
129
  per_agent_env_vars[agent_env_var_obj.key] = agent_env_var_obj.value
120
130
  return per_agent_env_vars
121
131
 
132
+ @model_validator(mode="after")
133
+ def set_file_defaults_based_on_context_window(self) -> "AgentState":
134
+ """Set reasonable defaults for file-related fields based on the model's context window size."""
135
+ # Only set defaults if not explicitly provided
136
+ if self.max_files_open is not None and self.per_file_view_window_char_limit is not None:
137
+ return self
138
+
139
+ # Get context window size from llm_config
140
+ context_window = self.llm_config.context_window if self.llm_config and self.llm_config.context_window else None
141
+
142
+ # Calculate defaults using the helper function
143
+ default_max_files, default_char_limit = calculate_file_defaults_based_on_context_window(context_window)
144
+
145
+ # Apply defaults only if not set
146
+ if self.max_files_open is None:
147
+ self.max_files_open = default_max_files
148
+ if self.per_file_view_window_char_limit is None:
149
+ self.per_file_view_window_char_limit = default_char_limit
150
+
151
+ return self
152
+
122
153
 
123
154
  class CreateAgent(BaseModel, validate_assignment=True): #
124
155
  # all optional as server can generate defaults
@@ -197,6 +228,14 @@ class CreateAgent(BaseModel, validate_assignment=True): #
197
228
  enable_sleeptime: Optional[bool] = Field(None, description="If set to True, memory management will move to a background agent thread.")
198
229
  response_format: Optional[ResponseFormatUnion] = Field(None, description="The response format for the agent.")
199
230
  timezone: Optional[str] = Field(None, description="The timezone of the agent (IANA format).")
231
+ max_files_open: Optional[int] = Field(
232
+ None,
233
+ description="Maximum number of files that can be open at once for this agent. Setting this too high may exceed the context window, which will break the agent.",
234
+ )
235
+ per_file_view_window_char_limit: Optional[int] = Field(
236
+ None,
237
+ 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
+ )
200
239
 
201
240
  @field_validator("name")
202
241
  @classmethod
@@ -291,6 +330,14 @@ class UpdateAgent(BaseModel):
291
330
  last_run_completion: Optional[datetime] = Field(None, description="The timestamp when the agent last completed a run.")
292
331
  last_run_duration_ms: Optional[int] = Field(None, description="The duration in milliseconds of the agent's last run.")
293
332
  timezone: Optional[str] = Field(None, description="The timezone of the agent (IANA format).")
333
+ max_files_open: Optional[int] = Field(
334
+ None,
335
+ description="Maximum number of files that can be open at once for this agent. Setting this too high may exceed the context window, which will break the agent.",
336
+ )
337
+ per_file_view_window_char_limit: Optional[int] = Field(
338
+ None,
339
+ 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
+ )
294
341
 
295
342
  class Config:
296
343
  extra = "ignore" # Ignores extra fields
@@ -313,6 +360,12 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
313
360
  return (
314
361
  "{% if sources %}"
315
362
  "<directories>\n"
363
+ "{% if max_files_open %}"
364
+ "<file_limits>\n"
365
+ "- current_files_open={{ file_blocks|selectattr('value')|list|length }}\n"
366
+ "- max_files_open={{ max_files_open }}\n"
367
+ "</file_limits>\n"
368
+ "{% endif %}"
316
369
  "{% for source in sources %}"
317
370
  f'<directory name="{{{{ source.name }}}}">\n'
318
371
  "{% if source.description %}"
@@ -323,7 +376,7 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
323
376
  "{% endif %}"
324
377
  "{% if file_blocks %}"
325
378
  "{% for block in file_blocks %}"
326
- "{% if block.metadata and block.metadata.get('source_id') == source.id %}"
379
+ "{% if block.source_id and block.source_id == source.id %}"
327
380
  f"<file status=\"{{{{ '{FileStatus.open.value}' if block.value else '{FileStatus.closed.value}' }}}}\">\n"
328
381
  "<{{ block.label }}>\n"
329
382
  "<description>\n"
@@ -380,6 +433,12 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
380
433
  "{% endif %}"
381
434
  "\n\n{% if sources %}"
382
435
  "<directories>\n"
436
+ "{% if max_files_open %}"
437
+ "<file_limits>\n"
438
+ "- current_files_open={{ file_blocks|selectattr('value')|list|length }}\n"
439
+ "- max_files_open={{ max_files_open }}\n"
440
+ "</file_limits>\n"
441
+ "{% endif %}"
383
442
  "{% for source in sources %}"
384
443
  f'<directory name="{{{{ source.name }}}}">\n'
385
444
  "{% if source.description %}"
@@ -390,7 +449,7 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
390
449
  "{% endif %}"
391
450
  "{% if file_blocks %}"
392
451
  "{% for block in file_blocks %}"
393
- "{% if block.metadata and block.metadata.get('source_id') == source.id %}"
452
+ "{% if block.source_id and block.source_id == source.id %}"
394
453
  f"<file status=\"{{{{ '{FileStatus.open.value}' if block.value else '{FileStatus.closed.value}' }}}}\" name=\"{{{{ block.label }}}}\">\n"
395
454
  "{% if block.description %}"
396
455
  "<description>\n"
@@ -446,6 +505,12 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
446
505
  "{% endif %}"
447
506
  "\n\n{% if sources %}"
448
507
  "<directories>\n"
508
+ "{% if max_files_open %}"
509
+ "<file_limits>\n"
510
+ "- current_files_open={{ file_blocks|selectattr('value')|list|length }}\n"
511
+ "- max_files_open={{ max_files_open }}\n"
512
+ "</file_limits>\n"
513
+ "{% endif %}"
449
514
  "{% for source in sources %}"
450
515
  f'<directory name="{{{{ source.name }}}}">\n'
451
516
  "{% if source.description %}"
@@ -456,7 +521,7 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
456
521
  "{% endif %}"
457
522
  "{% if file_blocks %}"
458
523
  "{% for block in file_blocks %}"
459
- "{% if block.metadata and block.metadata.get('source_id') == source.id %}"
524
+ "{% if block.source_id and block.source_id == source.id %}"
460
525
  f"<file status=\"{{{{ '{FileStatus.open.value}' if block.value else '{FileStatus.closed.value}' }}}}\" name=\"{{{{ block.label }}}}\">\n"
461
526
  "{% if block.description %}"
462
527
  "<description>\n"
@@ -145,6 +145,8 @@ class AgentSchema(CreateAgent):
145
145
  enable_sleeptime=False, # TODO: Need to figure out how to patch this
146
146
  response_format=agent_state.response_format,
147
147
  timezone=agent_state.timezone or "UTC",
148
+ max_files_open=agent_state.max_files_open,
149
+ per_file_view_window_char_limit=agent_state.per_file_view_window_char_limit,
148
150
  )
149
151
 
150
152
  messages = await message_manager.list_messages_for_agent_async(
letta/schemas/block.py CHANGED
@@ -1,3 +1,4 @@
1
+ from datetime import datetime
1
2
  from typing import Optional
2
3
 
3
4
  from pydantic import Field, model_validator
@@ -79,6 +80,16 @@ class Block(BaseBlock):
79
80
  last_updated_by_id: Optional[str] = Field(None, description="The id of the user that last updated this Block.")
80
81
 
81
82
 
83
+ class FileBlock(Block):
84
+ file_id: str = Field(..., description="Unique identifier of the file.")
85
+ source_id: str = Field(..., description="Unique identifier of the source.")
86
+ is_open: bool = Field(..., description="True if the agent currently has the file open.")
87
+ last_accessed_at: Optional[datetime] = Field(
88
+ default_factory=datetime.utcnow,
89
+ description="UTC timestamp of the agent’s most recent access to this file. Any operations from the open, close, or search tools will update this field.",
90
+ )
91
+
92
+
82
93
  class Human(Block):
83
94
  """Human block of the LLM context"""
84
95
 
@@ -2,6 +2,8 @@ from typing import Literal, Optional
2
2
 
3
3
  from pydantic import BaseModel, Field
4
4
 
5
+ from letta.constants import DEFAULT_EMBEDDING_CHUNK_SIZE
6
+
5
7
 
6
8
  class EmbeddingConfig(BaseModel):
7
9
  """
@@ -40,6 +42,7 @@ class EmbeddingConfig(BaseModel):
40
42
  "hugging-face",
41
43
  "mistral",
42
44
  "together", # completions endpoint
45
+ "pinecone",
43
46
  ] = Field(..., description="The endpoint type for the model.")
44
47
  embedding_endpoint: Optional[str] = Field(None, description="The endpoint for the model (`None` if local).")
45
48
  embedding_model: str = Field(..., description="The model for the embedding.")
@@ -62,7 +65,7 @@ class EmbeddingConfig(BaseModel):
62
65
  embedding_endpoint_type="openai",
63
66
  embedding_endpoint="https://api.openai.com/v1",
64
67
  embedding_dim=1536,
65
- embedding_chunk_size=300,
68
+ embedding_chunk_size=DEFAULT_EMBEDDING_CHUNK_SIZE,
66
69
  )
67
70
  if (model_name == "text-embedding-3-small" and provider == "openai") or (not model_name and provider == "openai"):
68
71
  return cls(
@@ -70,16 +73,25 @@ class EmbeddingConfig(BaseModel):
70
73
  embedding_endpoint_type="openai",
71
74
  embedding_endpoint="https://api.openai.com/v1",
72
75
  embedding_dim=2000,
73
- embedding_chunk_size=300,
76
+ embedding_chunk_size=DEFAULT_EMBEDDING_CHUNK_SIZE,
74
77
  )
75
78
  elif model_name == "letta":
76
79
  return cls(
77
80
  embedding_endpoint="https://embeddings.memgpt.ai",
78
81
  embedding_model="BAAI/bge-large-en-v1.5",
79
82
  embedding_dim=1024,
80
- embedding_chunk_size=300,
83
+ embedding_chunk_size=DEFAULT_EMBEDDING_CHUNK_SIZE,
81
84
  embedding_endpoint_type="hugging-face",
82
85
  )
86
+ elif provider == "pinecone":
87
+ # default config for pinecone with empty endpoint
88
+ return cls(
89
+ embedding_endpoint=None,
90
+ embedding_model="llama-text-embed-v2",
91
+ embedding_dim=1536, # assuming default openai dimension
92
+ embedding_chunk_size=DEFAULT_EMBEDDING_CHUNK_SIZE,
93
+ embedding_endpoint_type="pinecone",
94
+ )
83
95
  else:
84
96
  raise ValueError(f"Model {model_name} not supported.")
85
97
 
letta/schemas/enums.py CHANGED
@@ -8,6 +8,7 @@ class ProviderType(str, Enum):
8
8
  openai = "openai"
9
9
  letta = "letta"
10
10
  deepseek = "deepseek"
11
+ cerebras = "cerebras"
11
12
  lmstudio_openai = "lmstudio_openai"
12
13
  xai = "xai"
13
14
  mistral = "mistral"
@@ -17,6 +18,7 @@ class ProviderType(str, Enum):
17
18
  azure = "azure"
18
19
  vllm = "vllm"
19
20
  bedrock = "bedrock"
21
+ cohere = "cohere"
20
22
 
21
23
 
22
24
  class ProviderCategory(str, Enum):
letta/schemas/file.py CHANGED
@@ -67,7 +67,7 @@ class FileAgentBase(LettaBase):
67
67
  # Core file-agent association fields
68
68
  agent_id: str = Field(..., description="Unique identifier of the agent.")
69
69
  file_id: str = Field(..., description="Unique identifier of the file.")
70
- source_id: str = Field(..., description="Unique identifier of the source (denormalized from files.source_id).")
70
+ source_id: str = Field(..., description="Unique identifier of the source.")
71
71
  file_name: str = Field(..., description="Name of the file.")
72
72
  is_open: bool = Field(True, description="True if the agent currently has the file open.")
73
73
  visible_content: Optional[str] = Field(
@@ -0,0 +1,74 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
4
+ from pydantic import Field
5
+
6
+ from letta.schemas.embedding_config import EmbeddingConfig
7
+ from letta.schemas.letta_base import LettaBase
8
+
9
+
10
+ class BaseFolder(LettaBase):
11
+ """
12
+ Shared attributes across all folder schemas.
13
+ """
14
+
15
+ __id_prefix__ = "source" # TODO: change to "folder"
16
+
17
+ # Core folder fields
18
+ name: str = Field(..., description="The name of the folder.")
19
+ description: Optional[str] = Field(None, description="The description of the folder.")
20
+ instructions: Optional[str] = Field(None, description="Instructions for how to use the folder.")
21
+ metadata: Optional[dict] = Field(None, description="Metadata associated with the folder.")
22
+
23
+
24
+ class Folder(BaseFolder):
25
+ """
26
+ Representation of a folder, which is a collection of files and passages.
27
+
28
+ Parameters:
29
+ id (str): The ID of the folder
30
+ name (str): The name of the folder.
31
+ embedding_config (EmbeddingConfig): The embedding configuration used by the folder.
32
+ user_id (str): The ID of the user that created the folder.
33
+ metadata (dict): Metadata associated with the folder.
34
+ description (str): The description of the folder.
35
+ """
36
+
37
+ id: str = BaseFolder.generate_id_field()
38
+ embedding_config: EmbeddingConfig = Field(..., description="The embedding configuration used by the folder.")
39
+ organization_id: Optional[str] = Field(None, description="The ID of the organization that created the folder.")
40
+ metadata: Optional[dict] = Field(None, validation_alias="metadata_", description="Metadata associated with the folder.")
41
+
42
+ # metadata fields
43
+ created_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
44
+ last_updated_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
45
+ created_at: Optional[datetime] = Field(None, description="The timestamp when the folder was created.")
46
+ updated_at: Optional[datetime] = Field(None, description="The timestamp when the folder was last updated.")
47
+
48
+
49
+ class FolderCreate(BaseFolder):
50
+ """
51
+ Schema for creating a new Folder.
52
+ """
53
+
54
+ # TODO: @matt, make this required after shub makes the FE changes
55
+ embedding: Optional[str] = Field(None, description="The handle for the embedding config used by the folder.")
56
+ embedding_chunk_size: Optional[int] = Field(None, description="The chunk size of the embedding.")
57
+
58
+ # TODO: remove (legacy config)
59
+ embedding_config: Optional[EmbeddingConfig] = Field(None, description="(Legacy) The embedding configuration used by the folder.")
60
+
61
+
62
+ class FolderUpdate(BaseFolder):
63
+ """
64
+ Schema for updating an existing Folder.
65
+ """
66
+
67
+ # Override base fields to make them optional for updates
68
+ name: Optional[str] = Field(None, description="The name of the folder.")
69
+ description: Optional[str] = Field(None, description="The description of the folder.")
70
+ instructions: Optional[str] = Field(None, description="Instructions for how to use the folder.")
71
+ metadata: Optional[dict] = Field(None, description="Metadata associated with the folder.")
72
+
73
+ # Additional update-specific fields
74
+ embedding_config: Optional[EmbeddingConfig] = Field(None, description="The embedding configuration used by the folder.")
letta/schemas/memory.py CHANGED
@@ -11,7 +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.schemas.block import Block
14
+ from letta.schemas.block import Block, FileBlock
15
15
  from letta.schemas.message import Message
16
16
 
17
17
 
@@ -66,8 +66,8 @@ class Memory(BaseModel, validate_assignment=True):
66
66
 
67
67
  # Memory.block contains the list of memory blocks in the core memory
68
68
  blocks: List[Block] = Field(..., description="Memory blocks contained in the agent's in-context memory")
69
- file_blocks: List[Block] = Field(
70
- default_factory=list, description="Blocks representing the agent's in-context memory of an attached file"
69
+ file_blocks: List[FileBlock] = Field(
70
+ default_factory=list, description="Special blocks representing the agent's in-context memory of an attached file"
71
71
  )
72
72
 
73
73
  @field_validator("file_blocks")
@@ -124,7 +124,7 @@ class Memory(BaseModel, validate_assignment=True):
124
124
  Template(prompt_template)
125
125
 
126
126
  # Validate compatibility with current memory structure
127
- Template(prompt_template).render(blocks=self.blocks, file_blocks=self.file_blocks, sources=[])
127
+ Template(prompt_template).render(blocks=self.blocks, file_blocks=self.file_blocks, sources=[], max_files_open=None)
128
128
 
129
129
  # If we get here, the template is valid and compatible
130
130
  self.prompt_template = prompt_template
@@ -133,11 +133,17 @@ class Memory(BaseModel, validate_assignment=True):
133
133
  except Exception as e:
134
134
  raise ValueError(f"Prompt template is not compatible with current memory structure: {str(e)}")
135
135
 
136
- def compile(self, tool_usage_rules=None, sources=None) -> str:
136
+ def compile(self, tool_usage_rules=None, sources=None, max_files_open=None) -> str:
137
137
  """Generate a string representation of the memory in-context using the Jinja2 template"""
138
138
  try:
139
139
  template = Template(self.prompt_template)
140
- return template.render(blocks=self.blocks, file_blocks=self.file_blocks, tool_usage_rules=tool_usage_rules, sources=sources)
140
+ return template.render(
141
+ blocks=self.blocks,
142
+ file_blocks=self.file_blocks,
143
+ tool_usage_rules=tool_usage_rules,
144
+ sources=sources,
145
+ max_files_open=max_files_open,
146
+ )
141
147
  except TemplateSyntaxError as e:
142
148
  raise ValueError(f"Invalid Jinja2 template syntax: {str(e)}")
143
149
  except Exception as e:
@@ -0,0 +1,9 @@
1
+ from pydantic import Field
2
+
3
+ from letta.schemas.letta_base import OrmMetadataBase
4
+
5
+
6
+ class Prompt(OrmMetadataBase):
7
+ id: str = Field(..., description="The id of the agent. Assigned by the database.")
8
+ project_id: str | None = Field(None, description="The associated project id.")
9
+ prompt: str = Field(..., description="The string contents of the prompt.")
@@ -0,0 +1,47 @@
1
+ # Provider base classes and utilities
2
+ # Provider implementations
3
+ from .anthropic import AnthropicProvider
4
+ from .azure import AzureProvider
5
+ from .base import Provider, ProviderBase, ProviderCheck, ProviderCreate, ProviderUpdate
6
+ from .bedrock import BedrockProvider
7
+ from .cerebras import CerebrasProvider
8
+ from .cohere import CohereProvider
9
+ from .deepseek import DeepSeekProvider
10
+ from .google_gemini import GoogleAIProvider
11
+ from .google_vertex import GoogleVertexProvider
12
+ from .groq import GroqProvider
13
+ from .letta import LettaProvider
14
+ from .lmstudio import LMStudioOpenAIProvider
15
+ from .mistral import MistralProvider
16
+ from .ollama import OllamaProvider
17
+ from .openai import OpenAIProvider
18
+ from .together import TogetherProvider
19
+ from .vllm import VLLMProvider
20
+ from .xai import XAIProvider
21
+
22
+ __all__ = [
23
+ # Base classes
24
+ "Provider",
25
+ "ProviderBase",
26
+ "ProviderCreate",
27
+ "ProviderUpdate",
28
+ "ProviderCheck",
29
+ # Provider implementations
30
+ "AnthropicProvider",
31
+ "AzureProvider",
32
+ "BedrockProvider",
33
+ "CerebrasProvider", # NEW
34
+ "CohereProvider",
35
+ "DeepSeekProvider",
36
+ "GoogleAIProvider",
37
+ "GoogleVertexProvider",
38
+ "GroqProvider",
39
+ "LettaProvider",
40
+ "LMStudioOpenAIProvider",
41
+ "MistralProvider",
42
+ "OllamaProvider",
43
+ "OpenAIProvider",
44
+ "TogetherProvider",
45
+ "VLLMProvider", # Replaces ChatCompletions and Completions
46
+ "XAIProvider",
47
+ ]