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.
- letta/__init__.py +6 -1
- letta/agent.py +1 -0
- letta/agents/base_agent.py +8 -2
- letta/agents/ephemeral_summary_agent.py +33 -33
- letta/agents/letta_agent.py +104 -53
- letta/agents/voice_agent.py +2 -1
- letta/constants.py +8 -4
- letta/functions/function_sets/files.py +22 -7
- letta/functions/function_sets/multi_agent.py +34 -0
- letta/functions/types.py +1 -1
- letta/groups/helpers.py +8 -5
- letta/groups/sleeptime_multi_agent_v2.py +20 -15
- letta/interface.py +1 -1
- letta/interfaces/anthropic_streaming_interface.py +15 -8
- letta/interfaces/openai_chat_completions_streaming_interface.py +9 -6
- letta/interfaces/openai_streaming_interface.py +17 -11
- letta/llm_api/openai_client.py +2 -1
- letta/orm/agent.py +1 -0
- letta/orm/file.py +8 -2
- letta/orm/files_agents.py +36 -11
- letta/orm/mcp_server.py +3 -0
- letta/orm/source.py +2 -1
- letta/orm/step.py +3 -0
- letta/prompts/system/memgpt_v2_chat.txt +5 -8
- letta/schemas/agent.py +58 -23
- letta/schemas/embedding_config.py +3 -2
- letta/schemas/enums.py +4 -0
- letta/schemas/file.py +1 -0
- letta/schemas/letta_stop_reason.py +18 -0
- letta/schemas/mcp.py +15 -10
- letta/schemas/memory.py +35 -5
- letta/schemas/providers.py +11 -0
- letta/schemas/step.py +1 -0
- letta/schemas/tool.py +2 -1
- letta/server/rest_api/routers/v1/agents.py +320 -184
- letta/server/rest_api/routers/v1/groups.py +6 -2
- letta/server/rest_api/routers/v1/identities.py +6 -2
- letta/server/rest_api/routers/v1/jobs.py +49 -1
- letta/server/rest_api/routers/v1/sources.py +28 -19
- letta/server/rest_api/routers/v1/steps.py +7 -2
- letta/server/rest_api/routers/v1/tools.py +40 -9
- letta/server/rest_api/streaming_response.py +88 -0
- letta/server/server.py +61 -55
- letta/services/agent_manager.py +28 -16
- letta/services/file_manager.py +58 -9
- letta/services/file_processor/chunker/llama_index_chunker.py +2 -0
- letta/services/file_processor/embedder/openai_embedder.py +54 -10
- letta/services/file_processor/file_processor.py +59 -0
- letta/services/file_processor/parser/mistral_parser.py +2 -0
- letta/services/files_agents_manager.py +120 -2
- letta/services/helpers/agent_manager_helper.py +21 -4
- letta/services/job_manager.py +57 -6
- letta/services/mcp/base_client.py +1 -0
- letta/services/mcp_manager.py +13 -1
- letta/services/step_manager.py +14 -5
- letta/services/summarizer/summarizer.py +6 -22
- letta/services/tool_executor/builtin_tool_executor.py +0 -1
- letta/services/tool_executor/files_tool_executor.py +2 -2
- letta/services/tool_executor/multi_agent_tool_executor.py +23 -0
- letta/services/tool_manager.py +7 -7
- letta/settings.py +11 -2
- letta/templates/summary_request_text.j2 +19 -0
- letta/utils.py +95 -14
- {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/METADATA +2 -2
- {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/RECORD +69 -68
- /letta/{agents/prompts → prompts/system}/summary_system_prompt.txt +0 -0
- {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/LICENSE +0 -0
- {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/WHEEL +0 -0
- {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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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:
|
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:
|
66
|
+
input_messages: list[MessageCreate],
|
65
67
|
max_steps: int = DEFAULT_MAX_STEPS,
|
66
|
-
run_id:
|
68
|
+
run_id: str | None = None,
|
67
69
|
use_assistant_message: bool = True,
|
68
|
-
request_start_timestamp_ns:
|
69
|
-
include_return_message_types:
|
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: {
|
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:
|
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:
|
141
|
-
include_return_message_types:
|
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:
|
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:
|
164
|
-
include_return_message_types:
|
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:
|
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:
|
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"\
|
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
|
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:
|
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
|
-
|
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) ->
|
402
|
+
def get_reasoning_content(self) -> list[TextContent | ReasoningContent | RedactedReasoningContent]:
|
396
403
|
def _process_group(
|
397
|
-
group:
|
398
|
-
) ->
|
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
|
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:
|
23
|
-
self.content_buffer:
|
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:
|
28
|
+
self.tool_call_name: str | None = None
|
28
29
|
self.tool_call_args_str: str = ""
|
29
|
-
self.tool_call_id:
|
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:
|
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
|
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:
|
59
|
-
self.tool_call_name:
|
60
|
-
self.tool_call_id:
|
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) ->
|
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:
|
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
|
-
|
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.")
|
letta/llm_api/openai_client.py
CHANGED
@@ -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[
|
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
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=
|
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
|
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
|
-
|
29
|
-
UniqueConstraint("file_id", "agent_id", name="
|
30
|
-
|
31
|
-
|
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
|
-
#
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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.
|