letta-nightly 0.8.2.dev20250606215616__py3-none-any.whl → 0.8.3.dev20250607104236__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 (29) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +15 -11
  3. letta/agents/base_agent.py +1 -1
  4. letta/agents/helpers.py +13 -2
  5. letta/agents/letta_agent.py +23 -5
  6. letta/agents/voice_agent.py +1 -1
  7. letta/agents/voice_sleeptime_agent.py +12 -3
  8. letta/groups/sleeptime_multi_agent_v2.py +12 -2
  9. letta/llm_api/anthropic_client.py +8 -2
  10. letta/orm/passage.py +2 -0
  11. letta/schemas/letta_request.py +6 -0
  12. letta/schemas/passage.py +1 -0
  13. letta/server/rest_api/routers/v1/agents.py +9 -1
  14. letta/server/rest_api/routers/v1/tools.py +7 -2
  15. letta/server/server.py +7 -1
  16. letta/services/agent_manager.py +3 -3
  17. letta/services/context_window_calculator/context_window_calculator.py +1 -1
  18. letta/services/file_processor/file_processor.py +3 -1
  19. letta/services/helpers/agent_manager_helper.py +35 -4
  20. letta/services/mcp/stdio_client.py +5 -1
  21. letta/services/mcp_manager.py +4 -4
  22. letta/services/passage_manager.py +603 -18
  23. letta/services/tool_executor/files_tool_executor.py +9 -2
  24. letta/settings.py +2 -1
  25. {letta_nightly-0.8.2.dev20250606215616.dist-info → letta_nightly-0.8.3.dev20250607104236.dist-info}/METADATA +1 -1
  26. {letta_nightly-0.8.2.dev20250606215616.dist-info → letta_nightly-0.8.3.dev20250607104236.dist-info}/RECORD +29 -29
  27. {letta_nightly-0.8.2.dev20250606215616.dist-info → letta_nightly-0.8.3.dev20250607104236.dist-info}/LICENSE +0 -0
  28. {letta_nightly-0.8.2.dev20250606215616.dist-info → letta_nightly-0.8.3.dev20250607104236.dist-info}/WHEEL +0 -0
  29. {letta_nightly-0.8.2.dev20250606215616.dist-info → letta_nightly-0.8.3.dev20250607104236.dist-info}/entry_points.txt +0 -0
letta/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import os
2
2
 
3
- __version__ = "0.8.2"
3
+ __version__ = "0.8.3"
4
4
 
5
5
  if os.environ.get("LETTA_VERSION"):
6
6
  __version__ = os.environ["LETTA_VERSION"]
letta/agent.py CHANGED
@@ -70,7 +70,7 @@ from letta.services.step_manager import StepManager
70
70
  from letta.services.telemetry_manager import NoopTelemetryManager, TelemetryManager
71
71
  from letta.services.tool_executor.tool_execution_sandbox import ToolExecutionSandbox
72
72
  from letta.services.tool_manager import ToolManager
73
- from letta.settings import settings, summarizer_settings, model_settings
73
+ from letta.settings import settings, summarizer_settings
74
74
  from letta.streaming_interface import StreamingRefreshCLIInterface
75
75
  from letta.system import get_heartbeat, get_token_limit_warning, package_function_response, package_summarize_message, package_user_message
76
76
  from letta.utils import count_tokens, get_friendly_error_msg, get_tool_call_id, log_telemetry, parse_json, validate_function_response
@@ -503,7 +503,7 @@ class Agent(BaseAgent):
503
503
  response_message.function_call if response_message.function_call is not None else response_message.tool_calls[0].function
504
504
  )
505
505
  function_name = function_call.name
506
- self.logger.debug(f"Request to call function {function_name} with tool_call_id: {tool_call_id}")
506
+ self.logger.info(f"Request to call function {function_name} with tool_call_id: {tool_call_id}")
507
507
 
508
508
  # Failure case 1: function name is wrong (not in agent_state.tools)
509
509
  target_letta_tool = None
@@ -1282,7 +1282,7 @@ class Agent(BaseAgent):
1282
1282
  )
1283
1283
 
1284
1284
  async def get_context_window_async(self) -> ContextWindowOverview:
1285
- if os.getenv("LETTA_ENVIRONMENT") == "PRODUCTION" and model_settings.anthropic_api_key is not None:
1285
+ if os.getenv("LETTA_ENVIRONMENT") == "PRODUCTION":
1286
1286
  return await self.get_context_window_from_anthropic_async()
1287
1287
  return await self.get_context_window_from_tiktoken_async()
1288
1288
 
@@ -1291,8 +1291,8 @@ class Agent(BaseAgent):
1291
1291
  # Grab the in-context messages
1292
1292
  # conversion of messages to OpenAI dict format, which is passed to the token counter
