letta-nightly 0.11.7.dev20250915104130__py3-none-any.whl → 0.11.7.dev20250917104122__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 (99) hide show
  1. letta/__init__.py +10 -2
  2. letta/adapters/letta_llm_request_adapter.py +0 -1
  3. letta/adapters/letta_llm_stream_adapter.py +0 -1
  4. letta/agent.py +1 -1
  5. letta/agents/letta_agent.py +1 -4
  6. letta/agents/letta_agent_v2.py +2 -1
  7. letta/agents/voice_agent.py +1 -1
  8. letta/functions/function_sets/multi_agent.py +1 -1
  9. letta/functions/helpers.py +1 -1
  10. letta/helpers/converters.py +8 -2
  11. letta/helpers/crypto_utils.py +144 -0
  12. letta/llm_api/llm_api_tools.py +0 -1
  13. letta/llm_api/llm_client_base.py +0 -2
  14. letta/orm/__init__.py +1 -0
  15. letta/orm/agent.py +5 -1
  16. letta/orm/job.py +3 -1
  17. letta/orm/mcp_oauth.py +6 -0
  18. letta/orm/mcp_server.py +7 -1
  19. letta/orm/sqlalchemy_base.py +2 -1
  20. letta/prompts/gpt_system.py +13 -15
  21. letta/prompts/system_prompts/__init__.py +27 -0
  22. letta/prompts/{system/memgpt_chat.txt → system_prompts/memgpt_chat.py} +2 -0
  23. letta/prompts/{system/memgpt_generate_tool.txt → system_prompts/memgpt_generate_tool.py} +4 -2
  24. letta/prompts/{system/memgpt_v2_chat.txt → system_prompts/memgpt_v2_chat.py} +2 -0
  25. letta/prompts/{system/react.txt → system_prompts/react.py} +2 -0
  26. letta/prompts/{system/sleeptime_doc_ingest.txt → system_prompts/sleeptime_doc_ingest.py} +2 -0
  27. letta/prompts/{system/sleeptime_v2.txt → system_prompts/sleeptime_v2.py} +2 -0
  28. letta/prompts/{system/summary_system_prompt.txt → system_prompts/summary_system_prompt.py} +2 -0
  29. letta/prompts/{system/voice_chat.txt → system_prompts/voice_chat.py} +2 -0
  30. letta/prompts/{system/voice_sleeptime.txt → system_prompts/voice_sleeptime.py} +2 -0
  31. letta/prompts/{system/workflow.txt → system_prompts/workflow.py} +2 -0
  32. letta/schemas/agent.py +10 -7
  33. letta/schemas/job.py +10 -0
  34. letta/schemas/mcp.py +146 -6
  35. letta/schemas/provider_trace.py +0 -2
  36. letta/schemas/run.py +2 -0
  37. letta/schemas/secret.py +378 -0
  38. letta/serialize_schemas/marshmallow_agent.py +4 -0
  39. letta/server/rest_api/dependencies.py +37 -0
  40. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +4 -3
  41. letta/server/rest_api/routers/v1/__init__.py +2 -0
  42. letta/server/rest_api/routers/v1/agents.py +115 -107
  43. letta/server/rest_api/routers/v1/archives.py +113 -0
  44. letta/server/rest_api/routers/v1/blocks.py +44 -20
  45. letta/server/rest_api/routers/v1/embeddings.py +3 -3
  46. letta/server/rest_api/routers/v1/folders.py +107 -47
  47. letta/server/rest_api/routers/v1/groups.py +52 -32
  48. letta/server/rest_api/routers/v1/identities.py +110 -21
  49. letta/server/rest_api/routers/v1/internal_templates.py +28 -13
  50. letta/server/rest_api/routers/v1/jobs.py +19 -14
  51. letta/server/rest_api/routers/v1/llms.py +6 -8
  52. letta/server/rest_api/routers/v1/messages.py +14 -14
  53. letta/server/rest_api/routers/v1/organizations.py +1 -1
  54. letta/server/rest_api/routers/v1/providers.py +40 -16
  55. letta/server/rest_api/routers/v1/runs.py +28 -20
  56. letta/server/rest_api/routers/v1/sandbox_configs.py +25 -25
  57. letta/server/rest_api/routers/v1/sources.py +44 -45
  58. letta/server/rest_api/routers/v1/steps.py +27 -25
  59. letta/server/rest_api/routers/v1/tags.py +11 -7
  60. letta/server/rest_api/routers/v1/telemetry.py +11 -6
  61. letta/server/rest_api/routers/v1/tools.py +78 -80
  62. letta/server/rest_api/routers/v1/users.py +1 -1
  63. letta/server/rest_api/routers/v1/voice.py +6 -5
  64. letta/server/rest_api/utils.py +1 -18
  65. letta/services/agent_manager.py +17 -9
  66. letta/services/agent_serialization_manager.py +11 -3
  67. letta/services/archive_manager.py +73 -0
  68. letta/services/file_manager.py +6 -0
  69. letta/services/group_manager.py +2 -1
  70. letta/services/helpers/agent_manager_helper.py +6 -1
  71. letta/services/identity_manager.py +67 -0
  72. letta/services/job_manager.py +18 -2
  73. letta/services/mcp_manager.py +198 -82
  74. letta/services/provider_manager.py +14 -1
  75. letta/services/source_manager.py +11 -1
  76. letta/services/telemetry_manager.py +2 -0
  77. letta/services/tool_executor/composio_tool_executor.py +1 -1
  78. letta/services/tool_manager.py +46 -9
  79. letta/services/tool_sandbox/base.py +2 -3
  80. letta/utils.py +4 -2
  81. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/METADATA +5 -2
  82. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/RECORD +85 -94
  83. letta/prompts/system/memgpt_base.txt +0 -54
  84. letta/prompts/system/memgpt_chat_compressed.txt +0 -13
  85. letta/prompts/system/memgpt_chat_fstring.txt +0 -51
  86. letta/prompts/system/memgpt_convo_only.txt +0 -12
  87. letta/prompts/system/memgpt_doc.txt +0 -50
  88. letta/prompts/system/memgpt_gpt35_extralong.txt +0 -53
  89. letta/prompts/system/memgpt_intuitive_knowledge.txt +0 -31
  90. letta/prompts/system/memgpt_memory_only.txt +0 -29
  91. letta/prompts/system/memgpt_modified_chat.txt +0 -23
  92. letta/prompts/system/memgpt_modified_o1.txt +0 -31
  93. letta/prompts/system/memgpt_offline_memory.txt +0 -23
  94. letta/prompts/system/memgpt_offline_memory_chat.txt +0 -35
  95. letta/prompts/system/memgpt_sleeptime_chat.txt +0 -52
  96. letta/prompts/system/sleeptime.txt +0 -37
  97. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/WHEEL +0 -0
  98. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/entry_points.txt +0 -0
  99. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,378 @@
