letta-nightly 0.4.1.dev20241008104105__py3-none-any.whl → 0.4.1.dev20241009104130__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.

Potentially problematic release.


This version of letta-nightly might be problematic. Click here for more details.

Files changed (32) hide show
  1. letta/agent.py +18 -2
  2. letta/client/client.py +8 -1
  3. letta/credentials.py +2 -2
  4. letta/functions/schema_generator.py +1 -1
  5. letta/llm_api/anthropic.py +3 -24
  6. letta/llm_api/azure_openai.py +47 -98
  7. letta/llm_api/azure_openai_constants.py +10 -0
  8. letta/llm_api/google_ai.py +39 -64
  9. letta/llm_api/helpers.py +57 -2
  10. letta/llm_api/llm_api_tools.py +4 -3
  11. letta/llm_api/openai.py +5 -49
  12. letta/main.py +1 -1
  13. letta/metadata.py +2 -0
  14. letta/providers.py +139 -30
  15. letta/schemas/agent.py +14 -0
  16. letta/schemas/llm_config.py +0 -3
  17. letta/schemas/openai/chat_completion_response.py +3 -0
  18. letta/schemas/tool.py +3 -3
  19. letta/server/rest_api/routers/openai/assistants/threads.py +5 -5
  20. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +2 -2
  21. letta/server/rest_api/routers/v1/agents.py +11 -11
  22. letta/server/rest_api/routers/v1/blocks.py +2 -2
  23. letta/server/rest_api/routers/v1/jobs.py +2 -2
  24. letta/server/rest_api/routers/v1/sources.py +12 -12
  25. letta/server/rest_api/routers/v1/tools.py +6 -6
  26. letta/server/server.py +18 -5
  27. letta/settings.py +3 -112
  28. {letta_nightly-0.4.1.dev20241008104105.dist-info → letta_nightly-0.4.1.dev20241009104130.dist-info}/METADATA +1 -1
  29. {letta_nightly-0.4.1.dev20241008104105.dist-info → letta_nightly-0.4.1.dev20241009104130.dist-info}/RECORD +32 -31
  30. {letta_nightly-0.4.1.dev20241008104105.dist-info → letta_nightly-0.4.1.dev20241009104130.dist-info}/LICENSE +0 -0
  31. {letta_nightly-0.4.1.dev20241008104105.dist-info → letta_nightly-0.4.1.dev20241009104130.dist-info}/WHEEL +0 -0
  32. {letta_nightly-0.4.1.dev20241008104105.dist-info → letta_nightly-0.4.1.dev20241009104130.dist-info}/entry_points.txt +0 -0
@@ -28,7 +28,6 @@ from letta.local_llm.constants import (
28
28
  INNER_THOUGHTS_KWARG,
29
29
  INNER_THOUGHTS_KWARG_DESCRIPTION,
30
30
  )
31
- from letta.providers import GoogleAIProvider
32
31
  from letta.schemas.enums import OptionState
33
32
  from letta.schemas.llm_config import LLMConfig
34
33
  from letta.schemas.message import Message