1293
1293
  (in_context_messages, passage_manager_size, message_manager_size) = await asyncio.gather(
1294
- self.agent_manager.get_in_context_messages_async(agent_id=self.agent_state.id, actor=self.user),
1295
- self.passage_manager.size_async(actor=self.user, agent_id=self.agent_state.id),
1294
+ self.message_manager.get_messages_by_ids_async(message_ids=self.agent_state.message_ids, actor=self.user),
1295
+ self.passage_manager.agent_passage_size_async(actor=self.user, agent_id=self.agent_state.id),
1296
1296
  self.message_manager.size_async(actor=self.user, agent_id=self.agent_state.id),
1297
1297
  )
1298
1298
  in_context_messages_openai = [m.to_openai_dict() for m in in_context_messages]
@@ -1315,11 +1315,13 @@ class Agent(BaseAgent):
1315
1315
  core_memory = system_message[core_memory_marker_pos:].strip()
1316
1316
  else:
1317
1317
  # if no markers found, put everything in system message
1318
+ self.logger.info("No markers found in system message, core_memory and external_memory_summary will not be loaded")
1318
1319
  system_prompt = system_message
1319
1320
  external_memory_summary = ""
1320
1321
  core_memory = ""
1321
1322
  else:
1322
1323
  # if no system message, fall back on agent's system prompt
1324
+ self.logger.info("No system message found in history, core_memory and external_memory_summary will not be loaded")
1323
1325
  system_prompt = self.agent_state.system
1324
1326
  external_memory_summary = ""
1325
1327
  core_memory = ""
@@ -1411,8 +1413,8 @@ class Agent(BaseAgent):
1411
1413
  # Grab the in-context messages
1412
1414
  # conversion of messages to anthropic dict format, which is passed to the token counter
1413
1415
  (in_context_messages, passage_manager_size, message_manager_size) = await asyncio.gather(
1414
- self.agent_manager.get_in_context_messages_async(agent_id=self.agent_state.id, actor=self.user),
1415
- self.passage_manager.size_async(actor=self.user, agent_id=self.agent_state.id),
1416
+ self.message_manager.get_messages_by_ids_async(message_ids=self.agent_state.message_ids, actor=self.user),
1417
+ self.passage_manager.agent_passage_size_async(actor=self.user, agent_id=self.agent_state.id),
1416
1418
  self.message_manager.size_async(actor=self.user, agent_id=self.agent_state.id),
1417
1419
  )
1418
1420
  in_context_messages_anthropic = [m.to_anthropic_dict() for m in in_context_messages]
@@ -1435,14 +1437,16 @@ class Agent(BaseAgent):
1435
1437
  core_memory = system_message[core_memory_marker_pos:].strip()
1436
1438
  else:
1437
1439
  # if no markers found, put everything in system message
1440
+ self.logger.info("No markers found in system message, core_memory and external_memory_summary will not be loaded")
1438
1441
  system_prompt = system_message
1439
- external_memory_summary = None
1440
- core_memory = None
1442
+ external_memory_summary = ""
1443
+ core_memory = ""
1441
1444
  else:
1442
1445
  # if no system message, fall back on agent's system prompt
1446
+ self.logger.info("No system message found in history, core_memory and external_memory_summary will not be loaded")
1443
1447
  system_prompt = self.agent_state.system
1444
- external_memory_summary = None
1445
- core_memory = None
1448
+ external_memory_summary = ""
1449
+ core_memory = ""
1446
1450
 
1447
1451
  num_tokens_system_coroutine = anthropic_client.count_tokens(model=model, messages=[{"role": "user", "content": system_prompt}])
