letta-nightly 0.11.6.dev20250903104037__py3-none-any.whl → 0.11.7.dev20250904104046__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +10 -14
  3. letta/agents/base_agent.py +18 -0
  4. letta/agents/helpers.py +32 -7
  5. letta/agents/letta_agent.py +953 -762
  6. letta/agents/voice_agent.py +1 -1
  7. letta/client/streaming.py +0 -1
  8. letta/constants.py +11 -8
  9. letta/errors.py +9 -0
  10. letta/functions/function_sets/base.py +77 -69
  11. letta/functions/function_sets/builtin.py +41 -22
  12. letta/functions/function_sets/multi_agent.py +1 -2
  13. letta/functions/schema_generator.py +0 -1
  14. letta/helpers/converters.py +8 -3
  15. letta/helpers/datetime_helpers.py +5 -4
  16. letta/helpers/message_helper.py +1 -2
  17. letta/helpers/pinecone_utils.py +0 -1
  18. letta/helpers/tool_rule_solver.py +10 -0
  19. letta/helpers/tpuf_client.py +848 -0
  20. letta/interface.py +8 -8
  21. letta/interfaces/anthropic_streaming_interface.py +7 -0
  22. letta/interfaces/openai_streaming_interface.py +29 -6
  23. letta/llm_api/anthropic_client.py +188 -18
  24. letta/llm_api/azure_client.py +0 -1
  25. letta/llm_api/bedrock_client.py +1 -2
  26. letta/llm_api/deepseek_client.py +319 -5
  27. letta/llm_api/google_vertex_client.py +75 -17
  28. letta/llm_api/groq_client.py +0 -1
  29. letta/llm_api/helpers.py +2 -2
  30. letta/llm_api/llm_api_tools.py +1 -50
  31. letta/llm_api/llm_client.py +6 -8
  32. letta/llm_api/mistral.py +1 -1
  33. letta/llm_api/openai.py +16 -13
  34. letta/llm_api/openai_client.py +31 -16
  35. letta/llm_api/together_client.py +0 -1
  36. letta/llm_api/xai_client.py +0 -1
  37. letta/local_llm/chat_completion_proxy.py +7 -6
  38. letta/local_llm/settings/settings.py +1 -1
  39. letta/orm/__init__.py +1 -0
  40. letta/orm/agent.py +8 -6
  41. letta/orm/archive.py +9 -1
  42. letta/orm/block.py +3 -4
  43. letta/orm/block_history.py +3 -1
  44. letta/orm/group.py +2 -3
  45. letta/orm/identity.py +1 -2
  46. letta/orm/job.py +1 -2
  47. letta/orm/llm_batch_items.py +1 -2
  48. letta/orm/message.py +8 -4
  49. letta/orm/mixins.py +18 -0
  50. letta/orm/organization.py +2 -0
  51. letta/orm/passage.py +8 -1
  52. letta/orm/passage_tag.py +55 -0
  53. letta/orm/sandbox_config.py +1 -3
  54. letta/orm/step.py +1 -2
  55. letta/orm/tool.py +1 -0
  56. letta/otel/resource.py +2 -2
  57. letta/plugins/plugins.py +1 -1
  58. letta/prompts/prompt_generator.py +10 -2
  59. letta/schemas/agent.py +11 -0
  60. letta/schemas/archive.py +4 -0
  61. letta/schemas/block.py +13 -0
  62. letta/schemas/embedding_config.py +0 -1
  63. letta/schemas/enums.py +24 -7
  64. letta/schemas/group.py +12 -0
  65. letta/schemas/letta_message.py +55 -1
  66. letta/schemas/letta_message_content.py +28 -0
  67. letta/schemas/letta_request.py +21 -4
  68. letta/schemas/letta_stop_reason.py +9 -1
  69. letta/schemas/llm_config.py +24 -8
  70. letta/schemas/mcp.py +0 -3
  71. letta/schemas/memory.py +14 -0
  72. letta/schemas/message.py +245 -141
  73. letta/schemas/openai/chat_completion_request.py +2 -1
  74. letta/schemas/passage.py +1 -0
  75. letta/schemas/providers/bedrock.py +1 -1
  76. letta/schemas/providers/openai.py +2 -2
  77. letta/schemas/tool.py +11 -5
  78. letta/schemas/tool_execution_result.py +0 -1
  79. letta/schemas/tool_rule.py +71 -0
  80. letta/serialize_schemas/marshmallow_agent.py +1 -2
  81. letta/server/rest_api/app.py +3 -3
  82. letta/server/rest_api/auth/index.py +0 -1
  83. letta/server/rest_api/interface.py +3 -11
  84. letta/server/rest_api/redis_stream_manager.py +3 -4
  85. letta/server/rest_api/routers/v1/agents.py +143 -84
  86. letta/server/rest_api/routers/v1/blocks.py +1 -1
  87. letta/server/rest_api/routers/v1/folders.py +1 -1
  88. letta/server/rest_api/routers/v1/groups.py +23 -22
  89. letta/server/rest_api/routers/v1/internal_templates.py +68 -0
  90. letta/server/rest_api/routers/v1/sandbox_configs.py +11 -5
  91. letta/server/rest_api/routers/v1/sources.py +1 -1
  92. letta/server/rest_api/routers/v1/tools.py +167 -15
  93. letta/server/rest_api/streaming_response.py +4 -3
  94. letta/server/rest_api/utils.py +75 -18
  95. letta/server/server.py +24 -35
  96. letta/services/agent_manager.py +359 -45
  97. letta/services/agent_serialization_manager.py +23 -3
  98. letta/services/archive_manager.py +72 -3
  99. letta/services/block_manager.py +1 -2
  100. letta/services/context_window_calculator/token_counter.py +11 -6
  101. letta/services/file_manager.py +1 -3
  102. letta/services/files_agents_manager.py +2 -4
  103. letta/services/group_manager.py +73 -12
  104. letta/services/helpers/agent_manager_helper.py +5 -5
  105. letta/services/identity_manager.py +8 -3
  106. letta/services/job_manager.py +2 -14
  107. letta/services/llm_batch_manager.py +1 -3
  108. letta/services/mcp/base_client.py +1 -2
  109. letta/services/mcp_manager.py +5 -6
  110. letta/services/message_manager.py +536 -15
  111. letta/services/organization_manager.py +1 -2
  112. letta/services/passage_manager.py +287 -12
  113. letta/services/provider_manager.py +1 -3
  114. letta/services/sandbox_config_manager.py +12 -7
  115. letta/services/source_manager.py +1 -2
  116. letta/services/step_manager.py +0 -1
  117. letta/services/summarizer/summarizer.py +4 -2
  118. letta/services/telemetry_manager.py +1 -3
  119. letta/services/tool_executor/builtin_tool_executor.py +136 -316
  120. letta/services/tool_executor/core_tool_executor.py +231 -74
  121. letta/services/tool_executor/files_tool_executor.py +2 -2
  122. letta/services/tool_executor/mcp_tool_executor.py +0 -1
  123. letta/services/tool_executor/multi_agent_tool_executor.py +2 -2
  124. letta/services/tool_executor/sandbox_tool_executor.py +0 -1
  125. letta/services/tool_executor/tool_execution_sandbox.py +2 -3
  126. letta/services/tool_manager.py +181 -64
  127. letta/services/tool_sandbox/modal_deployment_manager.py +2 -2
  128. letta/services/user_manager.py +1 -2
  129. letta/settings.py +5 -3
  130. letta/streaming_interface.py +3 -3
  131. letta/system.py +1 -1
  132. letta/utils.py +0 -1
  133. {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/METADATA +11 -7
  134. {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/RECORD +137 -135
  135. letta/llm_api/deepseek.py +0 -303
  136. {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/WHEEL +0 -0
  137. {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/entry_points.txt +0 -0
  138. {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/licenses/LICENSE +0 -0
@@ -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.list_passages_async(
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": 400000,
214
- "gpt-5-2025-08-07": 400000,
215
- "gpt-5-mini": 400000,
216
- "gpt-5-mini-2025-08-07": 400000,
217
- "gpt-5-nano": 400000,
218
- "gpt-5-nano-2025-08-07": 400000,
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 = 1000000 # very high (we rely on implementation)
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(self: "Agent", query: str, page: Optional[int] = 0) -> Optional[str]:
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 case-insensitive string matching.
32
+ Search prior conversation history using hybrid search (text + semantic similarity).
26
33
 
27
34
  Args:
28
- query (str): String to search for.
29
- page (int): Allows you to page through results. Only use on a follow-up query. Defaults to 0 (first page).
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
- if page is None or (isinstance(page, str) and page.lower().strip() == "none"):
41
- page = 0
42
- try:
43
- page = int(page)
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
- limit=count,
69
+ roles=roles,
70
+ limit=limit,
54
71
  )
55
- total = len(messages)
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"Showing {len(messages)} of {total} results (page {page}/{num_pages}):"
61
- results_formatted = [message.content[0].text for message in messages]
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
- await self.passage_manager.insert_passage(
77
- agent_state=self.agent_state,
78
- text=content,
79
- actor=self.user,
80
- )
81
- self.agent_manager.rebuild_system_prompt(agent_id=self.agent_state.id, actor=self.user, force=True)
82
- return None
83
-
84
-
85
- async def archival_memory_search(self: "Agent", query: str, page: Optional[int] = 0, start: Optional[int] = 0) -> Optional[str]:
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
- page (Optional[int]): Allows you to page through results. Only use on a follow-up query. Defaults to 0 (first page).
92
- start (Optional[int]): Starting index for the search results. Defaults to 0.
93
-
94
- Returns:
95
- str: Query result string
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
- from letta.constants import RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
99
-
100
- if page is None or (isinstance(page, str) and page.lower().strip() == "none"):
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
- # Apply pagination
120
- end = min(count + start, len(all_results))
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
- # Format results to match previous implementation
124
- formatted_results = [{"timestamp": str(result.created_at), "content": result.text} for result in paged_results]
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
- return formatted_results, len(formatted_results)
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
- except Exception as e:
129
- raise e
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(tasks: List[SearchTask], limit: int = 1, return_raw: bool = True) -> str:
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 with a list of query/question pairs and extract passages that answer the corresponding questions.
32
+ Search the web using Exa's AI-powered search engine and retrieve relevant content.
23
33
 
24
34
  Examples:
25
- tasks -> [
26
- SearchTask(
27
- query="Tesla Q1 2025 earnings report PDF",
28
- question="What was Tesla's net profit in Q1 2025?"
29
- ),
30
- SearchTask(
31
- query="Letta API prebuilt tools core_memory_append",
32
- question="What does the core_memory_append tool do in Letta?"
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
- tasks (List[SearchTask]): A list of search tasks, each containing a `query` and a corresponding `question`.
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
- str: A JSON-encoded string containing a list of search results.
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 os.getenv("LETTA_ENVIRONMENT") == "PRODUCTION":
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
@@ -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 tool_rules
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("%Y-%m-%d %I:%M:%S %p %Z%z")
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
- formatted_time = local_time.strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
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
 
@@ -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
@@ -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