letta-nightly 0.8.8.dev20250703104323__py3-none-any.whl → 0.8.9.dev20250703191231__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 (69) hide show
  1. letta/__init__.py +6 -1
  2. letta/agent.py +1 -0
  3. letta/agents/base_agent.py +8 -2
  4. letta/agents/ephemeral_summary_agent.py +33 -33
  5. letta/agents/letta_agent.py +104 -53
  6. letta/agents/voice_agent.py +2 -1
  7. letta/constants.py +8 -4
  8. letta/functions/function_sets/files.py +22 -7
  9. letta/functions/function_sets/multi_agent.py +34 -0
  10. letta/functions/types.py +1 -1
  11. letta/groups/helpers.py +8 -5
  12. letta/groups/sleeptime_multi_agent_v2.py +20 -15
  13. letta/interface.py +1 -1
  14. letta/interfaces/anthropic_streaming_interface.py +15 -8
  15. letta/interfaces/openai_chat_completions_streaming_interface.py +9 -6
  16. letta/interfaces/openai_streaming_interface.py +17 -11
  17. letta/llm_api/openai_client.py +2 -1
  18. letta/orm/agent.py +1 -0
  19. letta/orm/file.py +8 -2
  20. letta/orm/files_agents.py +36 -11
  21. letta/orm/mcp_server.py +3 -0
  22. letta/orm/source.py +2 -1
  23. letta/orm/step.py +3 -0
  24. letta/prompts/system/memgpt_v2_chat.txt +5 -8
  25. letta/schemas/agent.py +58 -23
  26. letta/schemas/embedding_config.py +3 -2
  27. letta/schemas/enums.py +4 -0
  28. letta/schemas/file.py +1 -0
  29. letta/schemas/letta_stop_reason.py +18 -0
  30. letta/schemas/mcp.py +15 -10
  31. letta/schemas/memory.py +35 -5
  32. letta/schemas/providers.py +11 -0
  33. letta/schemas/step.py +1 -0
  34. letta/schemas/tool.py +2 -1
  35. letta/server/rest_api/routers/v1/agents.py +320 -184
  36. letta/server/rest_api/routers/v1/groups.py +6 -2
  37. letta/server/rest_api/routers/v1/identities.py +6 -2
  38. letta/server/rest_api/routers/v1/jobs.py +49 -1
  39. letta/server/rest_api/routers/v1/sources.py +28 -19
  40. letta/server/rest_api/routers/v1/steps.py +7 -2
  41. letta/server/rest_api/routers/v1/tools.py +40 -9
  42. letta/server/rest_api/streaming_response.py +88 -0
  43. letta/server/server.py +61 -55
  44. letta/services/agent_manager.py +28 -16
  45. letta/services/file_manager.py +58 -9
  46. letta/services/file_processor/chunker/llama_index_chunker.py +2 -0
  47. letta/services/file_processor/embedder/openai_embedder.py +54 -10
  48. letta/services/file_processor/file_processor.py +59 -0
  49. letta/services/file_processor/parser/mistral_parser.py +2 -0
  50. letta/services/files_agents_manager.py +120 -2
  51. letta/services/helpers/agent_manager_helper.py +21 -4
  52. letta/services/job_manager.py +57 -6
  53. letta/services/mcp/base_client.py +1 -0
  54. letta/services/mcp_manager.py +13 -1
  55. letta/services/step_manager.py +14 -5
  56. letta/services/summarizer/summarizer.py +6 -22
  57. letta/services/tool_executor/builtin_tool_executor.py +0 -1
  58. letta/services/tool_executor/files_tool_executor.py +2 -2
  59. letta/services/tool_executor/multi_agent_tool_executor.py +23 -0
  60. letta/services/tool_manager.py +7 -7
  61. letta/settings.py +11 -2
  62. letta/templates/summary_request_text.j2 +19 -0
  63. letta/utils.py +95 -14
  64. {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/METADATA +2 -2
  65. {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/RECORD +69 -68
  66. /letta/{agents/prompts → prompts/system}/summary_system_prompt.txt +0 -0
  67. {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/LICENSE +0 -0
  68. {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/WHEEL +0 -0
  69. {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import json
3
+ import os
3
4
  from concurrent.futures import ThreadPoolExecutor, as_completed
4
5
  from typing import TYPE_CHECKING, List
5
6
 
@@ -7,6 +8,7 @@ from letta.functions.helpers import (
7
8
  _send_message_to_all_agents_in_group_async,
8
9
  execute_send_message_to_agent,
9
10
  extract_send_message_from_steps_messages,
11
+ fire_and_forget_send_to_agent,
10
12
  )
11
13
  from letta.schemas.enums import MessageRole
12
14
  from letta.schemas.message import MessageCreate
@@ -125,3 +127,35 @@ def send_message_to_all_agents_in_group(self: "Agent", message: str) -> List[str
125
127
  """
126
128
 
127
129
  return asyncio.run(_send_message_to_all_agents_in_group_async(self, message))
130
+
131
+
132
+ def send_message_to_agent_async(self: "Agent", message: str, other_agent_id: str) -> str:
133
+ """
134
+ Sends a message to a specific Letta agent within the same organization. The sender's identity is automatically included, so no explicit introduction is required in the message. This function does not expect a response from the target agent, making it suitable for notifications or one-way communication.
135
+ Args:
136
+ message (str): The content of the message to be sent to the target agent.
137
+ other_agent_id (str): The unique identifier of the target Letta agent.
138
+ Returns:
139
+ str: A confirmation message indicating the message was successfully sent.
140
+ """
141
+ if os.getenv("LETTA_ENVIRONMENT") == "PRODUCTION":
142
+ raise RuntimeError("This tool is not allowed to be run on Letta Cloud.")
143
+
144
+ message = (
145
+ f"[Incoming message from agent with ID '{self.agent_state.id}' - to reply to this message, "
146
+ f"make sure to use the 'send_message_to_agent_async' tool, or the agent will not receive your message] "
147
+ f"{message}"
148
+ )
149
+ messages = [MessageCreate(role=MessageRole.system, content=message, name=self.agent_state.name)]
150
+
151
+ # Do the actual fire-and-forget
152
+ fire_and_forget_send_to_agent(
153
+ sender_agent=self,
154
+ messages=messages,
155
+ other_agent_id=other_agent_id,
156
+ log_prefix="[send_message_to_agent_async]",
157
+ use_retries=False, # or True if you want to use _async_send_message_with_retries
158
+ )
159
+
160
+ # Immediately return to caller
161
+ return "Successfully sent message"
letta/functions/types.py CHANGED
@@ -14,5 +14,5 @@ class FileOpenRequest(BaseModel):
14
14
  default=None, description="Optional starting line number (1-indexed). If not specified, starts from beginning of file."
15
15
  )
16
16
  length: Optional[int] = Field(
17
- default=None, description="Optional number of lines to view from offset. If not specified, views to end of file."
17
+ default=None, description="Optional number of lines to view from offset (inclusive). If not specified, views to end of file."
18
18
  )
letta/groups/helpers.py CHANGED
@@ -7,6 +7,7 @@ from letta.orm.group import Group
7
7
  from letta.orm.user import User
8
8
  from letta.schemas.agent import AgentState
9
9
  from letta.schemas.group import ManagerType
10
+ from letta.schemas.letta_message_content import ImageContent, TextContent
10
11
  from letta.schemas.message import Message
11
12
  from letta.services.mcp.base_client import AsyncBaseMCPClient
12
13
 
@@ -89,11 +90,13 @@ def stringify_message(message: Message, use_assistant_name: bool = False) -> str
89
90
  assistant_name = message.name or "assistant" if use_assistant_name else "assistant"
90
91
  if message.role == "user":
91
92
  try:
92
- content = json.loads(message.content[0].text)
93
- if content["type"] == "user_message":
94
- return f"{message.name or 'user'}: {content['message']}"
95
- else:
96
- return None
93
+ messages = []
94
+ for content in message.content:
95
+ if isinstance(content, TextContent):
96
+ messages.append(f"{message.name or 'user'}: {content.text}")
97
+ elif isinstance(content, ImageContent):
98
+ messages.append(f"{message.name or 'user'}: [Image Here]")
99
+ return "\n".join(messages)
97
100
  except:
98
101
  return f"{message.name or 'user'}: {message.content[0].text}"
99
102
  elif message.role == "assistant":
@@ -1,6 +1,6 @@
1
1
  import asyncio
2
+ from collections.abc import AsyncGenerator
2
3
  from datetime import datetime, timezone
3
- from typing import AsyncGenerator, List, Optional
4
4
 
5
5
  from letta.agents.base_agent import BaseAgent
6
6
  from letta.agents.letta_agent import LettaAgent
@@ -39,7 +39,8 @@ class SleeptimeMultiAgentV2(BaseAgent):
39
39
  actor: User,
40
40
  step_manager: StepManager = NoopStepManager(),
41
41
  telemetry_manager: TelemetryManager = NoopTelemetryManager(),
42
- group: Optional[Group] = None,
42
+ group: Group | None = None,
43
+ current_run_id: str | None = None,
43
44
  ):
44
45
  super().__init__(
45
46
  agent_id=agent_id,
@@ -54,6 +55,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
54
55
  self.job_manager = job_manager
55
56
  self.step_manager = step_manager
56
57
  self.telemetry_manager = telemetry_manager
58
+ self.current_run_id = current_run_id
57
59
  # Group settings
58
60
  assert group.manager_type == ManagerType.sleeptime, f"Expected group manager type to be 'sleeptime', got {group.manager_type}"
59
61
  self.group = group
@@ -61,12 +63,12 @@ class SleeptimeMultiAgentV2(BaseAgent):
61
63
  @trace_method
62
64
  async def step(
63
65
  self,
64
- input_messages: List[MessageCreate],
66
+ input_messages: list[MessageCreate],
65
67
  max_steps: int = DEFAULT_MAX_STEPS,
66
- run_id: Optional[str] = None,
68
+ run_id: str | None = None,
67
69
  use_assistant_message: bool = True,
68
- request_start_timestamp_ns: Optional[int] = None,
69
- include_return_message_types: Optional[List[MessageType]] = None,
70
+ request_start_timestamp_ns: int | None = None,
71
+ include_return_message_types: list[MessageType] | None = None,
70
72
  ) -> LettaResponse:
71
73
  run_ids = []
72
74
 
@@ -89,6 +91,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
89
91
  actor=self.actor,
90
92
  step_manager=self.step_manager,
91
93
  telemetry_manager=self.telemetry_manager,
94
+ current_run_id=self.current_run_id,
92
95
  )
93
96
  # Perform foreground agent step
94
97
  response = await foreground_agent.step(
@@ -125,7 +128,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
125
128
 
126
129
  except Exception as e:
127
130
  # Individual task failures
128
- print(f"Agent processing failed: {str(e)}")
131
+ print(f"Agent processing failed: {e!s}")
129
132
  raise e
130
133
 
131
134
  response.usage.run_ids = run_ids
@@ -134,11 +137,11 @@ class SleeptimeMultiAgentV2(BaseAgent):
134
137
  @trace_method
135
138
  async def step_stream_no_tokens(
136
139
  self,
137
- input_messages: List[MessageCreate],
140
+ input_messages: list[MessageCreate],
138
141
  max_steps: int = DEFAULT_MAX_STEPS,
139
142
  use_assistant_message: bool = True,
140
- request_start_timestamp_ns: Optional[int] = None,
141
- include_return_message_types: Optional[List[MessageType]] = None,
143
+ request_start_timestamp_ns: int | None = None,
144
+ include_return_message_types: list[MessageType] | None = None,
142
145
  ):
143
146
  response = await self.step(
144
147
  input_messages=input_messages,
@@ -157,11 +160,11 @@ class SleeptimeMultiAgentV2(BaseAgent):
157
160
  @trace_method
158
161
  async def step_stream(
159
162
  self,
160
- input_messages: List[MessageCreate],
163
+ input_messages: list[MessageCreate],
161
164
  max_steps: int = DEFAULT_MAX_STEPS,
162
165
  use_assistant_message: bool = True,
163
- request_start_timestamp_ns: Optional[int] = None,
164
- include_return_message_types: Optional[List[MessageType]] = None,
166
+ request_start_timestamp_ns: int | None = None,
167
+ include_return_message_types: list[MessageType] | None = None,
165
168
  ) -> AsyncGenerator[str, None]:
166
169
  # Prepare new messages
167
170
  new_messages = []
@@ -182,6 +185,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
182
185
  actor=self.actor,
183
186
  step_manager=self.step_manager,
184
187
  telemetry_manager=self.telemetry_manager,
188
+ current_run_id=self.current_run_id,
185
189
  )
186
190
  # Perform foreground agent step
187
191
  async for chunk in foreground_agent.step_stream(
@@ -218,7 +222,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
218
222
  async def _issue_background_task(
219
223
  self,
220
224
  sleeptime_agent_id: str,
221
- response_messages: List[Message],
225
+ response_messages: list[Message],
222
226
  last_processed_message_id: str,
223
227
  use_assistant_message: bool = True,
224
228
  ) -> str:
@@ -248,7 +252,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
248
252
  self,
249
253
  foreground_agent_id: str,
250
254
  sleeptime_agent_id: str,
251
- response_messages: List[Message],
255
+ response_messages: list[Message],
252
256
  last_processed_message_id: str,
253
257
  run_id: str,
254
258
  use_assistant_message: bool = True,
@@ -296,6 +300,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
296
300
  actor=self.actor,
297
301
  step_manager=self.step_manager,
298
302
  telemetry_manager=self.telemetry_manager,
303
+ current_run_id=self.current_run_id,
299
304
  message_buffer_limit=20, # TODO: Make this configurable
300
305
  message_buffer_min=8, # TODO: Make this configurable
301
306
  enable_summarization=False, # TODO: Make this configurable
letta/interface.py CHANGED
@@ -81,7 +81,7 @@ class CLIInterface(AgentInterface):
81
81
  @staticmethod
82
82
  def internal_monologue(msg: str, msg_obj: Optional[Message] = None, chunk_index: Optional[int] = None):
83
83
  # ANSI escape code for italic is '\x1B[3m'
84
- fstr = f"\x1B[3m{Fore.LIGHTBLACK_EX}{INNER_THOUGHTS_CLI_SYMBOL} {{msg}}{Style.RESET_ALL}"
84
+ fstr = f"\x1b[3m{Fore.LIGHTBLACK_EX}{INNER_THOUGHTS_CLI_SYMBOL} {{msg}}{Style.RESET_ALL}"
85
85
  if STRIP_UI:
86
86
  fstr = "{msg}"
87
87
  print(fstr.format(msg=msg))
@@ -1,7 +1,9 @@
1
+ import asyncio
1
2
  import json
3
+ from collections.abc import AsyncGenerator
2
4
  from datetime import datetime, timezone
3
5
  from enum import Enum
4
- from typing import AsyncGenerator, List, Optional, Union
6
+ from typing import Optional
5
7
 
6
8
  from anthropic import AsyncStream
7
9
  from anthropic.types.beta import (
@@ -131,14 +133,16 @@ class AnthropicStreamingInterface:
131
133
  self,
132
134
  stream: AsyncStream[BetaRawMessageStreamEvent],
133
135
  ttft_span: Optional["Span"] = None,
134
- provider_request_start_timestamp_ns: Optional[int] = None,
135
- ) -> AsyncGenerator[LettaMessage, None]:
136
+ provider_request_start_timestamp_ns: int | None = None,
137
+ ) -> AsyncGenerator[LettaMessage | LettaStopReason, None]:
136
138
  prev_message_type = None
137
139
  message_index = 0
138
140
  first_chunk = True
139
141
  try:
140
142
  async with stream:
141
143
  async for event in stream:
144
+ # TODO (cliandy): reconsider in stream cancellations
145
+ # await cancellation_token.check_and_raise_if_cancelled()
142
146
  if first_chunk and ttft_span is not None and provider_request_start_timestamp_ns is not None:
143
147
  now = get_utc_timestamp_ns()
144
148
  ttft_ns = now - provider_request_start_timestamp_ns
@@ -384,18 +388,21 @@ class AnthropicStreamingInterface:
384
388
  self.tool_call_buffer = []
385
389
 
386
390
  self.anthropic_mode = None
391
+ except asyncio.CancelledError as e:
392
+ logger.info("Cancelled stream %s", e)
393
+ yield LettaStopReason(stop_reason=StopReasonType.cancelled)
394
+ raise
387
395
  except Exception as e:
388
396
  logger.error("Error processing stream: %s", e)
389
- stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value)
390
- yield stop_reason
397
+ yield LettaStopReason(stop_reason=StopReasonType.error)
391
398
  raise
392
399
  finally:
393
400
  logger.info("AnthropicStreamingInterface: Stream processing complete.")
394
401
 
395
- def get_reasoning_content(self) -> List[Union[TextContent, ReasoningContent, RedactedReasoningContent]]:
402
+ def get_reasoning_content(self) -> list[TextContent | ReasoningContent | RedactedReasoningContent]:
396
403
  def _process_group(
397
- group: List[Union[ReasoningMessage, HiddenReasoningMessage]], group_type: str
398
- ) -> Union[TextContent, ReasoningContent, RedactedReasoningContent]:
404
+ group: list[ReasoningMessage | HiddenReasoningMessage], group_type: str
405
+ ) -> TextContent | ReasoningContent | RedactedReasoningContent:
399
406
  if group_type == "reasoning":
400
407
  reasoning_text = "".join(chunk.reasoning for chunk in group).strip()
401
408
  is_native = any(chunk.source == "reasoner_model" for chunk in group)
@@ -1,4 +1,5 @@
1
- from typing import Any, AsyncGenerator, Dict, List, Optional
1
+ from collections.abc import AsyncGenerator
2
+ from typing import Any
2
3
 
3
4
  from openai import AsyncStream
4
5
  from openai.types.chat.chat_completion_chunk import ChatCompletionChunk, Choice, ChoiceDelta
@@ -19,14 +20,14 @@ class OpenAIChatCompletionsStreamingInterface:
19
20
  self.optimistic_json_parser: OptimisticJSONParser = OptimisticJSONParser()
20
21
  self.stream_pre_execution_message: bool = stream_pre_execution_message
21
22
 
22
- self.current_parsed_json_result: Dict[str, Any] = {}
23
- self.content_buffer: List[str] = []
23
+ self.current_parsed_json_result: dict[str, Any] = {}
24
+ self.content_buffer: list[str] = []
24
25
  self.tool_call_happened: bool = False
25
26
  self.finish_reason_stop: bool = False
26
27
 
27
- self.tool_call_name: Optional[str] = None
28
+ self.tool_call_name: str | None = None
28
29
  self.tool_call_args_str: str = ""
29
- self.tool_call_id: Optional[str] = None
30
+ self.tool_call_id: str | None = None
30
31
 
31
32
  async def process(self, stream: AsyncStream[ChatCompletionChunk]) -> AsyncGenerator[str, None]:
32
33
  """
@@ -35,6 +36,8 @@ class OpenAIChatCompletionsStreamingInterface:
35
36
  """
36
37
  async with stream:
37
38
  async for chunk in stream:
39
+ # TODO (cliandy): reconsider in stream cancellations
40
+ # await cancellation_token.check_and_raise_if_cancelled()
38
41
  if chunk.choices:
39
42
  choice = chunk.choices[0]
40
43
  delta = choice.delta
@@ -103,7 +106,7 @@ class OpenAIChatCompletionsStreamingInterface:
103
106
  )
104
107
  )
105
108
 
106
- def _handle_finish_reason(self, finish_reason: Optional[str]) -> bool:
109
+ def _handle_finish_reason(self, finish_reason: str | None) -> bool:
107
110
  """Handles the finish reason and determines if streaming should stop."""
108
111
  if finish_reason == "tool_calls":
109
112
  self.tool_call_happened = True
@@ -1,5 +1,7 @@
1
+ import asyncio
2
+ from collections.abc import AsyncGenerator
1
3
  from datetime import datetime, timezone
2
- from typing import AsyncGenerator, List, Optional
4
+ from typing import Optional
3
5
 
4
6
  from openai import AsyncStream
5
7
  from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
@@ -55,12 +57,12 @@ class OpenAIStreamingInterface:
55
57
  self.input_tokens = 0
56
58
  self.output_tokens = 0
57
59
 
58
- self.content_buffer: List[str] = []
59
- self.tool_call_name: Optional[str] = None
60
- self.tool_call_id: Optional[str] = None
60
+ self.content_buffer: list[str] = []
61
+ self.tool_call_name: str | None = None
62
+ self.tool_call_id: str | None = None
61
63
  self.reasoning_messages = []
62
64
 
63
- def get_reasoning_content(self) -> List[TextContent]:
65
+ def get_reasoning_content(self) -> list[TextContent | OmittedReasoningContent]:
64
66
  content = "".join(self.reasoning_messages).strip()
65
67
 
66
68
  # Right now we assume that all models omit reasoning content for OAI,
@@ -87,8 +89,8 @@ class OpenAIStreamingInterface:
87
89
  self,
88
90
  stream: AsyncStream[ChatCompletionChunk],
89
91
  ttft_span: Optional["Span"] = None,
90
- provider_request_start_timestamp_ns: Optional[int] = None,
91
- ) -> AsyncGenerator[LettaMessage, None]:
92
+ provider_request_start_timestamp_ns: int | None = None,
93
+ ) -> AsyncGenerator[LettaMessage | LettaStopReason, None]:
92
94
  """
93
95
  Iterates over the OpenAI stream, yielding SSE events.
94
96
  It also collects tokens and detects if a tool call is triggered.
@@ -99,6 +101,8 @@ class OpenAIStreamingInterface:
99
101
  prev_message_type = None
100
102
  message_index = 0
101
103
  async for chunk in stream:
104
+ # TODO (cliandy): reconsider in stream cancellations
105
+ # await cancellation_token.check_and_raise_if_cancelled()
102
106
  if first_chunk and ttft_span is not None and provider_request_start_timestamp_ns is not None:
103
107
  now = get_utc_timestamp_ns()
104
108
  ttft_ns = now - provider_request_start_timestamp_ns
@@ -224,8 +228,7 @@ class OpenAIStreamingInterface:
224
228
  # If there was nothing in the name buffer, we can proceed to
225
229
  # output the arguments chunk as a ToolCallMessage
226
230
  else:
227
-
228
- # use_assisitant_message means that we should also not release main_json raw, and instead should only release the contents of "message": "..."
231
+ # use_assistant_message means that we should also not release main_json raw, and instead should only release the contents of "message": "..."
229
232
  if self.use_assistant_message and (
230
233
  self.last_flushed_function_name is not None
231
234
  and self.last_flushed_function_name == self.assistant_message_tool_name
@@ -349,10 +352,13 @@ class OpenAIStreamingInterface:
349
352
  prev_message_type = tool_call_msg.message_type
350
353
  yield tool_call_msg
351
354
  self.function_id_buffer = None
355
+ except asyncio.CancelledError as e:
356
+ logger.info("Cancelled stream %s", e)
357
+ yield LettaStopReason(stop_reason=StopReasonType.cancelled)
358
+ raise
352
359
  except Exception as e:
353
360
  logger.error("Error processing stream: %s", e)
354
- stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value)
355
- yield stop_reason
361
+ yield LettaStopReason(stop_reason=StopReasonType.error)
356
362
  raise
357
363
  finally:
358
364
  logger.info("OpenAIStreamingInterface: Stream processing complete.")
@@ -261,6 +261,7 @@ class OpenAIClient(LLMClientBase):
261
261
  """
262
262
  kwargs = await self._prepare_client_kwargs_async(llm_config)
263
263
  client = AsyncOpenAI(**kwargs)
264
+
264
265
  response: ChatCompletion = await client.chat.completions.create(**request_data)
265
266
  return response.model_dump()
266
267
 
@@ -304,7 +305,7 @@ class OpenAIClient(LLMClientBase):
304
305
  return response_stream
305
306
 
306
307
  @trace_method
307
- async def request_embeddings(self, inputs: List[str], embedding_config: EmbeddingConfig) -> List[dict]:
308
+ async def request_embeddings(self, inputs: List[str], embedding_config: EmbeddingConfig) -> List[List[float]]:
308
309
  """Request embeddings given texts and embedding config"""
309
310
  kwargs = self._prepare_client_kwargs_embedding(embedding_config)
310
311
  client = AsyncOpenAI(**kwargs)
letta/orm/agent.py CHANGED
@@ -245,6 +245,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, AsyncAttrs):
245
245
  Returns:
246
246
  PydanticAgentState: The Pydantic representation of the agent.
247
247
  """
248
+
248
249
  # Base fields: always included
249
250
  state = {
250
251
  "id": self.id,
letta/orm/file.py CHANGED
@@ -49,6 +49,7 @@ class FileMetadata(SqlalchemyBase, OrganizationMixin, SourceMixin, AsyncAttrs):
49
49
  )
50
50
 
51
51
  file_name: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The name of the file.")
52
+ original_file_name: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The original name of the file as uploaded.")
52
53
  file_path: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The file path on the system.")
53
54
  file_type: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The type of the file.")
54
55
  file_size: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, doc="The size of the file in bytes.")
@@ -81,7 +82,7 @@ class FileMetadata(SqlalchemyBase, OrganizationMixin, SourceMixin, AsyncAttrs):
81
82
  cascade="all, delete-orphan",
82
83
  )
83
84
 
84
- async def to_pydantic_async(self, include_content: bool = False) -> PydanticFileMetadata:
85
+ async def to_pydantic_async(self, include_content: bool = False, strip_directory_prefix: bool = False) -> PydanticFileMetadata:
85
86
  """
86
87
  Async version of `to_pydantic` that supports optional relationship loading
87
88
  without requiring `expire_on_commit=False`.
@@ -94,11 +95,16 @@ class FileMetadata(SqlalchemyBase, OrganizationMixin, SourceMixin, AsyncAttrs):
94
95
  else:
95
96
  content_text = None
96
97
 
98
+ file_name = self.file_name
99
+ if strip_directory_prefix and "/" in file_name:
100
+ file_name = "/".join(file_name.split("/")[1:])
101
+
97
102
  return PydanticFileMetadata(
98
103
  id=self.id,
99
104
  organization_id=self.organization_id,
100
105
  source_id=self.source_id,
101
- file_name=self.file_name,
106
+ file_name=file_name,
107
+ original_file_name=self.original_file_name,
102
108
  file_path=self.file_path,
103
109
  file_type=self.file_type,
104
110
  file_size=self.file_size,
letta/orm/files_agents.py CHANGED
@@ -19,25 +19,48 @@ class FileAgent(SqlalchemyBase, OrganizationMixin):
19
19
  """
20
20
  Join table between File and Agent.
21
21
 
22
- Tracks whether a file is currently open for the agent and
22
+ Tracks whether a file is currently "open" for the agent and
23
23
  the specific excerpt (grepped section) the agent is looking at.
24
24
  """
25
25
 
26
26
  __tablename__ = "files_agents"
27
27
  __table_args__ = (
28
- Index("ix_files_agents_file_id_agent_id", "file_id", "agent_id"),
29
- UniqueConstraint("file_id", "agent_id", name="uq_files_agents_file_agent"),
30
- UniqueConstraint("agent_id", "file_name", name="uq_files_agents_agent_file_name"),
31
- Index("ix_files_agents_agent_file_name", "agent_id", "file_name"),
28
+ # (file_id, agent_id) must be unique
29
+ UniqueConstraint("file_id", "agent_id", name="uq_file_agent"),
30
+ # (file_name, agent_id) must be unique
31
+ UniqueConstraint("agent_id", "file_name", name="uq_agent_filename"),
32
+ # helpful indexes for look-ups
33
+ Index("ix_file_agent", "file_id", "agent_id"),
34
+ Index("ix_agent_filename", "agent_id", "file_name"),
32
35
  )
33
36
  __pydantic_model__ = PydanticFileAgent
34
37
 
35
- # TODO: We want to migrate all the ORM models to do this, so we will need to move this to the SqlalchemyBase
36
- # TODO: Some still rely on the Pydantic object to do this
37
- id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"file_agent-{uuid.uuid4()}")
38
- file_id: Mapped[str] = mapped_column(String, ForeignKey("files.id", ondelete="CASCADE"), primary_key=True, doc="ID of the file.")
39
- file_name: Mapped[str] = mapped_column(String, nullable=False, doc="Denormalized copy of files.file_name; unique per agent.")
40
- agent_id: Mapped[str] = mapped_column(String, ForeignKey("agents.id", ondelete="CASCADE"), primary_key=True, doc="ID of the agent.")
38
+ # single-column surrogate PK
39
+ id: Mapped[str] = mapped_column(
40
+ String,
41
+ primary_key=True,
42
+ default=lambda: f"file_agent-{uuid.uuid4()}",
43
+ )
44
+
45
+ # not part of the PK, but NOT NULL + FK
46
+ file_id: Mapped[str] = mapped_column(
47
+ String,
48
+ ForeignKey("files.id", ondelete="CASCADE"),
49
+ nullable=False,
50
+ doc="ID of the file",
51
+ )
52
+ agent_id: Mapped[str] = mapped_column(
53
+ String,
54
+ ForeignKey("agents.id", ondelete="CASCADE"),
55
+ nullable=False,
56
+ doc="ID of the agent",
57
+ )
58
+
59
+ file_name: Mapped[str] = mapped_column(
60
+ String,
61
+ nullable=False,
62
+ doc="Denormalized copy of files.file_name; unique per agent",
63
+ )
41
64
 
42
65
  is_open: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, doc="True if the agent currently has the file open.")
43
66
  visible_content: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Portion of the file the agent is focused on.")
@@ -78,4 +101,6 @@ class FileAgent(SqlalchemyBase, OrganizationMixin):
78
101
  value=visible_content,
79
102
  label=self.file.file_name,
80
103
  read_only=True,
104
+ metadata={"source_id": self.file.source_id},
105
+ limit=CORE_MEMORY_SOURCE_CHAR_LIMIT,
81
106
  )
letta/orm/mcp_server.py CHANGED
@@ -39,6 +39,9 @@ class MCPServer(SqlalchemyBase, OrganizationMixin):
39
39
  # access token / api key for MCP servers that require authentication
40
40
  token: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The access token or api key for the MCP server")
41
41
 
42
+ # custom headers for authentication (key-value pairs)
43
+ custom_headers: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, doc="Custom authentication headers as key-value pairs")
44
+
42
45
  # stdio server
43
46
  stdio_config: Mapped[Optional[StdioServerConfig]] = mapped_column(
44
47
  MCPStdioServerConfigColumn, nullable=True, doc="The configuration for the stdio server"
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, Index
3
+ from sqlalchemy import JSON, Index, UniqueConstraint
4
4
  from sqlalchemy.orm import Mapped, mapped_column, relationship
5
5
 
6
6
  from letta.orm import FileMetadata
@@ -25,6 +25,7 @@ class Source(SqlalchemyBase, OrganizationMixin):
25
25
 
26
26
  __table_args__ = (
27
27
  Index(f"source_created_at_id_idx", "created_at", "id"),
28
+ UniqueConstraint("name", "organization_id", name="uq_source_name_organization"),
28
29
  {"extend_existing": True},
29
30
  )
30
31
 
letta/orm/step.py CHANGED
@@ -51,6 +51,9 @@ class Step(SqlalchemyBase):
51
51
  feedback: Mapped[Optional[str]] = mapped_column(
52
52
  None, nullable=True, doc="The feedback for this step. Must be either 'positive' or 'negative'."
53
53
  )
54
+ project_id: Mapped[Optional[str]] = mapped_column(
55
+ None, nullable=True, doc="The project that the agent that executed this step belongs to (cloud only)."
56
+ )
54
57
 
55
58
  # Relationships (foreign keys)
56
59
  organization: Mapped[Optional["Organization"]] = relationship("Organization")
@@ -43,14 +43,11 @@ Recall memory (conversation history):
43
43
  Even though you can only see recent messages in your immediate context, you can search over your entire message history from a database.
44
44
  This 'recall memory' database allows you to search through past interactions, effectively allowing you to remember prior engagements with a user.
45
45
 
46
- Archival memory (infinite size):
47
- Your archival memory is infinite size, but is held outside your immediate context, so you must explicitly run a retrieval/search operation to see data inside it.
48
- A more structured and deep storage space for your reflections, insights, or any other data that doesn't fit into the core memory but is essential enough not to be left only to the 'recall memory'.
49
-
50
- Data sources:
51
- You may be given access to external sources of data, relevant to the user's interaction. For example, code, style guides, and documentation relevant
52
- to the current interaction with the user. Your core memory will contain information about the contents of these data sources. You will have access
53
- to functions to open and close the files as a filesystem and maintain only the files that are relevant to the user's interaction.
46
+ Directories and Files:
47
+ You may be given access to a structured file system that mirrors real-world directories and files. Each directory may contain one or more files.
48
+ Files can include metadata (e.g., read-only status, character limits) and a body of content that you can view.
49
+ You will have access to functions that let you open and search these files, and your core memory will reflect the contents of any files currently open.
50
+ Maintain only those files relevant to the user’s current interaction.
54
51
 
55
52
 
56
53
  Base instructions finished.