letta-nightly 0.6.14.dev20250123104106__py3-none-any.whl → 0.6.15.dev20250124104035__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 (59) hide show
  1. letta/__init__.py +1 -1
  2. letta/client/client.py +144 -68
  3. letta/client/streaming.py +1 -1
  4. letta/functions/function_sets/extras.py +8 -3
  5. letta/functions/function_sets/multi_agent.py +1 -1
  6. letta/functions/helpers.py +2 -2
  7. letta/llm_api/llm_api_tools.py +2 -2
  8. letta/llm_api/openai.py +30 -138
  9. letta/memory.py +4 -4
  10. letta/offline_memory_agent.py +10 -10
  11. letta/orm/agent.py +10 -2
  12. letta/orm/block.py +14 -3
  13. letta/orm/job.py +2 -1
  14. letta/orm/message.py +12 -1
  15. letta/orm/passage.py +6 -2
  16. letta/orm/source.py +6 -1
  17. letta/orm/sqlalchemy_base.py +80 -32
  18. letta/orm/tool.py +5 -2
  19. letta/schemas/embedding_config_overrides.py +3 -0
  20. letta/schemas/enums.py +4 -0
  21. letta/schemas/job.py +1 -1
  22. letta/schemas/letta_message.py +22 -5
  23. letta/schemas/llm_config.py +5 -0
  24. letta/schemas/llm_config_overrides.py +38 -0
  25. letta/schemas/message.py +61 -15
  26. letta/schemas/openai/chat_completions.py +1 -1
  27. letta/schemas/passage.py +1 -1
  28. letta/schemas/providers.py +24 -8
  29. letta/schemas/source.py +1 -1
  30. letta/server/rest_api/app.py +12 -3
  31. letta/server/rest_api/interface.py +5 -7
  32. letta/server/rest_api/routers/v1/agents.py +7 -12
  33. letta/server/rest_api/routers/v1/blocks.py +19 -0
  34. letta/server/rest_api/routers/v1/organizations.py +2 -2
  35. letta/server/rest_api/routers/v1/providers.py +2 -2
  36. letta/server/rest_api/routers/v1/runs.py +15 -7
  37. letta/server/rest_api/routers/v1/sandbox_configs.py +4 -4
  38. letta/server/rest_api/routers/v1/sources.py +2 -2
  39. letta/server/rest_api/routers/v1/tags.py +2 -2
  40. letta/server/rest_api/routers/v1/tools.py +2 -2
  41. letta/server/rest_api/routers/v1/users.py +2 -2
  42. letta/server/server.py +62 -34
  43. letta/services/agent_manager.py +80 -33
  44. letta/services/block_manager.py +15 -2
  45. letta/services/helpers/agent_manager_helper.py +11 -4
  46. letta/services/job_manager.py +19 -9
  47. letta/services/message_manager.py +14 -8
  48. letta/services/organization_manager.py +8 -4
  49. letta/services/provider_manager.py +8 -4
  50. letta/services/sandbox_config_manager.py +16 -8
  51. letta/services/source_manager.py +4 -4
  52. letta/services/tool_manager.py +3 -3
  53. letta/services/user_manager.py +9 -5
  54. {letta_nightly-0.6.14.dev20250123104106.dist-info → letta_nightly-0.6.15.dev20250124104035.dist-info}/METADATA +2 -1
  55. {letta_nightly-0.6.14.dev20250123104106.dist-info → letta_nightly-0.6.15.dev20250124104035.dist-info}/RECORD +58 -57
  56. letta/orm/job_usage_statistics.py +0 -30
  57. {letta_nightly-0.6.14.dev20250123104106.dist-info → letta_nightly-0.6.15.dev20250124104035.dist-info}/LICENSE +0 -0
  58. {letta_nightly-0.6.14.dev20250123104106.dist-info → letta_nightly-0.6.15.dev20250124104035.dist-info}/WHEEL +0 -0
  59. {letta_nightly-0.6.14.dev20250123104106.dist-info → letta_nightly-0.6.15.dev20250124104035.dist-info}/entry_points.txt +0 -0