1
+ import json
2
+ from typing import Any, Dict, Optional
3
+
4
+ from pydantic import BaseModel, ConfigDict, PrivateAttr
5
+
6
+ from letta.helpers.crypto_utils import CryptoUtils
7
+ from letta.log import get_logger
8
+
9
+ logger = get_logger(__name__)
10
+
11
+
12
+ class Secret(BaseModel):
13
+ """
14
+ A wrapper class for encrypted credentials that keeps values encrypted in memory.
15
+
16
+ This class ensures that sensitive data remains encrypted as much as possible
17
+ while passing through the codebase, only decrypting when absolutely necessary.
18
+
19
+ TODO: Once we deprecate plaintext columns in the database:
20
+ - Remove the dual-write logic in to_dict()
21
+ - Remove the from_db() method's plaintext_value parameter
22
+ - Remove the _was_encrypted flag (no longer needed for migration)
23
+ - Simplify get_plaintext() to only handle encrypted values
24
+ """
25
+
26
+ # Store the encrypted value
27
+ _encrypted_value: Optional[str] = PrivateAttr(default=None)
28
+ # Cache the decrypted value to avoid repeated decryption
29
+ _plaintext_cache: Optional[str] = PrivateAttr(default=None)
30
+ # Flag to indicate if the value was originally encrypted
31
+ _was_encrypted: bool = PrivateAttr(default=False)
32
+
33
+ model_config = ConfigDict(frozen=True)
34
+
35
+ @classmethod
36
+ def from_plaintext(cls, value: Optional[str]) -> "Secret":
37
+ """
38
+ Create a Secret from a plaintext value, encrypting it if possible.
39
+
40
+ Args:
41
+ value: The plaintext value to encrypt
42
+
43
+ Returns:
44
+ A Secret instance with the encrypted value, or plaintext if encryption unavailable
45
+ """
46
+ if value is None:
47
+ instance = cls()
48
+ instance._encrypted_value = None
49
+ instance._was_encrypted = False
50
+ return instance
51
+
52
+ # Try to encrypt, but fall back to plaintext if no encryption key
53
+ try:
54
+ encrypted = CryptoUtils.encrypt(value)
55
+ instance = cls()
56
+ instance._encrypted_value = encrypted
57
+ instance._was_encrypted = False
58
+ return instance
59
+ except ValueError as e:
60
+ # No encryption key available, store as plaintext
61
+ if "No encryption key configured" in str(e):
62
+ logger.warning(
63
+ "No encryption key configured. Storing Secret value as plaintext. "
64
+ "Set LETTA_ENCRYPTION_KEY environment variable to enable encryption."
65
+ )
66
+ instance = cls()
67
+ instance._encrypted_value = value # Store plaintext
68
+ instance._plaintext_cache = value # Cache it
69
+ instance._was_encrypted = False
70
+ return instance
71
+ raise # Re-raise if it's a different error
72
+
73
+ @classmethod
74
+ def from_encrypted(cls, encrypted_value: Optional[str]) -> "Secret":
75
+ """
76
+ Create a Secret from an already encrypted value.
77
+
78
+ Args:
79
+ encrypted_value: The encrypted value
80
+
81
+ Returns:
82
+ A Secret instance
83
+ """
84
+ instance = cls()
85
+ instance._encrypted_value = encrypted_value
86
+ instance._was_encrypted = True
87
+ return instance
88
+
89
+ @classmethod
90
+ def from_db(cls, encrypted_value: Optional[str], plaintext_value: Optional[str]) -> "Secret":
91
+ """
92
+ Create a Secret from database values during migration phase.
93
+
94
+ Prefers encrypted value if available, falls back to plaintext.
95
+
96
+ Args:
97
+ encrypted_value: The encrypted value from the database
98
+ plaintext_value: The plaintext value from the database
99
+
100
+ Returns:
101
+ A Secret instance
102
+ """
103
+ if encrypted_value is not None:
104
+ return cls.from_encrypted(encrypted_value)
105
+ elif plaintext_value is not None:
106
+ return cls.from_plaintext(plaintext_value)
107
+ else:
108
+ return cls.from_plaintext(None)
109
+
110
+ def get_encrypted(self) -> Optional[str]:
111
+ """
112
+ Get the encrypted value.
113
+
114
+ Returns:
115
+ The encrypted value, or None if the secret is empty
116
+ """
117
+ return self._encrypted_value
118
+
119
+ def get_plaintext(self) -> Optional[str]:
120
+ """
121
+ Get the decrypted plaintext value.
122
+
123
+ This should only be called when the plaintext is actually needed,
124
+ such as when making an external API call.
125
+
126
+ Returns:
127
+ The decrypted plaintext value
128
+ """
129
+ if self._encrypted_value is None:
130
+ return None
131
+
132
+ # Use cached value if available, but only if it looks like plaintext
133
+ # or we're confident we can decrypt it
134
+ if self._plaintext_cache is not None:
135
+ # If we have a cache but the stored value looks encrypted and we have no key,
136
+ # we should not use the cache
137
+ if CryptoUtils.is_encrypted(self._encrypted_value) and not CryptoUtils.is_encryption_available():
138
+ self._plaintext_cache = None # Clear invalid cache
139
+ else:
140
+ return self._plaintext_cache
141
+
142
+ # Decrypt and cache
143
+ try:
144
+ plaintext = CryptoUtils.decrypt(self._encrypted_value)
145
+ # Cache the decrypted value (PrivateAttr fields can be mutated even with frozen=True)
146
+ self._plaintext_cache = plaintext
147
+ return plaintext
148
+ except ValueError as e:
149
+ error_msg = str(e)
150
+
151
+ # Handle missing encryption key
152
+ if "No encryption key configured" in error_msg:
153
+ # Check if the value looks encrypted
154
+ if CryptoUtils.is_encrypted(self._encrypted_value):
155
+ # Value was encrypted, but now we have no key - can't decrypt
156
+ logger.warning(
157
+ "Cannot decrypt Secret value - no encryption key configured. "
158
+ "The value was encrypted and requires the original key to decrypt."
159
+ )
160
+ # Return None to indicate we can't get the plaintext
161
+ return None
162
+ else:
163
+ # Value is plaintext (stored when no key was available)
164
+ logger.debug("Secret value is plaintext (stored without encryption)")
165
+ self._plaintext_cache = self._encrypted_value
166
+ return self._encrypted_value
167
+
168
+ # Handle decryption failure (might be plaintext stored as such)
169
+ elif "Failed to decrypt data" in error_msg:
170
+ # Check if it might be plaintext
171
+ if not CryptoUtils.is_encrypted(self._encrypted_value):
172
+ # It's plaintext that was stored when no key was available
173
+ logger.debug("Secret value appears to be plaintext (stored without encryption)")
174
+ self._plaintext_cache = self._encrypted_value
175
+ return self._encrypted_value
176
+ # Otherwise, it's corrupted or wrong key
177
+ logger.error("Failed to decrypt Secret value - data may be corrupted or wrong key")
178
+ raise
179
+
180
+ # Migration case: handle legacy plaintext
181
+ elif not self._was_encrypted:
182
+ if self._encrypted_value and not CryptoUtils.is_encrypted(self._encrypted_value):
183
+ self._plaintext_cache = self._encrypted_value
184
+ return self._encrypted_value
185
+ return None
186
+
187
+ # Re-raise for other errors
188
+ raise
189
+
190
+ def is_empty(self) -> bool:
191
+ """Check if the secret is empty/None."""
192
+ return self._encrypted_value is None
193
+
194
+ def __str__(self) -> str:
195
+ """String representation that doesn't expose the actual value."""
196
+ if self.is_empty():
197
+ return "<Secret: empty>"
198
+ return "<Secret: ****>"
199
+
200
+ def __repr__(self) -> str:
201
+ """Representation that doesn't expose the actual value."""
202
+ return self.__str__()
203
+
204
+ def to_dict(self) -> Dict[str, Any]:
205
+ """
206
+ Convert to dictionary for database storage.
207
+
208
+ Returns both encrypted and plaintext values for dual-write during migration.
209
+ """
210
+ return {"encrypted": self.get_encrypted(), "plaintext": self.get_plaintext() if not self._was_encrypted else None}
211
+
212
+ def __eq__(self, other: Any) -> bool:
213
+ """
214
+ Compare two secrets by their plaintext values.
215
+
216
+ Note: This decrypts both values, so use sparingly.
217
+ """
218
+ if not isinstance(other, Secret):
219
+ return False
220
+ return self.get_plaintext() == other.get_plaintext()
221
+
222
+
223
+ class SecretDict(BaseModel):
224
+ """
225
+ A wrapper for dictionaries containing sensitive key-value pairs.
226
+
227
+ Used for custom headers and other key-value configurations.
228
+
229
+ TODO: Once we deprecate plaintext columns in the database:
230
+ - Remove the dual-write logic in to_dict()
231
+ - Remove the from_db() method's plaintext_value parameter
232
+ - Remove the _was_encrypted flag (no longer needed for migration)
233
+ - Simplify get_plaintext() to only handle encrypted JSON values
234
+ """
235
+
236
+ _encrypted_value: Optional[str] = PrivateAttr(default=None)
237
+ _plaintext_cache: Optional[Dict[str, str]] = PrivateAttr(default=None)
238
+ _was_encrypted: bool = PrivateAttr(default=False)
239
+
240
+ model_config = ConfigDict(frozen=True)
241
+
242
+ @classmethod
243
+ def from_plaintext(cls, value: Optional[Dict[str, str]]) -> "SecretDict":
244
+ """Create a SecretDict from a plaintext dictionary."""
245
+ if value is None:
246
+ instance = cls()
247
+ instance._encrypted_value = None
248
+ instance._was_encrypted = False
249
+ return instance
250
+
251
+ # Serialize to JSON then try to encrypt
252
+ json_str = json.dumps(value)
253
+ try:
254
+ encrypted = CryptoUtils.encrypt(json_str)
255
+ instance = cls()
256
+ instance._encrypted_value = encrypted
257
+ instance._was_encrypted = False
258
+ return instance
259
+ except ValueError as e:
260
+ # No encryption key available, store as plaintext JSON
261
+ if "No encryption key configured" in str(e):
262
+ logger.warning(
263
+ "No encryption key configured. Storing SecretDict value as plaintext JSON. "
264
+ "Set LETTA_ENCRYPTION_KEY environment variable to enable encryption."
265
+ )
266
+ instance = cls()
267
+ instance._encrypted_value = json_str # Store JSON string
268
+ instance._plaintext_cache = value # Cache the dict
269
+ instance._was_encrypted = False
270
+ return instance
271
+ raise # Re-raise if it's a different error
272
+
273
+ @classmethod
274
+ def from_encrypted(cls, encrypted_value: Optional[str]) -> "SecretDict":
275
+ """Create a SecretDict from an encrypted value."""
276
+ instance = cls()
277
+ instance._encrypted_value = encrypted_value
278
+ instance._was_encrypted = True
279
+ return instance
280
+
281
+ @classmethod
282
+ def from_db(cls, encrypted_value: Optional[str], plaintext_value: Optional[Dict[str, str]]) -> "SecretDict":
283
+ """Create a SecretDict from database values during migration phase."""
284
+ if encrypted_value is not None:
285
+ return cls.from_encrypted(encrypted_value)
286
+ elif plaintext_value is not None:
287
+ return cls.from_plaintext(plaintext_value)
288
+ else:
289
+ return cls.from_plaintext(None)
290
+
291
+ def get_encrypted(self) -> Optional[str]:
292
+ """Get the encrypted value."""
293
+ return self._encrypted_value
294
+
295
+ def get_plaintext(self) -> Optional[Dict[str, str]]:
296
+ """Get the decrypted dictionary."""
297
+ if self._encrypted_value is None:
298
+ return None
299
+
300
+ # Use cached value if available, but only if it looks like plaintext
301
+ # or we're confident we can decrypt it
302
+ if self._plaintext_cache is not None:
303
+ # If we have a cache but the stored value looks encrypted and we have no key,
304
+ # we should not use the cache
305
+ if CryptoUtils.is_encrypted(self._encrypted_value) and not CryptoUtils.is_encryption_available():
306
+ self._plaintext_cache = None # Clear invalid cache
307
+ else:
308
+ return self._plaintext_cache
309
+
310
+ try:
311
+ decrypted_json = CryptoUtils.decrypt(self._encrypted_value)
312
+ plaintext_dict = json.loads(decrypted_json)
313
+ # Cache the decrypted value (PrivateAttr fields can be mutated even with frozen=True)
314
+ self._plaintext_cache = plaintext_dict
315
+ return plaintext_dict
316
+ except ValueError as e:
317
+ error_msg = str(e)
318
+
319
+ # Handle missing encryption key
320
+ if "No encryption key configured" in error_msg:
321
+ # Check if the value looks encrypted
322
+ if CryptoUtils.is_encrypted(self._encrypted_value):
323
+ # Value was encrypted, but now we have no key - can't decrypt
324
+ logger.warning(
325
+ "Cannot decrypt SecretDict value - no encryption key configured. "
326
+ "The value was encrypted and requires the original key to decrypt."
327
+ )
328
+ # Return None to indicate we can't get the plaintext
329
+ return None
330
+ else:
331
+ # Value is plaintext JSON (stored when no key was available)
332
+ logger.debug("SecretDict value is plaintext JSON (stored without encryption)")
333
+ try:
334
+ plaintext_dict = json.loads(self._encrypted_value)
335
+ self._plaintext_cache = plaintext_dict
336
+ return plaintext_dict
337
+ except json.JSONDecodeError:
338
+ logger.error("Failed to parse SecretDict plaintext as JSON")
339
+ return None
340
+
341
+ # Handle decryption failure (might be plaintext JSON)
342
+ elif "Failed to decrypt data" in error_msg:
343
+ # Check if it might be plaintext JSON
344
+ if not CryptoUtils.is_encrypted(self._encrypted_value):
345
+ # It's plaintext JSON that was stored when no key was available
346
+ logger.debug("SecretDict value appears to be plaintext JSON (stored without encryption)")
347
+ try:
348
+ plaintext_dict = json.loads(self._encrypted_value)
349
+ self._plaintext_cache = plaintext_dict
350
+ return plaintext_dict
351
+ except json.JSONDecodeError:
352
+ logger.error("Failed to parse SecretDict plaintext as JSON")
353
+ return None
354
+ # Otherwise, it's corrupted or wrong key
355
+ logger.error("Failed to decrypt SecretDict value - data may be corrupted or wrong key")
356
+ raise
357
+
358
+ # Migration case: handle legacy plaintext
359
+ elif not self._was_encrypted:
360
+ if self._encrypted_value:
361
+ try:
362
+ plaintext_dict = json.loads(self._encrypted_value)
363
+ self._plaintext_cache = plaintext_dict
364
+ return plaintext_dict
365
+ except json.JSONDecodeError:
366
+ pass
367
+ return None
368
+
369
+ # Re-raise for other errors
370
+ raise
371
+
372
+ def is_empty(self) -> bool:
373
+ """Check if the secret dict is empty/None."""
374
+ return self._encrypted_value is None
375
+
376
+ def to_dict(self) -> Dict[str, Any]:
377
+ """Convert to dictionary for database storage."""
378
+ return {"encrypted": self.get_encrypted(), "plaintext": self.get_plaintext() if not self._was_encrypted else None}
@@ -40,6 +40,7 @@ class MarshmallowAgentSchema(BaseSchema):
40
40
  core_memory = fields.List(fields.Nested(SerializedBlockSchema))