@@ -189,6 +188,9 @@ def create(
189
188
  if model_settings.azure_base_url is None:
190
189
  raise ValueError(f"Azure base url is missing. Did you set AZURE_BASE_URL in your env?")
191
190
 
191
+ if model_settings.azure_api_version is None:
192
+ raise ValueError(f"Azure API version is missing. Did you set AZURE_API_VERSION in your env?")
193
+
192
194
  # Set the llm config model_endpoint from model_settings
193
195
  # For Azure, this model_endpoint is required to be configured via env variable, so users don't need to provide it in the LLM config
194
196
  llm_config.model_endpoint = model_settings.azure_base_url
@@ -228,7 +230,7 @@ def create(
228
230
 
229
231
  return google_ai_chat_completions_request(
230
232
  inner_thoughts_in_kwargs=google_ai_inner_thoughts_in_kwarg,
231
- service_endpoint=GoogleAIProvider(model_settings.gemini_api_key).service_endpoint,
233
+ base_url=llm_config.model_endpoint,
232
234
  model=llm_config.model,
233
235
  api_key=model_settings.gemini_api_key,
234
236
  # see structure of payload here: https://ai.google.dev/docs/function_calling
@@ -296,7 +298,6 @@ def create(
296
298
  raise NotImplementedError(f"Streaming not yet implemented for Groq.")
297
299
 
298
300
  if model_settings.groq_api_key is None and llm_config.model_endpoint == "https://api.groq.com/openai/v1/chat/completions":
299
- # only is a problem if we are *not* using an openai proxy
300
301
  raise ValueError(f"Groq key is missing from letta config file")
301
302
 
302
303
  # force to true for groq, since they don't support 'content' is non-null
letta/llm_api/openai.py CHANGED
@@ -9,7 +9,7 @@ from httpx_sse._exceptions import SSEError
9
9
 
10
10
  from letta.constants import OPENAI_CONTEXT_WINDOW_ERROR_SUBSTRING
11
11
  from letta.errors import LLMError
12
- from letta.llm_api.helpers import add_inner_thoughts_to_functions
12
+ from letta.llm_api.helpers import add_inner_thoughts_to_functions, make_post_request
13
13
  from letta.local_llm.constants import (
14
14
  INNER_THOUGHTS_KWARG,
15
15
  INNER_THOUGHTS_KWARG_DESCRIPTION,
@@ -483,58 +483,14 @@ def openai_chat_completions_request(
483
483
  data.pop("tools")
484
484
  data.pop("tool_choice", None) # extra safe, should exist always (default="auto")
485
485
 
486
- printd(f"Sending request to {url}")
487
- try:
488
- response = requests.post(url, headers=headers, json=data)
489
- printd(f"response = {response}, response.text = {response.text}")
490
- # print(json.dumps(data, indent=4))
491
- # raise requests.exceptions.HTTPError
492
- response.raise_for_status() # Raises HTTPError for 4XX/5XX status
493
-
494
- response = response.json() # convert to dict from string
495
- printd(f"response.json = {response}")
496
-
497
- response = ChatCompletionResponse(**response) # convert to 'dot-dict' style which is the openai python client default
498
- return response
499
- except requests.exceptions.HTTPError as http_err:
500
- # Handle HTTP errors (e.g., response 4XX, 5XX)
501
- printd(f"Got HTTPError, exception={http_err}, payload={data}")
502
- raise http_err
503
- except requests.exceptions.RequestException as req_err:
504
- # Handle other requests-related errors (e.g., connection error)
505
- printd(f"Got RequestException, exception={req_err}")
506
- raise req_err
507
- except Exception as e:
508
- # Handle other potential errors
509
- printd(f"Got unknown Exception, exception={e}")
510
- raise e
486
+ response_json = make_post_request(url, headers, data)
487
+ return ChatCompletionResponse(**response_json)
511
488
 
512
489
 
513
490
  def openai_embeddings_request(url: str, api_key: str, data: dict) -> EmbeddingResponse:
514
491
  """https://platform.openai.com/docs/api-reference/embeddings/create"""
515
- from letta.utils import printd
516
492
 
517
493
  url = smart_urljoin(url, "embeddings")
518
494
  headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
519
-
520
- printd(f"Sending request to {url}")
521
- try:
522
- response = requests.post(url, headers=headers, json=data)
523
- printd(f"response = {response}")
524
- response.raise_for_status() # Raises HTTPError for 4XX/5XX status
525
- response = response.json() # convert to dict from string
526
- printd(f"response.json = {response}")
527
- response = EmbeddingResponse(**response) # convert to 'dot-dict' style which is the openai python client default
528
- return response
529
- except requests.exceptions.HTTPError as http_err:
530
- # Handle HTTP errors (e.g., response 4XX, 5XX)
531
- printd(f"Got HTTPError, exception={http_err}, payload={data}")
532
- raise http_err
533
- except requests.exceptions.RequestException as req_err:
534
- # Handle other requests-related errors (e.g., connection error)
535
- printd(f"Got RequestException, exception={req_err}")
536
- raise req_err
537
- except Exception as e:
538
- # Handle other potential errors
539
- printd(f"Got unknown Exception, exception={e}")
540
- raise e
495
+ response_json = make_post_request(url, headers, data)
496
+ return EmbeddingResponse(**response_json)
letta/main.py CHANGED
@@ -366,7 +366,7 @@ def run_agent_loop(
366
366
  first_message=False,
367
367
  skip_verify=no_verify,
368
368
  stream=stream,
369
- inner_thoughts_in_kwargs=inner_thoughts_in_kwargs,
369
+ inner_thoughts_in_kwargs_option=inner_thoughts_in_kwargs,
370
370
  ms=ms,
371
371
  )
372
372
  new_messages = step_response.messages
letta/metadata.py CHANGED
@@ -218,6 +218,7 @@ class AgentModel(Base):
218
218
  tools = Column(JSON)
219
219
 
220
220
  # configs
221
+ agent_type = Column(String)
221
222
  llm_config = Column(LLMConfigColumn)
222
223
  embedding_config = Column(EmbeddingConfigColumn)
223
224
 
@@ -243,6 +244,7 @@ class AgentModel(Base):
243
244
  memory=Memory.load(self.memory), # load dictionary
244
245
  system=self.system,
245
246
  tools=self.tools,
247
+ agent_type=self.agent_type,
246
248
  llm_config=self.llm_config,
247
249
  embedding_config=self.embedding_config,
248
250
  metadata_=self.metadata_,
letta/providers.py CHANGED
@@ -1,8 +1,13 @@
1
1
  from typing import List, Optional
2
2
 
3
- from pydantic import BaseModel, Field
3
+ from pydantic import BaseModel, Field, model_validator
4
4
 
5
5
  from letta.constants import LLM_MAX_TOKENS
6
+ from letta.llm_api.azure_openai import (
7
+ get_azure_chat_completions_endpoint,
8
+ get_azure_embeddings_endpoint,
9
+ )
10
+ from letta.llm_api.azure_openai_constants import AZURE_MODEL_TO_CONTEXT_LENGTH
6
11
  from letta.schemas.embedding_config import EmbeddingConfig
7
12
  from letta.schemas.llm_config import LLMConfig
8
13
 
@@ -122,34 +127,64 @@ class OllamaProvider(OpenAIProvider):
122
127
  response = requests.post(f"{self.base_url}/api/show", json={"name": model_name, "verbose": True})
123
128
  response_json = response.json()
124
129
 
125
- # thank you vLLM: https://github.com/vllm-project/vllm/blob/main/vllm/config.py#L1675
126
- possible_keys = [
127
- # OPT
128
- "max_position_embeddings",
129
- # GPT-2
130
- "n_positions",
131
- # MPT
132
- "max_seq_len",
133
- # ChatGLM2
134
- "seq_length",
135
- # Command-R
136
- "model_max_length",
137
- # Others
138
- "max_sequence_length",
139
- "max_seq_length",
140
- "seq_len",
141
- ]
142
-
130
+ ## thank you vLLM: https://github.com/vllm-project/vllm/blob/main/vllm/config.py#L1675
131
+ # possible_keys = [
132
+ # # OPT
133
+ # "max_position_embeddings",
134
+ # # GPT-2
135
+ # "n_positions",
136
+ # # MPT
137
+ # "max_seq_len",
138
+ # # ChatGLM2
139
+ # "seq_length",
140
+ # # Command-R
141
+ # "model_max_length",
142
+ # # Others
143
+ # "max_sequence_length",
144
+ # "max_seq_length",
145
+ # "seq_len",
146
+ # ]
143
147
  # max_position_embeddings
144
148
  # parse model cards: nous, dolphon, llama
145
149
  for key, value in response_json["model_info"].items():
146
- if "context_window" in key:
150
+ if "context_length" in key:
151
+ return value
152
+ return None
153
+
154
+ def get_model_embedding_dim(self, model_name: str):
155
+ import requests
156
+
157
+ response = requests.post(f"{self.base_url}/api/show", json={"name": model_name, "verbose": True})
158
+ response_json = response.json()
159
+ for key, value in response_json["model_info"].items():
160
+ if "embedding_length" in key:
147
161
  return value
148
162
  return None
149
163
 
150
164
  def list_embedding_models(self) -> List[EmbeddingConfig]:
151
- # TODO: filter embedding models
152
- return []
165
+ # https://github.com/ollama/ollama/blob/main/docs/api.md#list-local-models
166
+ import requests
167
+
168
+ response = requests.get(f"{self.base_url}/api/tags")
169
+ if response.status_code != 200:
170
+ raise Exception(f"Failed to list Ollama models: {response.text}")
171
+ response_json = response.json()
172
+
173
+ configs = []
174
+ for model in response_json["models"]:
175
+ embedding_dim = self.get_model_embedding_dim(model["name"])
176
+ if not embedding_dim:
177
+ continue
178
+ configs.append(
179
+ EmbeddingConfig(
180
+ embedding_model=model["name"],
181
+ embedding_endpoint_type="ollama",
182
+ embedding_endpoint=self.base_url,
183
+ embedding_dim=embedding_dim,
184
+ embedding_chunk_size=300,
185
+ )
186
+ )
187
+ return configs
153
188
 
154
189
 
155
190
  class GroqProvider(OpenAIProvider):
@@ -182,20 +217,21 @@ class GroqProvider(OpenAIProvider):
182
217
  class GoogleAIProvider(Provider):
183
218
  # gemini
184
219
  api_key: str = Field(..., description="API key for the Google AI API.")
185
- service_endpoint: str = "generativelanguage"
186
220
  base_url: str = "https://generativelanguage.googleapis.com"
187
221
 
188
222
  def list_llm_models(self):
189
223
  from letta.llm_api.google_ai import google_ai_get_model_list
190
224
 
191
- # TODO: use base_url instead
192
- model_options = google_ai_get_model_list(service_endpoint=self.service_endpoint, api_key=self.api_key)
225
+ model_options = google_ai_get_model_list(base_url=self.base_url, api_key=self.api_key)
226
+ # filter by 'generateContent' models
227
+ model_options = [mo for mo in model_options if "generateContent" in mo["supportedGenerationMethods"]]
193
228
  model_options = [str(m["name"]) for m in model_options]
229
+
230
+ # filter by model names
194
231
  model_options = [mo[len("models/") :] if mo.startswith("models/") else mo for mo in model_options]
232
+
195
233
  # TODO remove manual filtering for gemini-pro
196
234
  model_options = [mo for mo in model_options if str(mo).startswith("gemini") and "-pro" in str(mo)]
197
- # TODO: add context windows
198
- # model_options = ["gemini-pro"]
199
235
 
200
236
  configs = []
201
237
  for model in model_options:
@@ -210,21 +246,94 @@ class GoogleAIProvider(Provider):
210
246
  return configs
211
247
 
212
248
  def list_embedding_models(self):
213
- return []
249
+ from letta.llm_api.google_ai import google_ai_get_model_list
250
+
251
+ # TODO: use base_url instead
252
+ model_options = google_ai_get_model_list(base_url=self.base_url, api_key=self.api_key)
253
+ # filter by 'generateContent' models
254
+ model_options = [mo for mo in model_options if "embedContent" in mo["supportedGenerationMethods"]]
255
+ model_options = [str(m["name"]) for m in model_options]
256
+ model_options = [mo[len("models/") :] if mo.startswith("models/") else mo for mo in model_options]
257
+
258
+ configs = []
259
+ for model in model_options:
260
+ configs.append(
261
+ EmbeddingConfig(
262
+ embedding_model=model,
263
+ embedding_endpoint_type="google_ai",
264
+ embedding_endpoint=self.base_url,
265
+ embedding_dim=768,
266
+ embedding_chunk_size=300, # NOTE: max is 2048
267
+ )
268
+ )
269
+ return configs
214
270
 
215
271
  def get_model_context_window(self, model_name: str):
216
272
  from letta.llm_api.google_ai import google_ai_get_model_context_window
217
273
 
218
- # TODO: use base_url instead
219
- return google_ai_get_model_context_window(self.service_endpoint, self.api_key, model_name)
274
+ return google_ai_get_model_context_window(self.base_url, self.api_key, model_name)
220
275
 
221
276
 
222
277
  class AzureProvider(Provider):
223
278
  name: str = "azure"
279
+ latest_api_version: str = "2024-09-01-preview" # https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
224
280
  base_url: str = Field(
225
281
  ..., description="Base URL for the Azure API endpoint. This should be specific to your org, e.g. `https://letta.openai.azure.com`."
226
282
  )
227
283
  api_key: str = Field(..., description="API key for the Azure API.")
284
+ api_version: str = Field(latest_api_version, description="API version for the Azure API")
285
+
286
+ @model_validator(mode="before")
287
+ def set_default_api_version(cls, values):
288
+ """
289
+ This ensures that api_version is always set to the default if None is passed in.
290
+ """
291
+ if values.get("api_version") is None:
292
+ values["api_version"] = cls.model_fields["latest_api_version"].default
293
+ return values
294
+
295
+ def list_llm_models(self) -> List[LLMConfig]:
296
+ from letta.llm_api.azure_openai import (
297
+ azure_openai_get_chat_completion_model_list,
298
+ )
299
+
300
+ model_options = azure_openai_get_chat_completion_model_list(self.base_url, api_key=self.api_key, api_version=self.api_version)
301
+ configs = []
302
+ for model_option in model_options:
303
+ model_name = model_option["id"]
304
+ context_window_size = self.get_model_context_window(model_name)
305
+ model_endpoint = get_azure_chat_completions_endpoint(self.base_url, model_name, self.api_version)
306
+ configs.append(
307
+ LLMConfig(model=model_name, model_endpoint_type="azure", model_endpoint=model_endpoint, context_window=context_window_size)
308
+ )
309
+ return configs
310
+
311
+ def list_embedding_models(self) -> List[EmbeddingConfig]:
312
+ from letta.llm_api.azure_openai import azure_openai_get_embeddings_model_list
313
+
314
+ model_options = azure_openai_get_embeddings_model_list(
315
+ self.base_url, api_key=self.api_key, api_version=self.api_version, require_embedding_in_name=True
316
+ )
317
+ configs = []
318
+ for model_option in model_options:
319
+ model_name = model_option["id"]
320
+ model_endpoint = get_azure_embeddings_endpoint(self.base_url, model_name, self.api_version)
321
+ configs.append(
322
+ EmbeddingConfig(
323
+ embedding_model=model_name,
324
+ embedding_endpoint_type="azure",
325
+ embedding_endpoint=model_endpoint,
326
+ embedding_dim=768,
327
+ embedding_chunk_size=300, # NOTE: max is 2048
328
+ )
329
+ )
330
+ return configs
331
+
332
+ def get_model_context_window(self, model_name: str):
333
+ """
334
+ This is hardcoded for now, since there is no API endpoints to retrieve metadata for a model.
335
+ """
336
+ return AZURE_MODEL_TO_CONTEXT_LENGTH.get(model_name, 4096)
228
337
 
229
338
 
230
339
  class VLLMProvider(OpenAIProvider):
letta/schemas/agent.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import uuid
2
2
  from datetime import datetime
3
+ from enum import Enum
3
4
  from typing import Dict, List, Optional, Union
4
5
 
5
6
  from pydantic import BaseModel, Field, field_validator
@@ -21,6 +22,15 @@ class BaseAgent(LettaBase, validate_assignment=True):
21
22
  user_id: Optional[str] = Field(None, description="The user id of the agent.")
22
23
 
23
24
 
25
+ class AgentType(str, Enum):
26
+ """
27
+ Enum to represent the type of agent.
28
+ """
29
+
30
+ memgpt_agent = "memgpt_agent"
31
+ split_thread_agent = "split_thread_agent"
32
+
33
+
24
34
  class AgentState(BaseAgent):
25
35
  """
26
36
  Representation of an agent's state. This is the state of the agent at a given time, and is persisted in the DB backend. The state has all the information needed to recreate a persisted agent.
@@ -52,6 +62,9 @@ class AgentState(BaseAgent):
52
62
  # system prompt
53
63
  system: str = Field(..., description="The system prompt used by the agent.")
54
64
 
65
+ # agent configuration
66
+ agent_type: AgentType = Field(..., description="The type of agent.")
67
+
55
68
  # llm information
56
69
  llm_config: LLMConfig = Field(..., description="The LLM configuration used by the agent.")
57
70
  embedding_config: EmbeddingConfig = Field(..., description="The embedding configuration used by the agent.")
@@ -64,6 +77,7 @@ class CreateAgent(BaseAgent):
64
77
  memory: Optional[Memory] = Field(None, description="The in-context memory of the agent.")
65
78
  tools: Optional[List[str]] = Field(None, description="The tools used by the agent.")
66
79
  system: Optional[str] = Field(None, description="The system prompt used by the agent.")
80
+ agent_type: Optional[AgentType] = Field(None, description="The type of agent.")
67
81
  llm_config: Optional[LLMConfig] = Field(None, description="The LLM configuration used by the agent.")
68
82
  embedding_config: Optional[EmbeddingConfig] = Field(None, description="The embedding configuration used by the agent.")
69
83
 
@@ -35,9 +35,6 @@ class LLMConfig(BaseModel):
35
35
  "hugging-face",
36
36
  ] = Field(..., description="The endpoint type for the model.")
37
37
  model_endpoint: Optional[str] = Field(None, description="The endpoint for the model.")
38
- api_version: Optional[str] = Field(
39
- None, description="The version for the model API. Used by the Azure provider backend, e.g. 2023-03-15-preview."
40
- )
41
38
  model_wrapper: Optional[str] = Field(None, description="The wrapper for the model.")
42
39
  context_window: int = Field(..., description="The context window size for the model.")
43
40
 
@@ -74,6 +74,9 @@ class ChatCompletionResponse(BaseModel):
74
74
  object: Literal["chat.completion"] = "chat.completion"
75
75
  usage: UsageStatistics
76
76
 
77
+ def __str__(self):
78
+ return self.model_dump_json(indent=4)
79
+
77
80
 
78
81
  class FunctionCallDelta(BaseModel):
79
82
  # arguments: Optional[str] = None
letta/schemas/tool.py CHANGED
@@ -93,7 +93,7 @@ class Tool(BaseTool):
93
93
  # append heartbeat (necessary for triggering another reasoning step after this tool call)
94
94
  json_schema["parameters"]["properties"]["request_heartbeat"] = {
95
95
  "type": "boolean",
96
- "description": "Request an immediate heartbeat after function execution. Set to 'true' if you want to send a follow-up message or run a follow-up function.",
96
+ "description": "Request an immediate heartbeat after function execution. Set to `True` if you want to send a follow-up message or run a follow-up function.",
97
97
  }
98
98
  json_schema["parameters"]["required"].append("request_heartbeat")
99
99
 
@@ -128,7 +128,7 @@ class Tool(BaseTool):
128
128
  # append heartbeat (necessary for triggering another reasoning step after this tool call)
129
129
  json_schema["parameters"]["properties"]["request_heartbeat"] = {
130
130
  "type": "boolean",
131
- "description": "Request an immediate heartbeat after function execution. Set to 'true' if you want to send a follow-up message or run a follow-up function.",
131
+ "description": "Request an immediate heartbeat after function execution. Set to `True` if you want to send a follow-up message or run a follow-up function.",
132
132
  }
133
133
  json_schema["parameters"]["required"].append("request_heartbeat")
134
134
 
@@ -161,7 +161,7 @@ class Tool(BaseTool):
161
161
  # append heartbeat (necessary for triggering another reasoning step after this tool call)
162
162
  json_schema["parameters"]["properties"]["request_heartbeat"] = {
163
163
  "type": "boolean",
164
- "description": "Request an immediate heartbeat after function execution. Set to 'true' if you want to send a follow-up message or run a follow-up function.",
164
+ "description": "Request an immediate heartbeat after function execution. Set to `True` if you want to send a follow-up message or run a follow-up function.",
165
165
  }
166
166
  json_schema["parameters"]["required"].append("request_heartbeat")
167
167
 
@@ -1,5 +1,5 @@
1
1
  import uuid
2
- from typing import TYPE_CHECKING, List
2
+ from typing import TYPE_CHECKING, List, Optional
3
3
 
4
4
  from fastapi import APIRouter, Body, Depends, Header, HTTPException, Path, Query
5
5
 
@@ -43,7 +43,7 @@ router = APIRouter(prefix="/v1/threads", tags=["threads"])
43
43
  def create_thread(
44
44
  request: CreateThreadRequest = Body(...),
45
45
  server: SyncServer = Depends(get_letta_server),
46
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
46
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
47
47
  ):
48
48
  # TODO: use requests.description and requests.metadata fields
49
49
  # TODO: handle requests.file_ids and requests.tools
@@ -68,7 +68,7 @@ def create_thread(
68
68
  def retrieve_thread(
69
69
  thread_id: str = Path(..., description="The unique identifier of the thread."),
70
70
  server: SyncServer = Depends(get_letta_server),
71
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
71
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
72
72
  ):
73
73
  actor = server.get_user_or_default(user_id=user_id)
74
74
  agent = server.get_agent(user_id=actor.id, agent_id=thread_id)
@@ -102,7 +102,7 @@ def create_message(
102
102
  thread_id: str = Path(..., description="The unique identifier of the thread."),
103
103
  request: CreateMessageRequest = Body(...),
104
104
  server: SyncServer = Depends(get_letta_server),
105
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
105
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
106
106
  ):
107
107
  actor = server.get_user_or_default(user_id=user_id)
108
108
  agent_id = thread_id
@@ -146,7 +146,7 @@ def list_messages(
146
146
  after: str = Query(None, description="A cursor for use in pagination. `after` is an object ID that defines your place in the list."),
147
147
  before: str = Query(None, description="A cursor for use in pagination. `after` is an object ID that defines your place in the list."),
148
148
  server: SyncServer = Depends(get_letta_server),
149
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
149
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
150
150
  ):
151
151
  actor = server.get_user_or_default(user_id)
152
152
  after_uuid = after if before else None
@@ -1,5 +1,5 @@
1
1
  import json
2
- from typing import TYPE_CHECKING
2
+ from typing import TYPE_CHECKING, Optional
3
3
 
4
4
  from fastapi import APIRouter, Body, Depends, Header, HTTPException
5
5
 
@@ -30,7 +30,7 @@ router = APIRouter(prefix="/v1/chat/completions", tags=["chat_completions"])
30
30
  async def create_chat_completion(
31
31
  completion_request: ChatCompletionRequest = Body(...),
32
32
  server: "SyncServer" = Depends(get_letta_server),
33
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
33
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
34
34
  ):
35
35
  """Send a message to a Letta agent via a /chat/completions completion_request
36
36
  The bearer token will be used to identify the user.
@@ -40,7 +40,7 @@ router = APIRouter(prefix="/agents", tags=["agents"])
40
40
  @router.get("/", response_model=List[AgentState], operation_id="list_agents")
41
41
  def list_agents(
42
42
  server: "SyncServer" = Depends(get_letta_server),
43
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
43
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
44
44
  ):
45
45
  """
46
46
  List all agents associated with a given user.
@@ -55,7 +55,7 @@ def list_agents(
55
55
  def create_agent(
56
56
  agent: CreateAgent = Body(...),
57
57
  server: "SyncServer" = Depends(get_letta_server),
58
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
58
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
59
59
  ):
60
60
  """
61
61
  Create a new agent with the specified configuration.
@@ -76,7 +76,7 @@ def update_agent(
76
76
  agent_id: str,
77
77
  update_agent: UpdateAgentState = Body(...),
78
78
  server: "SyncServer" = Depends(get_letta_server),
79
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
79
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
80
80
  ):
81
81
  """Update an exsiting agent"""
82
82
  actor = server.get_user_or_default(user_id=user_id)
@@ -89,7 +89,7 @@ def update_agent(
89
89
  def get_agent_state(
90
90
  agent_id: str,
91
91
  server: "SyncServer" = Depends(get_letta_server),
92
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
92
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
93
93
  ):
94
94
  """
95
95
  Get the state of the agent.
@@ -107,7 +107,7 @@ def get_agent_state(
107
107
  def delete_agent(
108
108
  agent_id: str,
109
109
  server: "SyncServer" = Depends(get_letta_server),
110
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
110
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
111
111
  ):
112
112
  """
113
113
  Delete an agent.
@@ -159,7 +159,7 @@ def update_agent_memory(
159
159
  agent_id: str,
160
160
  request: Dict = Body(...),
161
161
  server: "SyncServer" = Depends(get_letta_server),
162
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
162
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
163
163
  ):
164
164
  """
165
165
  Update the core memory of a specific agent.
@@ -202,7 +202,7 @@ def get_agent_archival_memory(
202
202
  after: Optional[int] = Query(None, description="Unique ID of the memory to start the query range at."),
203
203
  before: Optional[int] = Query(None, description="Unique ID of the memory to end the query range at."),
204
204
  limit: Optional[int] = Query(None, description="How many results to include in the response."),
205
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
205
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
206
206
  ):
207
207
  """
208
208
  Retrieve the memories in an agent's archival memory store (paginated query).
@@ -227,7 +227,7 @@ def insert_agent_archival_memory(
227
227
  agent_id: str,
228
228
  request: CreateArchivalMemory = Body(...),
229
229
  server: "SyncServer" = Depends(get_letta_server),
230
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
230
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
231
231
  ):
232
232
  """
233
233
  Insert a memory into an agent's archival memory store.
@@ -245,7 +245,7 @@ def delete_agent_archival_memory(
245
245
  memory_id: str,
246
246
  # memory_id: str = Query(..., description="Unique ID of the memory to be deleted."),
247
247
  server: "SyncServer" = Depends(get_letta_server),
248
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
248
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
249
249
  ):
250
250
  """
251
251
  Delete a memory from an agent's archival memory store.
@@ -276,7 +276,7 @@ def get_agent_messages(
276
276
  DEFAULT_MESSAGE_TOOL_KWARG,
277
277
  description="[Only applicable if use_assistant_message is True] The name of the message argument in the designated message tool.",
278
278
  ),
279
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
279
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
280
280
  ):
281
281
  """
282
282
  Retrieve message history for an agent.
@@ -315,7 +315,7 @@ async def send_message(
315
315
  agent_id: str,
316
316
  server: SyncServer = Depends(get_letta_server),
317
317
  request: LettaRequest = Body(...),
318
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
318
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
319
319
  ):
320
320
  """
321
321
  Process a user message and return the agent's response.
@@ -19,7 +19,7 @@ def list_blocks(
19
19
  templates_only: bool = Query(True, description="Whether to include only templates"),
20
20
  name: Optional[str] = Query(None, description="Name of the block"),
21
21
  server: SyncServer = Depends(get_letta_server),
22
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
22
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
23
23
  ):
24
24
  actor = server.get_user_or_default(user_id=user_id)
25
25
 
@@ -33,7 +33,7 @@ def list_blocks(
33
33
  def create_block(
34
34
  create_block: CreateBlock = Body(...),
35
35
  server: SyncServer = Depends(get_letta_server),
36
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
36
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
37
37
  ):
38
38
  actor = server.get_user_or_default(user_id=user_id)
39
39
 
@@ -13,7 +13,7 @@ router = APIRouter(prefix="/jobs", tags=["jobs"])
13
13
  def list_jobs(
14
14
  server: "SyncServer" = Depends(get_letta_server),
15
15
  source_id: Optional[str] = Query(None, description="Only list jobs associated with the source."),
16
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
16
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
17
17
  ):
18
18
  """
19
19
  List all jobs.
@@ -34,7 +34,7 @@ def list_jobs(
34
34
  @router.get("/active", response_model=List[Job], operation_id="list_active_jobs")
35
35
  def list_active_jobs(
36
36
  server: "SyncServer" = Depends(get_letta_server),
37
- user_id: str = Header(None), # Extract user_id from header, default to None if not present
37
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
38
38
  ):
39
39
  """
40
40
  List all active jobs.