letta-nightly 0.8.2.dev20250606215616__py3-none-any.whl → 0.8.3.dev20250607000559__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.
- letta/__init__.py +1 -1
- letta/agent.py +15 -11
- letta/agents/base_agent.py +1 -1
- letta/agents/helpers.py +13 -2
- letta/agents/letta_agent.py +23 -5
- letta/agents/voice_agent.py +1 -1
- letta/agents/voice_sleeptime_agent.py +12 -3
- letta/groups/sleeptime_multi_agent_v2.py +12 -2
- letta/llm_api/anthropic_client.py +8 -2
- letta/orm/passage.py +2 -0
- letta/schemas/letta_request.py +6 -0
- letta/schemas/passage.py +1 -0
- letta/server/rest_api/routers/v1/agents.py +9 -1
- letta/server/rest_api/routers/v1/tools.py +7 -2
- letta/server/server.py +7 -1
- letta/services/agent_manager.py +3 -3
- letta/services/context_window_calculator/context_window_calculator.py +1 -1
- letta/services/file_processor/file_processor.py +3 -1
- letta/services/helpers/agent_manager_helper.py +35 -4
- letta/services/mcp/stdio_client.py +5 -1
- letta/services/mcp_manager.py +4 -4
- letta/services/passage_manager.py +603 -18
- letta/services/tool_executor/files_tool_executor.py +9 -2
- letta/settings.py +2 -1
- {letta_nightly-0.8.2.dev20250606215616.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/METADATA +1 -1
- {letta_nightly-0.8.2.dev20250606215616.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/RECORD +29 -29
- {letta_nightly-0.8.2.dev20250606215616.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/LICENSE +0 -0
- {letta_nightly-0.8.2.dev20250606215616.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/WHEEL +0 -0
- {letta_nightly-0.8.2.dev20250606215616.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/entry_points.txt +0 -0
letta/__init__.py
CHANGED
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
|
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.
|
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"
|
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.
|
1295
|
-
self.passage_manager.
|
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.
|
1415
|
-
self.passage_manager.
|
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 =
|
1440
|
-
core_memory =
|
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 =
|
1445
|
-
core_memory =
|
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 = (
|
letta/agents/base_agent.py
CHANGED
@@ -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.
|
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],
|
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
|
|
letta/agents/letta_agent.py
CHANGED
@@ -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,
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
),
|
letta/agents/voice_agent.py
CHANGED
@@ -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.
|
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(
|
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,
|
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,
|
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(
|
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
|
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 =
|
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"""
|
letta/schemas/letta_request.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
letta/services/agent_manager.py
CHANGED
@@ -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.
|
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.
|
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.
|
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(
|
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(
|
617
|
-
SourcePassage.
|
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
|
-
|
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))
|