1448
1452
  num_tokens_core_memory_coroutine = (
@@ -104,7 +104,7 @@ class BaseAgent(ABC):
104
104
  if num_messages is None:
105
105
  num_messages = await self.message_manager.size_async(actor=self.actor, agent_id=agent_state.id)
106
106
  if num_archival_memories is None:
107
- num_archival_memories = await self.passage_manager.size_async(actor=self.actor, agent_id=agent_state.id)
107
+ num_archival_memories = await self.passage_manager.agent_passage_size_async(actor=self.actor, agent_id=agent_state.id)
108
108
 
109
109
  new_system_message_str = compile_system_message(
110
110
  system_prompt=agent_state.system,
letta/agents/helpers.py CHANGED
@@ -1,8 +1,9 @@
1
1
  import uuid
2
2
  import xml.etree.ElementTree as ET
3
- from typing import List, Tuple
3
+ from typing import List, Optional, Tuple
4
4
 
5
5
  from letta.schemas.agent import AgentState
6
+ from letta.schemas.letta_message import MessageType
6
7
  from letta.schemas.letta_response import LettaResponse
7
8
  from letta.schemas.message import Message, MessageCreate
8
9
  from letta.schemas.usage import LettaUsageStatistics
@@ -12,16 +13,26 @@ from letta.services.message_manager import MessageManager
12
13
 
13
14
 
14
15
  def _create_letta_response(
15
- new_in_context_messages: list[Message], use_assistant_message: bool, usage: LettaUsageStatistics
16
+ new_in_context_messages: list[Message],
17
+ use_assistant_message: bool,
18
+ usage: LettaUsageStatistics,
19
+ include_return_message_types: Optional[List[MessageType]] = None,
16
20
  ) -> LettaResponse:
17
21
  """
18
22
  Converts the newly created/persisted messages into a LettaResponse.
19
23
  """
20
24
  # NOTE: hacky solution to avoid returning heartbeat messages and the original user message
21
25
  filter_user_messages = [m for m in new_in_context_messages if m.role != "user"]
26
+
27
+ # Convert to Letta messages first
22
28
  response_messages = Message.to_letta_messages_from_list(
23
29
  messages=filter_user_messages, use_assistant_message=use_assistant_message, reverse=False
24
30
  )
31
+
32
+ # Apply message type filtering if specified
33
+ if include_return_message_types is not None:
34
+ response_messages = [msg for msg in response_messages if msg.message_type in include_return_message_types]
35
+
25
36
  return LettaResponse(messages=response_messages, usage=usage)
26
37
 
27
38
 
@@ -30,6 +30,7 @@ from letta.otel.metric_registry import MetricRegistry
30
30
  from letta.otel.tracing import log_event, trace_method, tracer
31
31
  from letta.schemas.agent import AgentState
32
32
  from letta.schemas.enums import MessageRole, MessageStreamStatus
33
+ from letta.schemas.letta_message import MessageType
33
34
  from letta.schemas.letta_message_content import OmittedReasoningContent, ReasoningContent, RedactedReasoningContent, TextContent
34
35
  from letta.schemas.letta_response import LettaResponse
35
36
  from letta.schemas.llm_config import LLMConfig
@@ -121,6 +122,7 @@ class LettaAgent(BaseAgent):
121
122
  max_steps: int = 10,
122
123
  use_assistant_message: bool = True,
123
124
  request_start_timestamp_ns: Optional[int] = None,
125
+ include_return_message_types: Optional[List[MessageType]] = None,
124
126
  ) -> LettaResponse:
125
127
  agent_state = await self.agent_manager.get_agent_by_id_async(
126
128
  agent_id=self.agent_id, include_relationships=["tools", "memory", "tool_exec_environment_variables"], actor=self.actor
@@ -132,7 +134,10 @@ class LettaAgent(BaseAgent):
132
134
  request_start_timestamp_ns=request_start_timestamp_ns,
133
135
  )
134
136
  return _create_letta_response(
135
- new_in_context_messages=new_in_context_messages, use_assistant_message=use_assistant_message, usage=usage
137
+ new_in_context_messages=new_in_context_messages,
138
+ use_assistant_message=use_assistant_message,
139
+ usage=usage,
140
+ include_return_message_types=include_return_message_types,
136
141
  )
137
142
 
138
143
  @trace_method
@@ -142,6 +147,7 @@ class LettaAgent(BaseAgent):
142
147
  max_steps: int = 10,
143
148
  use_assistant_message: bool = True,
144
149
  request_start_timestamp_ns: Optional[int] = None,
150
+ include_return_message_types: Optional[List[MessageType]] = None,
145
151
  ):
146
152
  agent_state = await self.agent_manager.get_agent_by_id_async(
147
153
  agent_id=self.agent_id, include_relationships=["tools", "memory", "tool_exec_environment_variables"], actor=self.actor
@@ -250,8 +256,12 @@ class LettaAgent(BaseAgent):
250
256
  letta_messages = Message.to_letta_messages_from_list(
251
257
  filter_user_messages, use_assistant_message=use_assistant_message, reverse=False
252
258
  )
259
+
253
260
  for message in letta_messages:
254
- yield f"data: {message.model_dump_json()}\n\n"
261
+ if not include_return_message_types:
262
+ yield f"data: {message.model_dump_json()}\n\n"
263
+ elif include_return_message_types and message.message_type in include_return_message_types:
264
+ yield f"data: {message.model_dump_json()}\n\n"
255
265
 
256
266
  if not should_continue:
257
267
  break
@@ -409,6 +419,7 @@ class LettaAgent(BaseAgent):
409
419
  max_steps: int = 10,
410
420
  use_assistant_message: bool = True,
411
421
  request_start_timestamp_ns: Optional[int] = None,
422
+ include_return_message_types: Optional[List[MessageType]] = None,
412
423
  ) -> AsyncGenerator[str, None]:
