letta-nightly 0.11.6.dev20250903104037__py3-none-any.whl → 0.11.7.dev20250904045700__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 +10 -14
- letta/agents/base_agent.py +18 -0
- letta/agents/helpers.py +32 -7
- letta/agents/letta_agent.py +953 -762
- letta/agents/voice_agent.py +1 -1
- letta/client/streaming.py +0 -1
- letta/constants.py +11 -8
- letta/errors.py +9 -0
- letta/functions/function_sets/base.py +77 -69
- letta/functions/function_sets/builtin.py +41 -22
- letta/functions/function_sets/multi_agent.py +1 -2
- letta/functions/schema_generator.py +0 -1
- letta/helpers/converters.py +8 -3
- letta/helpers/datetime_helpers.py +5 -4
- letta/helpers/message_helper.py +1 -2
- letta/helpers/pinecone_utils.py +0 -1
- letta/helpers/tool_rule_solver.py +10 -0
- letta/helpers/tpuf_client.py +848 -0
- letta/interface.py +8 -8
- letta/interfaces/anthropic_streaming_interface.py +7 -0
- letta/interfaces/openai_streaming_interface.py +29 -6
- letta/llm_api/anthropic_client.py +188 -18
- letta/llm_api/azure_client.py +0 -1
- letta/llm_api/bedrock_client.py +1 -2
- letta/llm_api/deepseek_client.py +319 -5
- letta/llm_api/google_vertex_client.py +75 -17
- letta/llm_api/groq_client.py +0 -1
- letta/llm_api/helpers.py +2 -2
- letta/llm_api/llm_api_tools.py +1 -50
- letta/llm_api/llm_client.py +6 -8
- letta/llm_api/mistral.py +1 -1
- letta/llm_api/openai.py +16 -13
- letta/llm_api/openai_client.py +31 -16
- letta/llm_api/together_client.py +0 -1
- letta/llm_api/xai_client.py +0 -1
- letta/local_llm/chat_completion_proxy.py +7 -6
- letta/local_llm/settings/settings.py +1 -1
- letta/orm/__init__.py +1 -0
- letta/orm/agent.py +8 -6
- letta/orm/archive.py +9 -1
- letta/orm/block.py +3 -4
- letta/orm/block_history.py +3 -1
- letta/orm/group.py +2 -3
- letta/orm/identity.py +1 -2
- letta/orm/job.py +1 -2
- letta/orm/llm_batch_items.py +1 -2
- letta/orm/message.py +8 -4
- letta/orm/mixins.py +18 -0
- letta/orm/organization.py +2 -0
- letta/orm/passage.py +8 -1
- letta/orm/passage_tag.py +55 -0
- letta/orm/sandbox_config.py +1 -3
- letta/orm/step.py +1 -2
- letta/orm/tool.py +1 -0
- letta/otel/resource.py +2 -2
- letta/plugins/plugins.py +1 -1
- letta/prompts/prompt_generator.py +10 -2
- letta/schemas/agent.py +11 -0
- letta/schemas/archive.py +4 -0
- letta/schemas/block.py +13 -0
- letta/schemas/embedding_config.py +0 -1
- letta/schemas/enums.py +24 -7
- letta/schemas/group.py +12 -0
- letta/schemas/letta_message.py +55 -1
- letta/schemas/letta_message_content.py +28 -0
- letta/schemas/letta_request.py +21 -4
- letta/schemas/letta_stop_reason.py +9 -1
- letta/schemas/llm_config.py +24 -8
- letta/schemas/mcp.py +0 -3
- letta/schemas/memory.py +14 -0
- letta/schemas/message.py +245 -141
- letta/schemas/openai/chat_completion_request.py +2 -1
- letta/schemas/passage.py +1 -0
- letta/schemas/providers/bedrock.py +1 -1
- letta/schemas/providers/openai.py +2 -2
- letta/schemas/tool.py +11 -5
- letta/schemas/tool_execution_result.py +0 -1
- letta/schemas/tool_rule.py +71 -0
- letta/serialize_schemas/marshmallow_agent.py +1 -2
- letta/server/rest_api/app.py +3 -3
- letta/server/rest_api/auth/index.py +0 -1
- letta/server/rest_api/interface.py +3 -11
- letta/server/rest_api/redis_stream_manager.py +3 -4
- letta/server/rest_api/routers/v1/agents.py +143 -84
- letta/server/rest_api/routers/v1/blocks.py +1 -1
- letta/server/rest_api/routers/v1/folders.py +1 -1
- letta/server/rest_api/routers/v1/groups.py +23 -22
- letta/server/rest_api/routers/v1/internal_templates.py +68 -0
- letta/server/rest_api/routers/v1/sandbox_configs.py +11 -5
- letta/server/rest_api/routers/v1/sources.py +1 -1
- letta/server/rest_api/routers/v1/tools.py +167 -15
- letta/server/rest_api/streaming_response.py +4 -3
- letta/server/rest_api/utils.py +75 -18
- letta/server/server.py +24 -35
- letta/services/agent_manager.py +359 -45
- letta/services/agent_serialization_manager.py +23 -3
- letta/services/archive_manager.py +72 -3
- letta/services/block_manager.py +1 -2
- letta/services/context_window_calculator/token_counter.py +11 -6
- letta/services/file_manager.py +1 -3
- letta/services/files_agents_manager.py +2 -4
- letta/services/group_manager.py +73 -12
- letta/services/helpers/agent_manager_helper.py +5 -5
- letta/services/identity_manager.py +8 -3
- letta/services/job_manager.py +2 -14
- letta/services/llm_batch_manager.py +1 -3
- letta/services/mcp/base_client.py +1 -2
- letta/services/mcp_manager.py +5 -6
- letta/services/message_manager.py +536 -15
- letta/services/organization_manager.py +1 -2
- letta/services/passage_manager.py +287 -12
- letta/services/provider_manager.py +1 -3
- letta/services/sandbox_config_manager.py +12 -7
- letta/services/source_manager.py +1 -2
- letta/services/step_manager.py +0 -1
- letta/services/summarizer/summarizer.py +4 -2
- letta/services/telemetry_manager.py +1 -3
- letta/services/tool_executor/builtin_tool_executor.py +136 -316
- letta/services/tool_executor/core_tool_executor.py +231 -74
- letta/services/tool_executor/files_tool_executor.py +2 -2
- letta/services/tool_executor/mcp_tool_executor.py +0 -1
- letta/services/tool_executor/multi_agent_tool_executor.py +2 -2
- letta/services/tool_executor/sandbox_tool_executor.py +0 -1
- letta/services/tool_executor/tool_execution_sandbox.py +2 -3
- letta/services/tool_manager.py +181 -64
- letta/services/tool_sandbox/modal_deployment_manager.py +2 -2
- letta/services/user_manager.py +1 -2
- letta/settings.py +5 -3
- letta/streaming_interface.py +3 -3
- letta/system.py +1 -1
- letta/utils.py +0 -1
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/METADATA +11 -7
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/RECORD +137 -135
- letta/llm_api/deepseek.py +0 -303
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/licenses/LICENSE +0 -0
letta/agents/voice_agent.py
CHANGED
@@ -484,7 +484,7 @@ class VoiceAgent(BaseAgent):
|
|
484
484
|
if start_date and end_date and start_date > end_date:
|
485
485
|
start_date, end_date = end_date, start_date
|
486
486
|
|
487
|
-
archival_results = await self.agent_manager.
|
487
|
+
archival_results = await self.agent_manager.query_agent_passages_async(
|
488
488
|
actor=self.actor,
|
489
489
|
agent_id=self.agent_id,
|
490
490
|
query_text=archival_query,
|
letta/client/streaming.py
CHANGED
@@ -23,7 +23,6 @@ def _sse_post(url: str, data: dict, headers: dict) -> Generator[Union[LettaStrea
|
|
23
23
|
# TODO: Please note his is a very generous timeout for e2b reasons
|
24
24
|
with httpx.Client(timeout=httpx.Timeout(5 * 60.0, read=5 * 60.0)) as client:
|
25
25
|
with connect_sse(client, method="POST", url=url, json=data, headers=headers) as event_source:
|
26
|
-
|
27
26
|
# Check for immediate HTTP errors before processing the SSE stream
|
28
27
|
if not event_source.response.is_success:
|
29
28
|
response_bytes = event_source.response.read()
|
letta/constants.py
CHANGED
@@ -132,7 +132,7 @@ MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX = re.compile(
|
|
132
132
|
)
|
133
133
|
|
134
134
|
# Built in tools
|
135
|
-
BUILTIN_TOOLS = ["run_code", "web_search"]
|
135
|
+
BUILTIN_TOOLS = ["run_code", "web_search", "fetch_webpage"]
|
136
136
|
|
137
137
|
# Built in tools
|
138
138
|
FILES_TOOLS = ["open_files", "grep_files", "semantic_search_files"]
|
@@ -167,6 +167,9 @@ def FUNCTION_RETURN_VALUE_TRUNCATED(return_str, return_char: int, return_char_li
|
|
167
167
|
DEFAULT_MESSAGE_TOOL = SEND_MESSAGE_TOOL_NAME
|
168
168
|
DEFAULT_MESSAGE_TOOL_KWARG = "message"
|
169
169
|
|
170
|
+
# The name of the conversation search tool - messages with this tool should not be indexed
|
171
|
+
CONVERSATION_SEARCH_TOOL_NAME = "conversation_search"
|
172
|
+
|
170
173
|
PRE_EXECUTION_MESSAGE_ARG = "pre_exec_msg"
|
171
174
|
|
172
175
|
REQUEST_HEARTBEAT_PARAM = "request_heartbeat"
|
@@ -210,12 +213,12 @@ LLM_MAX_TOKENS = {
|
|
210
213
|
"deepseek-reasoner": 64000,
|
211
214
|
## OpenAI models: https://platform.openai.com/docs/models/overview
|
212
215
|
# gpt-5
|
213
|
-
"gpt-5":
|
214
|
-
"gpt-5-2025-08-07":
|
215
|
-
"gpt-5-mini":
|
216
|
-
"gpt-5-mini-2025-08-07":
|
217
|
-
"gpt-5-nano":
|
218
|
-
"gpt-5-nano-2025-08-07":
|
216
|
+
"gpt-5": 272000,
|
217
|
+
"gpt-5-2025-08-07": 272000,
|
218
|
+
"gpt-5-mini": 272000,
|
219
|
+
"gpt-5-mini-2025-08-07": 272000,
|
220
|
+
"gpt-5-nano": 272000,
|
221
|
+
"gpt-5-nano-2025-08-07": 272000,
|
219
222
|
# reasoners
|
220
223
|
"o1": 200000,
|
221
224
|
# "o1-pro": 200000, # responses API only
|
@@ -340,7 +343,7 @@ CORE_MEMORY_BLOCK_CHAR_LIMIT: int = 20000
|
|
340
343
|
|
341
344
|
# Function return limits
|
342
345
|
FUNCTION_RETURN_CHAR_LIMIT = 50000 # ~300 words
|
343
|
-
BASE_FUNCTION_RETURN_CHAR_LIMIT =
|
346
|
+
BASE_FUNCTION_RETURN_CHAR_LIMIT = 50000 # same as regular function limit
|
344
347
|
FILE_IS_TRUNCATED_WARNING = "# NOTE: This block is truncated, use functions to view the full content."
|
345
348
|
|
346
349
|
MAX_PAUSE_HEARTBEATS = 360 # in min
|
letta/errors.py
CHANGED
@@ -60,6 +60,15 @@ class LettaToolNameConflictError(LettaError):
|
|
60
60
|
)
|
61
61
|
|
62
62
|
|
63
|
+
class LettaToolNameSchemaMismatchError(LettaToolCreateError):
|
64
|
+
"""Error raised when a tool name our source codedoes not match the name in the JSON schema."""
|
65
|
+
|
66
|
+
def __init__(self, tool_name: str, json_schema_name: str, source_code: str):
|
67
|
+
super().__init__(
|
68
|
+
message=f"Tool name '{tool_name}' does not match the name in the JSON schema '{json_schema_name}' or in the source code `{source_code}`",
|
69
|
+
)
|
70
|
+
|
71
|
+
|
63
72
|
class LettaConfigurationError(LettaError):
|
64
73
|
"""Error raised when there are configuration-related issues."""
|
65
74
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import Optional
|
1
|
+
from typing import List, Literal, Optional
|
2
2
|
|
3
3
|
from letta.agent import Agent
|
4
4
|
from letta.constants import CORE_MEMORY_LINE_NUMBER_WARNING
|
@@ -20,113 +20,121 @@ def send_message(self: "Agent", message: str) -> Optional[str]:
|
|
20
20
|
return None
|
21
21
|
|
22
22
|
|
23
|
-
def conversation_search(
|
23
|
+
def conversation_search(
|
24
|
+
self: "Agent",
|
25
|
+
query: str,
|
26
|
+
roles: Optional[List[Literal["assistant", "user", "tool"]]] = None,
|
27
|
+
limit: Optional[int] = None,
|
28
|
+
start_date: Optional[str] = None,
|
29
|
+
end_date: Optional[str] = None,
|
30
|
+
) -> Optional[str]:
|
24
31
|
"""
|
25
|
-
Search prior conversation history using
|
32
|
+
Search prior conversation history using hybrid search (text + semantic similarity).
|
26
33
|
|
27
34
|
Args:
|
28
|
-
query (str): String to search for.
|
29
|
-
|
35
|
+
query (str): String to search for using both text matching and semantic similarity.
|
36
|
+
roles (Optional[List[Literal["assistant", "user", "tool"]]]): Optional list of message roles to filter by.
|
37
|
+
limit (Optional[int]): Maximum number of results to return. Uses system default if not specified.
|
38
|
+
start_date (Optional[str]): Filter results to messages created after this date. ISO 8601 format: "YYYY-MM-DD" or "YYYY-MM-DDTHH:MM". Examples: "2024-01-15", "2024-01-15T14:30".
|
39
|
+
end_date (Optional[str]): Filter results to messages created before this date. ISO 8601 format: "YYYY-MM-DD" or "YYYY-MM-DDTHH:MM". Examples: "2024-01-20", "2024-01-20T17:00".
|
40
|
+
|
41
|
+
Examples:
|
42
|
+
# Search all messages
|
43
|
+
conversation_search(query="project updates")
|
44
|
+
|
45
|
+
# Search only assistant messages
|
46
|
+
conversation_search(query="error handling", roles=["assistant"])
|
47
|
+
|
48
|
+
# Search with date range
|
49
|
+
conversation_search(query="meetings", start_date="2024-01-15", end_date="2024-01-20")
|
50
|
+
|
51
|
+
# Search with limit
|
52
|
+
conversation_search(query="debugging", limit=10)
|
30
53
|
|
31
54
|
Returns:
|
32
|
-
str: Query result string
|
55
|
+
str: Query result string containing matching messages with timestamps and content.
|
33
56
|
"""
|
34
57
|
|
35
|
-
import math
|
36
|
-
|
37
58
|
from letta.constants import RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
|
38
59
|
from letta.helpers.json_helpers import json_dumps
|
39
60
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
except:
|
45
|
-
raise ValueError("'page' argument must be an integer")
|
46
|
-
count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
|
47
|
-
# TODO: add paging by page number. currently cursor only works with strings.
|
48
|
-
# original: start=page * count
|
61
|
+
# Use provided limit or default
|
62
|
+
if limit is None:
|
63
|
+
limit = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
|
64
|
+
|
49
65
|
messages = self.message_manager.list_messages_for_agent(
|
50
66
|
agent_id=self.agent_state.id,
|
51
67
|
actor=self.user,
|
52
68
|
query_text=query,
|
53
|
-
|
69
|
+
roles=roles,
|
70
|
+
limit=limit,
|
54
71
|
)
|
55
|
-
|
56
|
-
num_pages = math.ceil(total / count) - 1 # 0 index
|
72
|
+
|
57
73
|
if len(messages) == 0:
|
58
74
|
results_str = "No results found."
|
59
75
|
else:
|
60
|
-
results_pref = f"
|
61
|
-
results_formatted = [
|
76
|
+
results_pref = f"Found {len(messages)} results:"
|
77
|
+
results_formatted = []
|
78
|
+
for message in messages:
|
79
|
+
# Extract text content from message
|
80
|
+
text_content = message.content[0].text if message.content else ""
|
81
|
+
result_entry = {"role": message.role, "content": text_content}
|
82
|
+
results_formatted.append(result_entry)
|
62
83
|
results_str = f"{results_pref} {json_dumps(results_formatted)}"
|
63
84
|
return results_str
|
64
85
|
|
65
86
|
|
66
|
-
async def archival_memory_insert(self: "Agent", content: str) -> Optional[str]:
|
87
|
+
async def archival_memory_insert(self: "Agent", content: str, tags: Optional[list[str]] = None) -> Optional[str]:
|
67
88
|
"""
|
68
89
|
Add to archival memory. Make sure to phrase the memory contents such that it can be easily queried later.
|
69
90
|
|
70
91
|
Args:
|
71
92
|
content (str): Content to write to the memory. All unicode (including emojis) are supported.
|
93
|
+
tags (Optional[list[str]]): Optional list of tags to associate with this memory for better organization and filtering.
|
72
94
|
|
73
95
|
Returns:
|
74
96
|
Optional[str]: None is always returned as this function does not produce a response.
|
75
97
|
"""
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
98
|
+
raise NotImplementedError("This should never be invoked directly. Contact Letta if you see this error message.")
|
99
|
+
|
100
|
+
|
101
|
+
async def archival_memory_search(
|
102
|
+
self: "Agent",
|
103
|
+
query: str,
|
104
|
+
tags: Optional[list[str]] = None,
|
105
|
+
tag_match_mode: Literal["any", "all"] = "any",
|
106
|
+
top_k: Optional[int] = None,
|
107
|
+
start_datetime: Optional[str] = None,
|
108
|
+
end_datetime: Optional[str] = None,
|
109
|
+
) -> Optional[str]:
|
86
110
|
"""
|
87
|
-
Search archival memory using semantic (embedding-based) search.
|
111
|
+
Search archival memory using semantic (embedding-based) search with optional temporal filtering.
|
88
112
|
|
89
113
|
Args:
|
90
|
-
query (str): String to search for.
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
str:
|
96
|
-
"""
|
114
|
+
query (str): String to search for using semantic similarity.
|
115
|
+
tags (Optional[list[str]]): Optional list of tags to filter search results. Only passages with these tags will be returned.
|
116
|
+
tag_match_mode (Literal["any", "all"]): How to match tags - "any" to match passages with any of the tags, "all" to match only passages with all tags. Defaults to "any".
|
117
|
+
top_k (Optional[int]): Maximum number of results to return. Uses system default if not specified.
|
118
|
+
start_datetime (Optional[str]): Filter results to passages created after this datetime. ISO 8601 format: "YYYY-MM-DD" or "YYYY-MM-DDTHH:MM". Examples: "2024-01-15", "2024-01-15T14:30".
|
119
|
+
end_datetime (Optional[str]): Filter results to passages created before this datetime. ISO 8601 format: "YYYY-MM-DD" or "YYYY-MM-DDTHH:MM". Examples: "2024-01-20", "2024-01-20T17:00".
|
97
120
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
page = 0
|
102
|
-
try:
|
103
|
-
page = int(page)
|
104
|
-
except:
|
105
|
-
raise ValueError("'page' argument must be an integer")
|
106
|
-
count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
|
107
|
-
|
108
|
-
try:
|
109
|
-
# Get results using passage manager
|
110
|
-
all_results = await self.agent_manager.list_passages_async(
|
111
|
-
actor=self.user,
|
112
|
-
agent_id=self.agent_state.id,
|
113
|
-
query_text=query,
|
114
|
-
limit=count + start, # Request enough results to handle offset
|
115
|
-
embedding_config=self.agent_state.embedding_config,
|
116
|
-
embed_query=True,
|
117
|
-
)
|
121
|
+
Examples:
|
122
|
+
# Search all passages
|
123
|
+
archival_memory_search(query="project updates")
|
118
124
|
|
119
|
-
#
|
120
|
-
|
121
|
-
paged_results = all_results[start:end]
|
125
|
+
# Search with date range (full days)
|
126
|
+
archival_memory_search(query="meetings", start_datetime="2024-01-15", end_datetime="2024-01-20")
|
122
127
|
|
123
|
-
#
|
124
|
-
|
128
|
+
# Search with specific time range
|
129
|
+
archival_memory_search(query="error logs", start_datetime="2024-01-15T09:30", end_datetime="2024-01-15T17:30")
|
125
130
|
|
126
|
-
|
131
|
+
# Search from a specific point in time onwards
|
132
|
+
archival_memory_search(query="customer feedback", start_datetime="2024-01-15T14:00")
|
127
133
|
|
128
|
-
|
129
|
-
|
134
|
+
Returns:
|
135
|
+
str: Query result string containing matching passages with timestamps and content.
|
136
|
+
"""
|
137
|
+
raise NotImplementedError("This should never be invoked directly. Contact Letta if you see this error message.")
|
130
138
|
|
131
139
|
|
132
140
|
def core_memory_append(agent_state: "AgentState", label: str, content: str) -> Optional[str]: # type: ignore
|
@@ -1,6 +1,4 @@
|
|
1
|
-
from typing import List, Literal
|
2
|
-
|
3
|
-
from letta.functions.types import SearchTask
|
1
|
+
from typing import List, Literal, Optional
|
4
2
|
|
5
3
|
|
6
4
|
def run_code(code: str, language: Literal["python", "js", "ts", "r", "java"]) -> str:
|
@@ -17,31 +15,52 @@ def run_code(code: str, language: Literal["python", "js", "ts", "r", "java"]) ->
|
|
17
15
|
raise NotImplementedError("This is only available on the latest agent architecture. Please contact the Letta team.")
|
18
16
|
|
19
17
|
|
20
|
-
async def web_search(
|
18
|
+
async def web_search(
|
19
|
+
query: str,
|
20
|
+
num_results: int = 10,
|
21
|
+
category: Optional[
|
22
|
+
Literal["company", "research paper", "news", "pdf", "github", "tweet", "personal site", "linkedin profile", "financial report"]
|
23
|
+
] = None,
|
24
|
+
include_text: bool = False,
|
25
|
+
include_domains: Optional[List[str]] = None,
|
26
|
+
exclude_domains: Optional[List[str]] = None,
|
27
|
+
start_published_date: Optional[str] = None,
|
28
|
+
end_published_date: Optional[str] = None,
|
29
|
+
user_location: Optional[str] = None,
|
30
|
+
) -> str:
|
21
31
|
"""
|
22
|
-
Search the web
|
32
|
+
Search the web using Exa's AI-powered search engine and retrieve relevant content.
|
23
33
|
|
24
34
|
Examples:
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
)
|
34
|
-
|
35
|
+
web_search("Tesla Q1 2025 earnings report", num_results=5, category="financial report")
|
36
|
+
web_search("Latest research in large language models", category="research paper", include_domains=["arxiv.org", "paperswithcode.com"])
|
37
|
+
web_search("Letta API documentation core_memory_append", num_results=3)
|
38
|
+
|
39
|
+
Args:
|
40
|
+
query (str): The search query to find relevant web content.
|
41
|
+
num_results (int, optional): Number of results to return (1-100). Defaults to 10.
|
42
|
+
category (Optional[Literal], optional): Focus search on specific content types. Defaults to None.
|
43
|
+
include_text (bool, optional): Whether to retrieve full page content. Defaults to False (only returns summary and highlights, since the full text usually will overflow the context window).
|
44
|
+
include_domains (Optional[List[str]], optional): List of domains to include in search results. Defaults to None.
|
45
|
+
exclude_domains (Optional[List[str]], optional): List of domains to exclude from search results. Defaults to None.
|
46
|
+
start_published_date (Optional[str], optional): Only return content published after this date (ISO format). Defaults to None.
|
47
|
+
end_published_date (Optional[str], optional): Only return content published before this date (ISO format). Defaults to None.
|
48
|
+
user_location (Optional[str], optional): Two-letter country code for localized results (e.g., "US"). Defaults to None.
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
str: A JSON-encoded string containing search results with title, URL, content, highlights, and summary.
|
52
|
+
"""
|
53
|
+
raise NotImplementedError("This is only available on the latest agent architecture. Please contact the Letta team.")
|
54
|
+
|
55
|
+
|
56
|
+
async def fetch_webpage(url: str) -> str:
|
57
|
+
"""
|
58
|
+
Fetch a webpage and convert it to markdown/text format using Jina AI reader.
|
35
59
|
|
36
60
|
Args:
|
37
|
-
|
38
|
-
limit (int, optional): Maximum number of URLs to fetch and analyse per task (must be > 0). Defaults to 1.
|
39
|
-
return_raw (bool, optional): If set to True, returns the raw content of the web pages.
|
40
|
-
This should be True unless otherwise specified by the user. Defaults to True.
|
61
|
+
url: The URL of the webpage to fetch and convert
|
41
62
|
|
42
63
|
Returns:
|
43
|
-
|
44
|
-
Each result includes ranked snippets with their source URLs and relevance scores,
|
45
|
-
corresponding to each search task.
|
64
|
+
String containing the webpage content in markdown/text format
|
46
65
|
"""
|
47
66
|
raise NotImplementedError("This is only available on the latest agent architecture. Please contact the Letta team.")
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import asyncio
|
2
2
|
import json
|
3
|
-
import os
|
4
3
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
5
4
|
from typing import TYPE_CHECKING, List
|
6
5
|
|
@@ -138,7 +137,7 @@ def send_message_to_agent_async(self: "Agent", message: str, other_agent_id: str
|
|
138
137
|
Returns:
|
139
138
|
str: A confirmation message indicating the message was successfully sent.
|
140
139
|
"""
|
141
|
-
if
|
140
|
+
if settings.environment == "PRODUCTION":
|
142
141
|
raise RuntimeError("This tool is not allowed to be run on Letta Cloud.")
|
143
142
|
|
144
143
|
message = (
|
@@ -593,7 +593,6 @@ def generate_tool_schema_for_mcp(
|
|
593
593
|
append_heartbeat: bool = True,
|
594
594
|
strict: bool = False,
|
595
595
|
) -> Dict[str, Any]:
|
596
|
-
|
597
596
|
# MCP tool.inputSchema is a JSON schema
|
598
597
|
# https://github.com/modelcontextprotocol/python-sdk/blob/775f87981300660ee957b63c2a14b448ab9c3675/src/mcp/types.py#L678
|
599
598
|
parameters_schema = mcp_tool.inputSchema
|
letta/helpers/converters.py
CHANGED
@@ -2,8 +2,7 @@ from typing import Any, Dict, List, Optional, Union
|
|
2
2
|
|
3
3
|
import numpy as np
|
4
4
|
from anthropic.types.beta.messages import BetaMessageBatch, BetaMessageBatchIndividualResponse
|
5
|
-
from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall
|
6
|
-
from openai.types.chat.chat_completion_message_tool_call import Function as OpenAIFunction
|
5
|
+
from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall, Function as OpenAIFunction
|
7
6
|
from sqlalchemy import Dialect
|
8
7
|
|
9
8
|
from letta.functions.mcp_client.types import StdioServerConfig
|
@@ -39,6 +38,7 @@ from letta.schemas.tool_rule import (
|
|
39
38
|
MaxCountPerStepToolRule,
|
40
39
|
ParentToolRule,
|
41
40
|
RequiredBeforeExitToolRule,
|
41
|
+
RequiresApprovalToolRule,
|
42
42
|
TerminalToolRule,
|
43
43
|
ToolRule,
|
44
44
|
)
|
@@ -91,8 +91,11 @@ def serialize_tool_rules(tool_rules: Optional[List[ToolRule]]) -> List[Dict[str,
|
|
91
91
|
if not tool_rules:
|
92
92
|
return []
|
93
93
|
|
94
|
+
# de-duplicate tool rules using dict.fromkeys (preserves order in Python 3.7+)
|
95
|
+
deduplicated_rules = list(dict.fromkeys(tool_rules))
|
96
|
+
|
94
97
|
data = [
|
95
|
-
{**rule.model_dump(mode="json"), "type": rule.type.value} for rule in
|
98
|
+
{**rule.model_dump(mode="json"), "type": rule.type.value} for rule in deduplicated_rules
|
96
99
|
] # Convert Enum to string for JSON compatibility
|
97
100
|
|
98
101
|
# Validate ToolRule structure
|
@@ -136,6 +139,8 @@ def deserialize_tool_rule(
|
|
136
139
|
return ParentToolRule(**data)
|
137
140
|
elif rule_type == ToolRuleType.required_before_exit:
|
138
141
|
return RequiredBeforeExitToolRule(**data)
|
142
|
+
elif rule_type == ToolRuleType.requires_approval:
|
143
|
+
return RequiresApprovalToolRule(**data)
|
139
144
|
raise ValueError(f"Unknown ToolRule type: {rule_type}")
|
140
145
|
|
141
146
|
|
@@ -1,7 +1,6 @@
|
|
1
1
|
import re
|
2
2
|
import time
|
3
|
-
from datetime import datetime, timedelta
|
4
|
-
from datetime import timezone as dt_timezone
|
3
|
+
from datetime import datetime, timedelta, timezone as dt_timezone
|
5
4
|
from typing import Callable
|
6
5
|
|
7
6
|
import pytz
|
@@ -21,11 +20,13 @@ def datetime_to_timestamp(dt):
|
|
21
20
|
|
22
21
|
def get_local_time_fast(timezone):
|
23
22
|
# Get current UTC time and convert to the specified timezone
|
23
|
+
# Only return the date to avoid cache busting on every request
|
24
24
|
if not timezone:
|
25
|
-
return datetime.now().strftime("%
|
25
|
+
return datetime.now().strftime("%B %d, %Y")
|
26
26
|
current_time_utc = datetime.now(pytz.utc)
|
27
27
|
local_time = current_time_utc.astimezone(pytz.timezone(timezone))
|
28
|
-
|
28
|
+
# Return only the date in a human-readable format (e.g., "June 1, 2021")
|
29
|
+
formatted_time = local_time.strftime("%B %d, %Y")
|
29
30
|
|
30
31
|
return formatted_time
|
31
32
|
|
letta/helpers/message_helper.py
CHANGED
@@ -40,8 +40,7 @@ def _convert_message_create_to_message(
|
|
40
40
|
assert isinstance(message_create, MessageCreate)
|
41
41
|
|
42
42
|
# Extract message content
|
43
|
-
if isinstance(message_create.content, str):
|
44
|
-
assert message_create.content != "", "Message content must not be empty"
|
43
|
+
if isinstance(message_create.content, str) and message_create.content != "":
|
45
44
|
message_content = [TextContent(text=message_create.content)]
|
46
45
|
elif isinstance(message_create.content, list) and len(message_create.content) > 0:
|
47
46
|
message_content = message_create.content
|
letta/helpers/pinecone_utils.py
CHANGED
@@ -317,7 +317,6 @@ async def list_pinecone_index_for_files(file_id: str, actor: User, limit: int =
|
|
317
317
|
async with PineconeAsyncio(api_key=settings.pinecone_api_key) as pc:
|
318
318
|
description = await pc.describe_index(name=settings.pinecone_source_index)
|
319
319
|
async with pc.IndexAsyncio(host=description.index.host) as dense_index:
|
320
|
-
|
321
320
|
kwargs = {"namespace": namespace, "prefix": file_id}
|
322
321
|
if limit is not None:
|
323
322
|
kwargs["limit"] = limit
|
@@ -11,6 +11,7 @@ from letta.schemas.tool_rule import (
|
|
11
11
|
MaxCountPerStepToolRule,
|
12
12
|
ParentToolRule,
|
13
13
|
RequiredBeforeExitToolRule,
|
14
|
+
RequiresApprovalToolRule,
|
14
15
|
TerminalToolRule,
|
15
16
|
ToolRule,
|
16
17
|
)
|
@@ -44,6 +45,9 @@ class ToolRulesSolver(BaseModel):
|
|
44
45
|
required_before_exit_tool_rules: list[RequiredBeforeExitToolRule] = Field(
|
45
46
|
default_factory=list, description="Tool rules that must be called before the agent can exit.", exclude=True
|
46
47
|
)
|
48
|
+
requires_approval_tool_rules: list[RequiresApprovalToolRule] = Field(
|
49
|
+
default_factory=list, description="Tool rules that trigger an approval request for human-in-the-loop.", exclude=True
|
50
|
+
)
|
47
51
|
tool_call_history: list[str] = Field(default_factory=list, description="History of tool calls, updated with each tool call.")
|
48
52
|
|
49
53
|
def __init__(self, tool_rules: list[ToolRule] | None = None, **kwargs):
|
@@ -68,6 +72,8 @@ class ToolRulesSolver(BaseModel):
|
|
68
72
|
self.parent_tool_rules.append(rule)
|
69
73
|
elif isinstance(rule, RequiredBeforeExitToolRule):
|
70
74
|
self.required_before_exit_tool_rules.append(rule)
|
75
|
+
elif isinstance(rule, RequiresApprovalToolRule):
|
76
|
+
self.requires_approval_tool_rules.append(rule)
|
71
77
|
|
72
78
|
def register_tool_call(self, tool_name: str):
|
73
79
|
"""Update the internal state to track tool call history."""
|
@@ -117,6 +123,10 @@ class ToolRulesSolver(BaseModel):
|
|
117
123
|
"""Check if the tool is defined as a continue tool in the tool rules."""
|
118
124
|
return any(rule.tool_name == tool_name for rule in self.continue_tool_rules)
|
119
125
|
|
126
|
+
def is_requires_approval_tool(self, tool_name: ToolName):
|
127
|
+
"""Check if the tool is defined as a requires-approval tool in the tool rules."""
|
128
|
+
return any(rule.tool_name == tool_name for rule in self.requires_approval_tool_rules)
|
129
|
+
|
120
130
|
def has_required_tools_been_called(self, available_tools: set[ToolName]) -> bool:
|
121
131
|
"""Check if all required-before-exit tools have been called."""
|
122
132
|
return len(self.get_uncalled_required_tools(available_tools=available_tools)) == 0
|