41
41
  tools = fields.List(fields.Nested(SerializedToolSchema))
42
42
  tool_exec_environment_variables = fields.List(fields.Nested(SerializedAgentEnvironmentVariableSchema))
43
+ secrets = fields.List(fields.Nested(SerializedAgentEnvironmentVariableSchema))
43
44
  tags = fields.List(fields.Nested(SerializedAgentTagSchema))
44
45
 
45
46
  def __init__(self, *args, session: sessionmaker, actor: User, max_steps: Optional[int] = None, **kwargs):
@@ -214,6 +215,9 @@ class MarshmallowAgentSchema(BaseSchema):
214
215
  for env_var in data.get("tool_exec_environment_variables", []):
215
216
  # need to be re-set at load time
216
217
  env_var["value"] = ""
218
+ for env_var in data.get("secrets", []):
219
+ # need to be re-set at load time
220
+ env_var["value"] = ""
217
221
  return data
218
222
 
219
223
  @pre_load
@@ -0,0 +1,37 @@
1
+ from typing import TYPE_CHECKING, Optional
2
+
3
+ from fastapi import Header
4
+ from pydantic import BaseModel
5
+
6
+ if TYPE_CHECKING:
7
+ from letta.server.server import SyncServer
8
+
9
+
10
+ class HeaderParams(BaseModel):
11
+ """Common header parameters used across REST API endpoints."""
12
+
13
+ actor_id: Optional[str] = None
14
+ user_agent: Optional[str] = None
15
+ project_id: Optional[str] = None
16
+
17
+
18
+ def get_headers(
19
+ actor_id: Optional[str] = Header(None, alias="user_id"),
20
+ user_agent: Optional[str] = Header(None, alias="User-Agent"),
21
+ project_id: Optional[str] = Header(None, alias="X-Project-Id"),
22
+ ) -> HeaderParams:
23
+ """Dependency injection function to extract common headers from requests."""
24
+ return HeaderParams(
25
+ actor_id=actor_id,
26
+ user_agent=user_agent,
27
+ project_id=project_id,
28
+ )
29
+
30
+
31
+ # TODO: why does this double up the interface?
32
+ async def get_letta_server() -> "SyncServer":
33
+ # Check if a global server is already instantiated
34
+ from letta.server.rest_api.app import server
35
+
36
+ # assert isinstance(server, SyncServer)
37
+ return server
@@ -11,9 +11,10 @@ from letta.log import get_logger
11
11
  from letta.schemas.message import Message, MessageCreate