413
424
  """
414
425
  Carries out an invocation of the agent loop in a streaming fashion that yields partial tokens.
@@ -486,7 +497,12 @@ class LettaAgent(BaseAgent):
486
497
  request_span.add_event(name="time_to_first_token_ms", attributes={"ttft_ms": ns_to_ms(ttft_ns)})
487
498
  first_chunk = False
488
499
 
489
- yield f"data: {chunk.model_dump_json()}\n\n"
500
+ if include_return_message_types is None:
501
+ # return all data
502
+ yield f"data: {chunk.model_dump_json()}\n\n"
503
+ elif include_return_message_types and chunk.message_type in include_return_message_types:
504
+ # filter down returned data
505
+ yield f"data: {chunk.model_dump_json()}\n\n"
490
506
 
491
507
  # update usage
492
508
  usage.step_count += 1
@@ -563,7 +579,9 @@ class LettaAgent(BaseAgent):
563
579
 
564
580
  tool_return = [msg for msg in persisted_messages if msg.role == "tool"][-1].to_letta_messages()[0]
565
581
  if not (use_assistant_message and tool_return.name == "send_message"):
566
- yield f"data: {tool_return.model_dump_json()}\n\n"
582
+ # Apply message type filtering if specified
583
+ if include_return_message_types is None or tool_return.message_type in include_return_message_types:
584
+ yield f"data: {tool_return.model_dump_json()}\n\n"
567
585
 
568
586
  if not should_continue:
569
587
  break
@@ -763,7 +781,7 @@ class LettaAgent(BaseAgent):
763
781
  else asyncio.sleep(0, result=self.num_messages)
764
782
  ),
765
783
  (
766
- self.passage_manager.size_async(actor=self.actor, agent_id=agent_state.id)
784
+ self.passage_manager.agent_passage_size_async(actor=self.actor, agent_id=agent_state.id)
767
785
  if self.num_archival_memories is None
768
786
  else asyncio.sleep(0, result=self.num_archival_memories)
769
787
  ),
@@ -305,7 +305,7 @@ class VoiceAgent(BaseAgent):
305
305
  else asyncio.sleep(0, result=self.num_messages)
306
306
  ),
307
307
  (
308
- self.passage_manager.size_async(actor=self.actor, agent_id=agent_state.id)
308
+ self.passage_manager.agent_passage_size_async(actor=self.actor, agent_id=agent_state.id)
309
309
  if self.num_archival_memories is None
310
310
  else asyncio.sleep(0, result=self.num_archival_memories)
311
311
  ),
@@ -7,7 +7,7 @@ from letta.otel.tracing import trace_method
7
7
  from letta.schemas.agent import AgentState
8
8
  from letta.schemas.block import BlockUpdate
9
9
  from letta.schemas.enums import MessageStreamStatus
10
- from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage
10
+ from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage, MessageType
11
11
  from letta.schemas.letta_response import LettaResponse
12
12
  from letta.schemas.message import MessageCreate
13
13
  from letta.schemas.tool_rule import ChildToolRule, ContinueToolRule, InitToolRule, TerminalToolRule
@@ -59,7 +59,13 @@ class VoiceSleeptimeAgent(LettaAgent):
59
59
  def update_message_transcript(self, message_transcripts: List[str]):
60
60
  self.message_transcripts = message_transcripts
61
61
 
62
- async def step(self, input_messages: List[MessageCreate], max_steps: int = 20, use_assistant_message: bool = True) -> LettaResponse:
62
+ async def step(
63
+ self,
64
+ input_messages: List[MessageCreate],
65
+ max_steps: int = 20,
66
+ use_assistant_message: bool = True,
67
+ include_return_message_types: Optional[List[MessageType]] = None,
68
+ ) -> LettaResponse:
63
69
  """
64
70
  Process the user's input message, allowing the model to call memory-related tools
65
71
  until it decides to stop and provide a final response.
@@ -86,7 +92,10 @@ class VoiceSleeptimeAgent(LettaAgent):
86
92
  )
87
93
 
88
94
  return _create_letta_response(
89
- new_in_context_messages=new_in_context_messages, use_assistant_message=use_assistant_message, usage=usage
95
+ new_in_context_messages=new_in_context_messages,
96
+ use_assistant_message=use_assistant_message,
97
+ usage=usage,
98
+ include_return_message_types=include_return_message_types,
90
99
  )
91
100
 
92
101
  @trace_method
@@ -9,6 +9,7 @@ from letta.otel.tracing import trace_method
9
9
  from letta.schemas.enums import JobStatus
10
10
  from letta.schemas.group import Group, ManagerType
11
11
  from letta.schemas.job import JobUpdate
12
+ from letta.schemas.letta_message import MessageType
12
13
  from letta.schemas.letta_message_content import TextContent