@@ -237,6 +237,7 @@ def create(
237
237
  data=dict(
238
238
  contents=[m.to_google_ai_dict() for m in messages],
239
239
  tools=tools,
240
+ generation_config={"temperature": llm_config.temperature},
240
241
  ),
241
242
  inner_thoughts_in_kwargs=llm_config.put_inner_thoughts_in_kwargs,
242
243
  )
@@ -261,6 +262,7 @@ def create(
261
262
  # user=str(user_id),
262
263
  # NOTE: max_tokens is required for Anthropic API
263
264
  max_tokens=1024, # TODO make dynamic
265
+ temperature=llm_config.temperature,
264
266
  ),
265
267
  )
266
268
 
@@ -290,7 +292,6 @@ def create(
290
292
  # # max_tokens=1024, # TODO make dynamic
291
293
  # ),
292
294
  # )
293
-
294
295
  elif llm_config.model_endpoint_type == "groq":
295
296
  if stream:
296
297
  raise NotImplementedError(f"Streaming not yet implemented for Groq.")
@@ -329,7 +330,6 @@ def create(
329
330
  try:
330
331
  # groq uses the openai chat completions API, so this component should be reusable
331
332
  response = openai_chat_completions_request(
332
- url=llm_config.model_endpoint,
333
333
  api_key=model_settings.groq_api_key,
334
334
  chat_completion_request=data,
335
335
  )
letta/llm_api/openai.py CHANGED
@@ -1,14 +1,9 @@
1
- import json
2
1
  import warnings
3
2
  from typing import Generator, List, Optional, Union
4
3
 
5
- import httpx
6
4
  import requests
7
- from httpx_sse import connect_sse
8
- from httpx_sse._exceptions import SSEError
5
+ from openai import OpenAI
9
6
 
10
- from letta.constants import OPENAI_CONTEXT_WINDOW_ERROR_SUBSTRING
11
- from letta.errors import LLMError
12
7
  from letta.llm_api.helpers import add_inner_thoughts_to_functions, convert_to_structured_output, make_post_request
13
8
  from letta.local_llm.constants import INNER_THOUGHTS_KWARG, INNER_THOUGHTS_KWARG_DESCRIPTION
14
9
  from letta.local_llm.utils import num_tokens_from_functions, num_tokens_from_messages
@@ -130,7 +125,8 @@ def build_openai_chat_completions_request(
130
125
  tools=[Tool(type="function", function=f) for f in functions] if functions else None,
131
126
  tool_choice=tool_choice,
132
127
  user=str(user_id),
133
- max_tokens=max_tokens,
128
+ max_completion_tokens=max_tokens,
129
+ temperature=llm_config.temperature,
134
130
  )
135
131
  else:
136
132
  data = ChatCompletionRequest(
@@ -139,7 +135,8 @@ def build_openai_chat_completions_request(
139
135
  functions=functions,
140
136
  function_call=function_call,
141
137
  user=str(user_id),
142
- max_tokens=max_tokens,
138
+ max_completion_tokens=max_tokens,
139
+ temperature=llm_config.temperature,
143
140
  )
144
141
  # https://platform.openai.com/docs/guides/text-generation/json-mode
145
142
  # only supported by gpt-4o, gpt-4-turbo, or gpt-3.5-turbo
@@ -378,126 +375,21 @@ def openai_chat_completions_process_stream(
378
375
  return chat_completion_response
379
376
 
380
377
 
381
- def _sse_post(url: str, data: dict, headers: dict) -> Generator[ChatCompletionChunkResponse, None, None]:
382
-
383
- with httpx.Client() as client:
384
- with connect_sse(client, method="POST", url=url, json=data, headers=headers) as event_source:
385
-
386
- # Inspect for errors before iterating (see https://github.com/florimondmanca/httpx-sse/pull/12)
387
- if not event_source.response.is_success:
388
- # handle errors
389
- from letta.utils import printd
390
-
391
- printd("Caught error before iterating SSE request:", vars(event_source.response))
392
- printd(event_source.response.read())
393
-
394
- try:
395
- response_bytes = event_source.response.read()
396
- response_dict = json.loads(response_bytes.decode("utf-8"))
397
- error_message = response_dict["error"]["message"]
398
- # e.g.: This model's maximum context length is 8192 tokens. However, your messages resulted in 8198 tokens (7450 in the messages, 748 in the functions). Please reduce the length of the messages or functions.
399
- if OPENAI_CONTEXT_WINDOW_ERROR_SUBSTRING in error_message:
400
- raise LLMError(error_message)
401
- except LLMError:
402
- raise
403
- except:
404
- print(f"Failed to parse SSE message, throwing SSE HTTP error up the stack")
405
- event_source.response.raise_for_status()
406
-
407
- try:
408
- for sse in event_source.iter_sse():
409
- # printd(sse.event, sse.data, sse.id, sse.retry)
410
- if sse.data == OPENAI_SSE_DONE:
411
- # print("finished")
412
- break
413
- else:
414
- chunk_data = json.loads(sse.data)
415
- # print("chunk_data::", chunk_data)
416
- chunk_object = ChatCompletionChunkResponse(**chunk_data)
417
- # print("chunk_object::", chunk_object)
418
- # id=chunk_data["id"],
419
- # choices=[ChunkChoice],
420
- # model=chunk_data["model"],
421
- # system_fingerprint=chunk_data["system_fingerprint"]
422
- # )
423
- yield chunk_object
424
-
425
- except SSEError as e:
426
- print("Caught an error while iterating the SSE stream:", str(e))
427
- if "application/json" in str(e): # Check if the error is because of JSON response
428
- # TODO figure out a better way to catch the error other than re-trying with a POST
429
- response = client.post(url=url, json=data, headers=headers) # Make the request again to get the JSON response
430
- if response.headers["Content-Type"].startswith("application/json"):
431
- error_details = response.json() # Parse the JSON to get the error message
432
- print("Request:", vars(response.request))
433
- print("POST Error:", error_details)
434
- print("Original SSE Error:", str(e))
435
- else:
436
- print("Failed to retrieve JSON error message via retry.")
437
- else:
438
- print("SSEError not related to 'application/json' content type.")
439
-
440
- # Optionally re-raise the exception if you need to propagate it
441
- raise e
442
-
443
- except Exception as e:
444
- if event_source.response.request is not None:
445
- print("HTTP Request:", vars(event_source.response.request))
446
- if event_source.response is not None:
447
- print("HTTP Status:", event_source.response.status_code)
448
- print("HTTP Headers:", event_source.response.headers)
449
- # print("HTTP Body:", event_source.response.text)
450
- print("Exception message:", str(e))
451
- raise e
452
-
453
-
454
378
  def openai_chat_completions_request_stream(
455
379
  url: str,
456
380
  api_key: str,
457
381
  chat_completion_request: ChatCompletionRequest,
458
382
  ) -> Generator[ChatCompletionChunkResponse, None, None]:
459
- from letta.utils import printd
460
-
461
- url = smart_urljoin(url, "chat/completions")
462
- headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
463
- data = chat_completion_request.model_dump(exclude_none=True)
464
-
465
- printd("Request:\n", json.dumps(data, indent=2))
466
-
467
- # If functions == None, strip from the payload
468
- if "functions" in data and data["functions"] is None:
469
- data.pop("functions")
470
- data.pop("function_call", None) # extra safe, should exist always (default="auto")
471
-
472
- if "tools" in data and data["tools"] is None:
473
- data.pop("tools")
474
- data.pop("tool_choice", None) # extra safe, should exist always (default="auto")
475
-
476
- if "tools" in data:
477
- for tool in data["tools"]:
478
- # tool["strict"] = True
479
- try:
480
- tool["function"] = convert_to_structured_output(tool["function"])
481
- except ValueError as e:
482
- warnings.warn(f"Failed to convert tool function to structured output, tool={tool}, error={e}")
483
-
484
- # print(f"\n\n\n\nData[tools]: {json.dumps(data['tools'], indent=2)}")
485
-
486
- printd(f"Sending request to {url}")
487
- try:
488
- return _sse_post(url=url, data=data, headers=headers)
489
- except requests.exceptions.HTTPError as http_err:
490
- # Handle HTTP errors (e.g., response 4XX, 5XX)
491
- printd(f"Got HTTPError, exception={http_err}, payload={data}")
492
- raise http_err
493
- except requests.exceptions.RequestException as req_err:
494
- # Handle other requests-related errors (e.g., connection error)
495
- printd(f"Got RequestException, exception={req_err}")
496
- raise req_err
497
- except Exception as e:
498
- # Handle other potential errors
499
- printd(f"Got unknown Exception, exception={e}")
500
- raise e
383
+ data = prepare_openai_payload(chat_completion_request)
384
+ data["stream"] = True
385
+ client = OpenAI(
386
+ api_key=api_key,
387
+ base_url=url,
388
+ )
389
+ stream = client.chat.completions.create(**data)
390
+ for chunk in stream:
391
+ # TODO: Use the native OpenAI objects here?
392
+ yield ChatCompletionChunkResponse(**chunk.model_dump(exclude_none=True))
501
393
 
502
394
 
503
395
  def openai_chat_completions_request(
@@ -512,18 +404,28 @@ def openai_chat_completions_request(
512
404
 
513
405
  https://platform.openai.com/docs/guides/text-generation?lang=curl
514
406
  """
515
- from letta.utils import printd
407
+ data = prepare_openai_payload(chat_completion_request)
408
+ client = OpenAI(api_key=api_key, base_url=url)
409
+ chat_completion = client.chat.completions.create(**data)
410
+ return ChatCompletionResponse(**chat_completion.model_dump())
411
+
412
+
413
+ def openai_embeddings_request(url: str, api_key: str, data: dict) -> EmbeddingResponse:
414
+ """https://platform.openai.com/docs/api-reference/embeddings/create"""
516
415
 
517
- url = smart_urljoin(url, "chat/completions")
416
+ url = smart_urljoin(url, "embeddings")
518
417
  headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
418
+ response_json = make_post_request(url, headers, data)
419
+ return EmbeddingResponse(**response_json)
420
+
421
+
422
+ def prepare_openai_payload(chat_completion_request: ChatCompletionRequest):
519
423
  data = chat_completion_request.model_dump(exclude_none=True)
520
424
 
521
425
  # add check otherwise will cause error: "Invalid value for 'parallel_tool_calls': 'parallel_tool_calls' is only allowed when 'tools' are specified."
522
426
  if chat_completion_request.tools is not None:
523
427
  data["parallel_tool_calls"] = False
524
428
 
525
- printd("Request:\n", json.dumps(data, indent=2))
526
-
527
429
  # If functions == None, strip from the payload
528
430
  if "functions" in data and data["functions"] is None:
529
431
  data.pop("functions")
@@ -540,14 +442,4 @@ def openai_chat_completions_request(
540
442
  except ValueError as e:
541
443
  warnings.warn(f"Failed to convert tool function to structured output, tool={tool}, error={e}")
542
444
 
543
- response_json = make_post_request(url, headers, data)
544
- return ChatCompletionResponse(**response_json)
545
-
546
-
547
- def openai_embeddings_request(url: str, api_key: str, data: dict) -> EmbeddingResponse:
548
- """https://platform.openai.com/docs/api-reference/embeddings/create"""
549
-
550
- url = smart_urljoin(url, "embeddings")
551
- headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
552
- response_json = make_post_request(url, headers, data)
553
- return EmbeddingResponse(**response_json)
445
+ return data
letta/memory.py CHANGED
@@ -6,7 +6,7 @@ from letta.prompts.gpt_summarize import SYSTEM as SUMMARY_PROMPT_SYSTEM
6
6
  from letta.schemas.agent import AgentState
7
7
  from letta.schemas.enums import MessageRole
8
8
  from letta.schemas.memory import Memory
9
- from letta.schemas.message import Message
9
+ from letta.schemas.message import Message, TextContent
10
10
  from letta.settings import summarizer_settings
11
11
  from letta.utils import count_tokens, printd
12
12
 
@@ -60,9 +60,9 @@ def summarize_messages(
60
60
 
61
61
  dummy_agent_id = agent_state.id
62
62
  message_sequence = [
63
- Message(agent_id=dummy_agent_id, role=MessageRole.system, text=summary_prompt),
64
- Message(agent_id=dummy_agent_id, role=MessageRole.assistant, text=MESSAGE_SUMMARY_REQUEST_ACK),
65
- Message(agent_id=dummy_agent_id, role=MessageRole.user, text=summary_input),
63
+ Message(agent_id=dummy_agent_id, role=MessageRole.system, content=[TextContent(text=summary_prompt)]),
64
+ Message(agent_id=dummy_agent_id, role=MessageRole.assistant, content=[TextContent(text=MESSAGE_SUMMARY_REQUEST_ACK)]),
65
+ Message(agent_id=dummy_agent_id, role=MessageRole.user, content=[TextContent(text=summary_input)]),
66
66
  ]
67
67
 
68
68
  # TODO: We need to eventually have a separate LLM config for the summarizer LLM
@@ -8,12 +8,12 @@ from letta.schemas.openai.chat_completion_response import UsageStatistics
8
8
  from letta.schemas.usage import LettaUsageStatistics
9
9
 
10
10
 
11
- def trigger_rethink_memory(agent_state: "AgentState", message: Optional[str]) -> Optional[str]: # type: ignore
11
+ def trigger_rethink_memory(agent_state: "AgentState", message: str) -> None: # type: ignore
12
12
  """
13
13
  Called if and only when user says the word trigger_rethink_memory". It will trigger the re-evaluation of the memory.
14
14
 
15
15
  Args:
16
- message (Optional[str]): Description of what aspect of the memory should be re-evaluated.
16
+ message (str): Description of what aspect of the memory should be re-evaluated.
17
17
 
18
18
  """
19
19
  from letta import create_client
@@ -25,12 +25,12 @@ def trigger_rethink_memory(agent_state: "AgentState", message: Optional[str]) ->
25
25
  client.user_message(agent_id=agent.id, message=message)
26
26
 
27
27
 
28
- def trigger_rethink_memory_convo(agent_state: "AgentState", message: Optional[str]) -> Optional[str]: # type: ignore
28
+ def trigger_rethink_memory_convo(agent_state: "AgentState", message: str) -> None: # type: ignore
29
29
  """
30
30
  Called if and only when user says the word "trigger_rethink_memory". It will trigger the re-evaluation of the memory.
31
31
 
32
32
  Args:
33
- message (Optional[str]): Description of what aspect of the memory should be re-evaluated.
33
+ message (str): Description of what aspect of the memory should be re-evaluated.
34
34
 
35
35
  """
36
36
  from letta import create_client
@@ -48,7 +48,7 @@ def trigger_rethink_memory_convo(agent_state: "AgentState", message: Optional[st
48
48
  client.user_message(agent_id=agent.id, message=message)
49
49
 
50
50
 
51
- def rethink_memory_convo(agent_state: "AgentState", new_memory: str, target_block_label: Optional[str], source_block_label: Optional[str]) -> Optional[str]: # type: ignore
51
+ def rethink_memory_convo(agent_state: "AgentState", new_memory: str, target_block_label: str, source_block_label: str) -> None: # type: ignore
52
52
  """
53
53
  Re-evaluate the memory in block_name, integrating new and updated facts. Replace outdated information with the most likely truths, avoiding redundancy with original memories. Ensure consistency with other memory blocks.
54
54
 
@@ -58,7 +58,7 @@ def rethink_memory_convo(agent_state: "AgentState", new_memory: str, target_bloc
58
58
  target_block_label (str): The name of the block to write to. This should be chat_agent_human_new or chat_agent_persona_new.
59
59
 
60
60
  Returns:
61
- Optional[str]: None is always returned as this function does not produce a response.
61
+ None: None is always returned as this function does not produce a response.
62
62
  """
63
63
  if target_block_label is not None:
64
64
  if agent_state.memory.get_block(target_block_label) is None:
@@ -67,7 +67,7 @@ def rethink_memory_convo(agent_state: "AgentState", new_memory: str, target_bloc
67
67
  return None
68
68
 
69
69
 
70
- def rethink_memory(agent_state: "AgentState", new_memory: str, target_block_label: Optional[str], source_block_label: Optional[str]) -> Optional[str]: # type: ignore
70
+ def rethink_memory(agent_state: "AgentState", new_memory: str, target_block_label: str, source_block_label: str) -> None: # type: ignore
71
71
  """
72
72
  Re-evaluate the memory in block_name, integrating new and updated facts.
73
73
  Replace outdated information with the most likely truths, avoiding redundancy with original memories.
@@ -78,7 +78,7 @@ def rethink_memory(agent_state: "AgentState", new_memory: str, target_block_labe
78
78
  source_block_label (str): The name of the block to integrate information from. None if all the information has been integrated to terminate the loop.
79
79
  target_block_label (str): The name of the block to write to.
80
80
  Returns:
81
- Optional[str]: None is always returned as this function does not produce a response.
81
+ None: None is always returned as this function does not produce a response.
82
82
  """
83
83
 
84
84
  if target_block_label is not None:
@@ -88,7 +88,7 @@ def rethink_memory(agent_state: "AgentState", new_memory: str, target_block_labe
88
88
  return None
89
89
 
90
90
 
91
- def finish_rethinking_memory(agent_state: "AgentState") -> Optional[str]: # type: ignore
91
+ def finish_rethinking_memory(agent_state: "AgentState") -> None: # type: ignore
92
92
  """
93
93
  This function is called when the agent is done rethinking the memory.
94
94
 
@@ -98,7 +98,7 @@ def finish_rethinking_memory(agent_state: "AgentState") -> Optional[str]: # typ
98
98
  return None
99
99
 
100
100
 
101
- def finish_rethinking_memory_convo(agent_state: "AgentState") -> Optional[str]: # type: ignore
101
+ def finish_rethinking_memory_convo(agent_state: "AgentState") -> None: # type: ignore
102
102
  """
103
103
  This function is called when the agent is done rethinking the memory.
104
104
 
letta/orm/agent.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import uuid
2
2
  from typing import TYPE_CHECKING, List, Optional
3
3
 
4
- from sqlalchemy import JSON, String
4
+ from sqlalchemy import JSON, Index, String
5
5
  from sqlalchemy.orm import Mapped, mapped_column, relationship
6
6
 
7
7
  from letta.orm.block import Block
@@ -27,6 +27,7 @@ if TYPE_CHECKING:
27
27
  class Agent(SqlalchemyBase, OrganizationMixin):
28
28
  __tablename__ = "agents"
29
29
  __pydantic_model__ = PydanticAgentState
30
+ __table_args__ = (Index("ix_agents_created_at", "created_at", "id"),)
30
31
 
31
32
  # agent generates its own id
32
33
  # TODO: We want to migrate all the ORM models to do this, so we will need to move this to the SqlalchemyBase
@@ -69,7 +70,14 @@ class Agent(SqlalchemyBase, OrganizationMixin):
69
70
  )
70
71
  tools: Mapped[List["Tool"]] = relationship("Tool", secondary="tools_agents", lazy="selectin", passive_deletes=True)
71
72
  sources: Mapped[List["Source"]] = relationship("Source", secondary="sources_agents", lazy="selectin")
72
- core_memory: Mapped[List["Block"]] = relationship("Block", secondary="blocks_agents", lazy="selectin")
73
+ core_memory: Mapped[List["Block"]] = relationship(
74
+ "Block",
75
+ secondary="blocks_agents",
76
+ lazy="selectin",
77
+ passive_deletes=True, # Ensures SQLAlchemy doesn't fetch blocks_agents rows before deleting
78
+ back_populates="agents",
79
+ doc="Blocks forming the core memory of the agent.",
80
+ )
73
81
  messages: Mapped[List["Message"]] = relationship(
74
82
  "Message",
75
83
  back_populates="agent",
letta/orm/block.py CHANGED
@@ -1,6 +1,6 @@
1
- from typing import TYPE_CHECKING, Optional, Type
1
+ from typing import TYPE_CHECKING, List, Optional, Type
2
2
 
3
- from sqlalchemy import JSON, BigInteger, Integer, UniqueConstraint, event
3
+ from sqlalchemy import JSON, BigInteger, Index, Integer, UniqueConstraint, event
4
4
  from sqlalchemy.orm import Mapped, attributes, mapped_column, relationship
5
5
 
6
6
  from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT
@@ -20,7 +20,10 @@ class Block(OrganizationMixin, SqlalchemyBase):
20
20
  __tablename__ = "block"
21
21
  __pydantic_model__ = PydanticBlock
22
22
  # This may seem redundant, but is necessary for the BlocksAgents composite FK relationship
23
- __table_args__ = (UniqueConstraint("id", "label", name="unique_block_id_label"),)
23
+ __table_args__ = (
24
+ UniqueConstraint("id", "label", name="unique_block_id_label"),
25
+ Index("created_at_label_idx", "created_at", "label"),
26
+ )
24
27
 
25
28
  template_name: Mapped[Optional[str]] = mapped_column(
26
29
  nullable=True, doc="the unique name that identifies a block in a human-readable way"
@@ -36,6 +39,14 @@ class Block(OrganizationMixin, SqlalchemyBase):
36
39
 
37
40
  # relationships
38
41
  organization: Mapped[Optional["Organization"]] = relationship("Organization")
42
+ agents: Mapped[List["Agent"]] = relationship(
43
+ "Agent",
44
+ secondary="blocks_agents",
45
+ lazy="selectin",
46
+ passive_deletes=True, # Ensures SQLAlchemy doesn't fetch blocks_agents rows before deleting
47
+ back_populates="core_memory",
48
+ doc="Agents associated with this block.",
49
+ )
39
50
 
40
51
  def to_pydantic(self) -> Type:
41
52
  match self.label:
letta/orm/job.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from datetime import datetime
2
2
  from typing import TYPE_CHECKING, List, Optional
3
3
 
4
- from sqlalchemy import JSON, String
4
+ from sqlalchemy import JSON, Index, String
5
5
  from sqlalchemy.orm import Mapped, mapped_column, relationship
6
6
 
7
7
  from letta.orm.enums import JobType
@@ -25,6 +25,7 @@ class Job(SqlalchemyBase, UserMixin):
25
25
 
26
26
  __tablename__ = "jobs"
27
27
  __pydantic_model__ = PydanticJob
28
+ __table_args__ = (Index("ix_jobs_created_at", "created_at", "id"),)
28
29
 
29
30
  status: Mapped[JobStatus] = mapped_column(String, default=JobStatus.created, doc="The current status of the job.")
30
31
  completed_at: Mapped[Optional[datetime]] = mapped_column(nullable=True, doc="The unix timestamp of when the job was completed.")
letta/orm/message.py CHANGED
@@ -8,13 +8,17 @@ from letta.orm.custom_columns import ToolCallColumn
8
8
  from letta.orm.mixins import AgentMixin, OrganizationMixin
9
9
  from letta.orm.sqlalchemy_base import SqlalchemyBase
10
10
  from letta.schemas.message import Message as PydanticMessage
11
+ from letta.schemas.message import TextContent as PydanticTextContent
11
12
 
12
13
 
13
14
  class Message(SqlalchemyBase, OrganizationMixin, AgentMixin):
14
15
  """Defines data model for storing Message objects"""
15
16
 
16
17
  __tablename__ = "messages"
17
- __table_args__ = (Index("ix_messages_agent_created_at", "agent_id", "created_at"),)
18
+ __table_args__ = (
19
+ Index("ix_messages_agent_created_at", "agent_id", "created_at"),
20
+ Index("ix_messages_created_at", "created_at", "id"),
21
+ )
18
22
  __pydantic_model__ = PydanticMessage
19
23
 
20
24
  id: Mapped[str] = mapped_column(primary_key=True, doc="Unique message identifier")
@@ -42,3 +46,10 @@ class Message(SqlalchemyBase, OrganizationMixin, AgentMixin):
42
46
  def job(self) -> Optional["Job"]:
43
47
  """Get the job associated with this message, if any."""
44
48
  return self.job_message.job if self.job_message else None
49
+
50
+ def to_pydantic(self) -> PydanticMessage:
51
+ """custom pydantic conversion for message content mapping"""
52
+ model = self.__pydantic_model__.model_validate(self)
53
+ if self.text:
54
+ model.content = [PydanticTextContent(text=self.text)]
55
+ return model
letta/orm/passage.py CHANGED
@@ -45,8 +45,12 @@ class BasePassage(SqlalchemyBase, OrganizationMixin):
45
45
  @declared_attr
46
46
  def __table_args__(cls):
47
47
  if settings.letta_pg_uri_no_default:
48
- return (Index(f"{cls.__tablename__}_org_idx", "organization_id"), {"extend_existing": True})
49
- return ({"extend_existing": True},)
48
+ return (
49
+ Index(f"{cls.__tablename__}_org_idx", "organization_id"),
50
+ Index(f"{cls.__tablename__}_created_at_id_idx", "created_at", "id"),
51
+ {"extend_existing": True},
52
+ )
53
+ return (Index(f"{cls.__tablename__}_created_at_id_idx", "created_at", "id"), {"extend_existing": True})
50
54
 
51
55
 
52
56
  class SourcePassage(BasePassage, FileMixin, SourceMixin):
letta/orm/source.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from typing import TYPE_CHECKING, List, Optional
2
2
 
3
- from sqlalchemy import JSON
3
+ from sqlalchemy import JSON, Index
4
4
  from sqlalchemy.orm import Mapped, mapped_column, relationship
5
5
 
6
6
  from letta.orm import FileMetadata
@@ -23,6 +23,11 @@ class Source(SqlalchemyBase, OrganizationMixin):
23
23
  __tablename__ = "sources"
24
24
  __pydantic_model__ = PydanticSource
25
25
 
26
+ __table_args__ = (
27
+ Index(f"source_created_at_id_idx", "created_at", "id"),
28
+ {"extend_existing": True},
29
+ )
30
+
26
31
  name: Mapped[str] = mapped_column(doc="the name of the source, must be unique within the org", nullable=False)
27
32
  description: Mapped[str] = mapped_column(nullable=True, doc="a human-readable description of the source")
28
33
  embedding_config: Mapped[EmbeddingConfig] = mapped_column(EmbeddingConfigColumn, doc="Configuration settings for embedding.")