12
12
  from letta.schemas.user import User
13
13
  from letta.server.rest_api.chat_completions_interface import ChatCompletionsStreamingInterface
14
+ from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
14
15
 
15
16
  # TODO this belongs in a controller!
16
- from letta.server.rest_api.utils import get_letta_server, get_user_message_from_chat_completions_request, sse_async_generator
17
+ from letta.server.rest_api.utils import get_user_message_from_chat_completions_request, sse_async_generator
17
18
  from letta.utils import safe_create_task
18
19
 
19
20
  if TYPE_CHECKING:
@@ -39,13 +40,13 @@ async def create_chat_completions(
39
40
  agent_id: str,
40
41
  completion_request: CompletionCreateParams = Body(...),
41
42
  server: "SyncServer" = Depends(get_letta_server),
42
- user_id: Optional[str] = Header(None, alias="user_id"),
43
+ headers: HeaderParams = Depends(get_headers),
43
44
  ):
44
45
  # Validate and process fields
45
46
  if not completion_request["stream"]:
46
47
  raise HTTPException(status_code=400, detail="Must be streaming request: `stream` was set to `False` in the request.")
47
48
 
48
- actor = server.user_manager.get_user_or_default(user_id=user_id)
49
+ actor = server.user_manager.get_user_or_default(user_id=headers.actor_id)
49
50
 
50
51
  letta_agent = server.load_agent(agent_id=agent_id, actor=actor)
51
52
  llm_config = letta_agent.agent_state.llm_config
@@ -1,4 +1,5 @@
1
1
  from letta.server.rest_api.routers.v1.agents import router as agents_router
2
+ from letta.server.rest_api.routers.v1.archives import router as archives_router
2
3
  from letta.server.rest_api.routers.v1.blocks import router as blocks_router
3
4
  from letta.server.rest_api.routers.v1.embeddings import router as embeddings_router
4
5
  from letta.server.rest_api.routers.v1.folders import router as folders_router
@@ -20,6 +21,7 @@ from letta.server.rest_api.routers.v1.tools import router as tools_router
20
21
  from letta.server.rest_api.routers.v1.voice import router as voice_router
21
22
 
22
23
  ROUTERS = [
24
+ archives_router,
23
25
  tools_router,
24
26
  sources_router,
25
27
  folders_router,