13
14
  from letta.schemas.letta_response import LettaResponse
14
15
  from letta.schemas.message import Message, MessageCreate
@@ -63,6 +64,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
63
64
  max_steps: int = 10,
64
65
  use_assistant_message: bool = True,
65
66
  request_start_timestamp_ns: Optional[int] = None,
67
+ include_return_message_types: Optional[List[MessageType]] = None,
66
68
  ) -> LettaResponse:
67
69
  run_ids = []
68
70
 
@@ -87,7 +89,10 @@ class SleeptimeMultiAgentV2(BaseAgent):
87
89
  )
88
90
  # Perform foreground agent step
89
91
  response = await foreground_agent.step(
90
- input_messages=new_messages, max_steps=max_steps, use_assistant_message=use_assistant_message
92
+ input_messages=new_messages,
93
+ max_steps=max_steps,
94
+ use_assistant_message=use_assistant_message,
95
+ include_return_message_types=include_return_message_types,
91
96
  )
92
97
 
93
98
  # Get last response messages
@@ -129,8 +134,11 @@ class SleeptimeMultiAgentV2(BaseAgent):
129
134
  max_steps: int = 10,
130
135
  use_assistant_message: bool = True,
131
136
  request_start_timestamp_ns: Optional[int] = None,
137
+ include_return_message_types: Optional[List[MessageType]] = None,
132
138
  ):
133
- response = await self.step(input_messages, max_steps, use_assistant_message)
139
+ response = await self.step(
140
+ input_messages, max_steps, use_assistant_message, request_start_timestamp_ns, include_return_message_types
141
+ )
134
142
 
135
143
  for message in response.messages:
136
144
  yield f"data: {message.model_dump_json()}\n\n"
@@ -144,6 +152,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
144
152
  max_steps: int = 10,
145
153
  use_assistant_message: bool = True,
146
154
  request_start_timestamp_ns: Optional[int] = None,
155
+ include_return_message_types: Optional[List[MessageType]] = None,
147
156
  ) -> AsyncGenerator[str, None]:
148
157
  # Prepare new messages
149
158
  new_messages = []
@@ -170,6 +179,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
170
179
  max_steps=max_steps,
171
180
  use_assistant_message=use_assistant_message,
172
181
  request_start_timestamp_ns=request_start_timestamp_ns,
182
+ include_return_message_types=include_return_message_types,
173
183
  ):
174
184
  yield chunk
175
185
 
@@ -427,10 +427,16 @@ class AnthropicClient(LLMClientBase):
427
427
  if content_part.type == "text":
428
428
  content = strip_xml_tags(string=content_part.text, tag="thinking")
429
429
  if content_part.type == "tool_use":
430
- # hack for tool rules
430
+ # hack for incorrect tool format
431
431
  tool_input = json.loads(json.dumps(content_part.input))
432
432
  if "id" in tool_input and tool_input["id"].startswith("toolu_") and "function" in tool_input:
433
- arguments = str(tool_input["function"]["arguments"])
433
+ arguments = json.dumps(tool_input["function"]["arguments"], indent=2)
434
+ try:
435
+ args_json = json.loads(arguments)
436
+ if not isinstance(args_json, dict):
437
+ raise ValueError("Expected parseable json object for arguments")
438
+ except:
439
+ arguments = str(tool_input["function"]["arguments"])
434
440
  else:
435
441
  arguments = json.dumps(tool_input, indent=2)
436
442
  tool_calls = [
letta/orm/passage.py CHANGED
@@ -47,6 +47,8 @@ class SourcePassage(BasePassage, FileMixin, SourceMixin):
47
47
 
48
48
  __tablename__ = "source_passages"
49
49
 
50
+ file_name: Mapped[str] = mapped_column(doc="The name of the file that this passage was derived from")
51
+
50
52
  @declared_attr
51
53
  def file(cls) -> Mapped["FileMetadata"]:
52
54
  """Relationship to file"""
@@ -3,6 +3,7 @@ from typing import List, Optional
3
3
  from pydantic import BaseModel, Field, HttpUrl
4
4
 
5
5
  from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
6
+ from letta.schemas.letta_message import MessageType
6
7
  from letta.schemas.message import MessageCreate
7
8
 
8
9
 
@@ -21,6 +22,11 @@ class LettaRequest(BaseModel):
21
22
  description="The name of the message argument in the designated message tool.",
22
23
  )
23
24
 
25
+ # filter to only return specific message types
26
+ include_return_message_types: Optional[List[MessageType]] = Field(
27
+ default=None, description="Only return specified message types in the response. If `None` (default) returns all messages."
28
+ )
29
+
24
30
 
25
31
  class LettaStreamingRequest(LettaRequest):
26
32
  stream_tokens: bool = Field(
letta/schemas/passage.py CHANGED
@@ -23,6 +23,7 @@ class PassageBase(OrmMetadataBase):
23
23
 
24
24
  # file association
25
25
  file_id: Optional[str] = Field(None, description="The unique identifier of the file associated with the passage.")
26
+ file_name: Optional[str] = Field(None, description="The name of the file (only for source passages).")
26
27
  metadata: Optional[Dict] = Field({}, validation_alias="metadata_", description="The metadata of the passage.")
27
28
 
28
29
 
@@ -23,7 +23,7 @@ from letta.schemas.agent import AgentState, AgentType, CreateAgent, UpdateAgent
23
23
  from letta.schemas.block import Block, BlockUpdate
24
24
  from letta.schemas.group import Group
25
25
  from letta.schemas.job import JobStatus, JobUpdate, LettaRequestConfig
26
- from letta.schemas.letta_message import LettaMessageUnion, LettaMessageUpdateUnion
26
+ from letta.schemas.letta_message import LettaMessageUnion, LettaMessageUpdateUnion, MessageType
27
27
  from letta.schemas.letta_request import LettaRequest, LettaStreamingRequest
28
28
  from letta.schemas.letta_response import LettaResponse
29
29
  from letta.schemas.memory import ContextWindowOverview, CreateArchivalMemory, Memory
@@ -704,6 +704,7 @@ async def send_message(
704
704
  max_steps=10,
705
705
  use_assistant_message=request.use_assistant_message,
706
706
  request_start_timestamp_ns=request_start_timestamp_ns,
707
+ include_return_message_types=request.include_return_message_types,
707
708
  )
708
709
  else:
709
710
  result = await server.send_message_to_agent(
@@ -716,6 +717,7 @@ async def send_message(
716
717
  use_assistant_message=request.use_assistant_message,
717
718
  assistant_message_tool_name=request.assistant_message_tool_name,
718
719
  assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
720
+ include_return_message_types=request.include_return_message_types,
719
721
  )
720
722
  return result
721
723
 
@@ -791,6 +793,7 @@ async def send_message_streaming(
791
793
  max_steps=10,
792
794
  use_assistant_message=request.use_assistant_message,
793
795
  request_start_timestamp_ns=request_start_timestamp_ns,
796
+ include_return_message_types=request.include_return_message_types,
794
797
  ),
795
798
  media_type="text/event-stream",
796
799
  )
@@ -801,6 +804,7 @@ async def send_message_streaming(
801
804
  max_steps=10,
802
805
  use_assistant_message=request.use_assistant_message,
803
806
  request_start_timestamp_ns=request_start_timestamp_ns,
807
+ include_return_message_types=request.include_return_message_types,
804
808
  ),
805
809
  media_type="text/event-stream",
806
810
  )
@@ -816,6 +820,7 @@ async def send_message_streaming(
816
820
  assistant_message_tool_name=request.assistant_message_tool_name,
817
821
  assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
818
822
  request_start_timestamp_ns=request_start_timestamp_ns,
823
+ include_return_message_types=request.include_return_message_types,
819
824
  )
820
825
 
821
826
  return result
@@ -830,6 +835,7 @@ async def process_message_background(
830
835
  use_assistant_message: bool,
831
836
  assistant_message_tool_name: str,
832
837
  assistant_message_tool_kwarg: str,
838
+ include_return_message_types: Optional[List[MessageType]] = None,
833
839
  ) -> None:
834
840
  """Background task to process the message and update job status."""
835
841
  try:
@@ -845,6 +851,7 @@ async def process_message_background(
845
851
  assistant_message_tool_kwarg=assistant_message_tool_kwarg,
846
852
  metadata={"job_id": job_id}, # Pass job_id through metadata
847
853
  request_start_timestamp_ns=request_start_timestamp_ns,
854
+ include_return_message_types=include_return_message_types,
848
855
  )
849
856
 
850
857
  # Update job status to completed
@@ -912,6 +919,7 @@ async def send_message_async(
912
919
  use_assistant_message=request.use_assistant_message,
913
920
  assistant_message_tool_name=request.assistant_message_tool_name,
914
921
  assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
922
+ include_return_message_types=request.include_return_message_types,
915
923
  )
916
924
 
917
925
  return run
@@ -501,7 +501,8 @@ async def add_mcp_server_to_config(
501
501
  if isinstance(request, StdioServerConfig):
502
502
  mapped_request = MCPServer(server_name=request.server_name, server_type=request.type, stdio_config=request)
503
503
  # don't allow stdio servers
504
- raise HTTPException(status_code=400, detail="StdioServerConfig is not supported")
504
+ if tool_settings.mcp_disable_stdio: # protected server
505
+ raise HTTPException(status_code=400, detail="StdioServerConfig is not supported")
505
506
  elif isinstance(request, SSEServerConfig):
506
507
  mapped_request = MCPServer(server_name=request.server_name, server_type=request.type, server_url=request.server_url)
507
508
  # TODO: add HTTP streaming
@@ -530,4 +531,8 @@ async def delete_mcp_server_from_config(
530
531
  # log to DB
531
532
  actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
532
533
  mcp_server_id = await server.mcp_manager.get_mcp_server_id_by_name(mcp_server_name, actor)
533
- return server.mcp_manager.delete_mcp_server_by_id(mcp_server_id, actor=actor)
534
+ await server.mcp_manager.delete_mcp_server_by_id(mcp_server_id, actor=actor)
535
+
536
+ # TODO: don't do this in the future (just return MCPServer)
537
+ all_servers = await server.mcp_manager.list_mcp_servers(actor=actor)
538
+ return [server.to_config() for server in all_servers]
letta/server/server.py CHANGED
@@ -45,7 +45,7 @@ from letta.schemas.enums import JobStatus, MessageStreamStatus, ProviderCategory
45
45
  from letta.schemas.environment_variables import SandboxEnvironmentVariableCreate
46
46
  from letta.schemas.group import GroupCreate, ManagerType, SleeptimeManager, VoiceSleeptimeManager
47
47
  from letta.schemas.job import Job, JobUpdate
48
- from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage, ToolReturnMessage
48
+ from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage, MessageType, ToolReturnMessage
49
49
  from letta.schemas.letta_message_content import TextContent
50
50
  from letta.schemas.letta_response import LettaResponse
51
51
  from letta.schemas.llm_config import LLMConfig
@@ -2237,6 +2237,7 @@ class SyncServer(Server):
2237
2237
  assistant_message_tool_kwarg: str = constants.DEFAULT_MESSAGE_TOOL_KWARG,
2238
2238
  metadata: Optional[dict] = None,
2239
2239
  request_start_timestamp_ns: Optional[int] = None,
2240
+ include_return_message_types: Optional[List[MessageType]] = None,
2240
2241
  ) -> Union[StreamingResponse, LettaResponse]:
2241
2242
  """Split off into a separate function so that it can be imported in the /chat/completion proxy."""
2242
2243
  # TODO: @charles is this the correct way to handle?
@@ -2342,6 +2343,11 @@ class SyncServer(Server):
2342
2343
 
2343
2344
  # Get rid of the stream status messages
2344
2345
  filtered_stream = [d for d in generated_stream if not isinstance(d, MessageStreamStatus)]
2346
+
2347
+ # Apply message type filtering if specified
2348
+ if include_return_message_types is not None:
2349
+ filtered_stream = [msg for msg in filtered_stream if msg.message_type in include_return_message_types]
2350
+
2345
2351
  usage = await task
2346
2352
 
2347
2353
  # By default the stream will be messages of type LettaMessage or LettaLegacyMessage
@@ -1483,7 +1483,7 @@ class AgentManager:
1483
1483
  memory_edit_timestamp = curr_system_message.created_at
1484
1484
 
1485
1485
  num_messages = await self.message_manager.size_async(actor=actor, agent_id=agent_id)
1486
- num_archival_memories = await self.passage_manager.size_async(actor=actor, agent_id=agent_id)
1486
+ num_archival_memories = await self.passage_manager.agent_passage_size_async(actor=actor, agent_id=agent_id)
1487
1487
 
1488
1488
  # update memory (TODO: potentially update recall/archival stats separately)
1489
1489
  new_system_message_str = compile_system_message(
@@ -2075,6 +2075,7 @@ class AgentManager:
2075
2075
  # This is an AgentPassage - remove source fields
2076
2076
  data.pop("source_id", None)
2077
2077
  data.pop("file_id", None)
2078
+ data.pop("file_name", None)
2078
2079
  passage = AgentPassage(**data)
2079
2080
  else:
2080
2081
  # This is a SourcePassage - remove agent field
@@ -2135,6 +2136,7 @@ class AgentManager:
2135
2136
  # This is an AgentPassage - remove source fields
2136
2137
  data.pop("source_id", None)
2137
2138
  data.pop("file_id", None)
2139
+ data.pop("file_name", None)
2138
2140
  passage = AgentPassage(**data)
2139
2141
  else:
2140
2142
  # This is a SourcePassage - remove agent field
@@ -2198,14 +2200,12 @@ class AgentManager:
2198
2200
  self,
2199
2201
  actor: PydanticUser,
2200
2202
  agent_id: Optional[str] = None,
2201
- file_id: Optional[str] = None,
2202
2203
  limit: Optional[int] = 50,
2203
2204
  query_text: Optional[str] = None,
2204
2205
  start_date: Optional[datetime] = None,
2205
2206
  end_date: Optional[datetime] = None,
2206
2207
  before: Optional[str] = None,
2207
2208
  after: Optional[str] = None,
2208
- source_id: Optional[str] = None,
2209
2209
  embed_query: bool = False,
2210
2210
  ascending: bool = True,
2211
2211
  embedding_config: Optional[EmbeddingConfig] = None,
@@ -63,7 +63,7 @@ class ContextWindowCalculator:
63
63
  # Fetch data concurrently
64
64
  (in_context_messages, passage_manager_size, message_manager_size) = await asyncio.gather(
65
65
  message_manager.get_messages_by_ids_async(message_ids=agent_state.message_ids, actor=actor),
66
- passage_manager.size_async(actor=actor, agent_id=agent_state.id),
66
+ passage_manager.agent_passage_size_async(actor=actor, agent_id=agent_state.id),
67
67
  message_manager.size_async(actor=actor, agent_id=agent_state.id),
68
68
  )
69
69
 
@@ -111,7 +111,9 @@ class FileProcessor:
111
111
  )
112
112
  all_passages.extend(passages)
113
113
 
114
- all_passages = await self.passage_manager.create_many_passages_async(all_passages, self.actor)
114
+ all_passages = await self.passage_manager.create_many_source_passages_async(
115
+ passages=all_passages, file_metadata=file_metadata, actor=self.actor
116
+ )
115
117
 
116
118
  logger.info(f"Successfully processed {filename}: {len(all_passages)} passages")
117
119
 
@@ -607,15 +607,45 @@ def build_passage_query(
607
607
  if not agent_only: # Include source passages
608
608
  if agent_id is not None:
609
609
  source_passages = (
610
- select(SourcePassage, literal(None).label("agent_id"))
610
+ select(
611
+ SourcePassage.file_name,
612
+ SourcePassage.id,
613
+ SourcePassage.text,
614
+ SourcePassage.embedding_config,
615
+ SourcePassage.metadata_,
616
+ SourcePassage.embedding,
617
+ SourcePassage.created_at,
618
+ SourcePassage.updated_at,
619
+ SourcePassage.is_deleted,
620
+ SourcePassage._created_by_id,
621
+ SourcePassage._last_updated_by_id,
622
+ SourcePassage.organization_id,
623
+ SourcePassage.file_id,
624
+ SourcePassage.source_id,
625
+ literal(None).label("agent_id"),
626
+ )
611
627
  .join(SourcesAgents, SourcesAgents.source_id == SourcePassage.source_id)
612
628
  .where(SourcesAgents.agent_id == agent_id)
613
629
  .where(SourcePassage.organization_id == actor.organization_id)
614
630
  )
615
631
  else:
616
- source_passages = select(SourcePassage, literal(None).label("agent_id")).where(
617
- SourcePassage.organization_id == actor.organization_id
618
- )
632
+ source_passages = select(
633
+ SourcePassage.file_name,
634
+ SourcePassage.id,
635
+ SourcePassage.text,
636
+ SourcePassage.embedding_config,
637
+ SourcePassage.metadata_,
638
+ SourcePassage.embedding,
639
+ SourcePassage.created_at,
640
+ SourcePassage.updated_at,
641
+ SourcePassage.is_deleted,
642
+ SourcePassage._created_by_id,
643
+ SourcePassage._last_updated_by_id,
644
+ SourcePassage.organization_id,
645
+ SourcePassage.file_id,
646
+ SourcePassage.source_id,
647
+ literal(None).label("agent_id"),
648
+ ).where(SourcePassage.organization_id == actor.organization_id)
619
649
 
620
650
  if source_id:
621
651
  source_passages = source_passages.where(SourcePassage.source_id == source_id)
@@ -627,6 +657,7 @@ def build_passage_query(
627
657
  if agent_id is not None:
628
658
  agent_passages = (
629
659
  select(
660
+ literal(None).label("file_name"),
630
661
  AgentPassage.id,
631
662
  AgentPassage.text,
632
663
  AgentPassage.embedding_config,
@@ -11,7 +11,11 @@ logger = get_logger(__name__)
11
11
  # TODO: Get rid of Async prefix on this class name once we deprecate old sync code
12
12
  class AsyncStdioMCPClient(AsyncBaseMCPClient):
13
13
  async def _initialize_connection(self, server_config: StdioServerConfig) -> None:
14
- server_params = StdioServerParameters(command=server_config.command, args=server_config.args)
14
+
15
+ args = [arg.split() for arg in server_config.args]
16
+ # flatten
17
+ args = [arg for sublist in args for arg in sublist]
18
+ server_params = StdioServerParameters(command=server_config.command, args=args)
15
19
  stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
16
20
  self.stdio, self.write = stdio_transport
17
21
  self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))