letta-nightly 0.8.4.dev20250619104255__py3-none-any.whl → 0.8.5.dev20250620104328__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/agents/letta_agent.py +54 -20
- letta/agents/voice_agent.py +47 -31
- letta/constants.py +1 -1
- letta/data_sources/redis_client.py +11 -6
- letta/functions/function_sets/builtin.py +35 -11
- letta/functions/prompts.py +26 -0
- letta/functions/types.py +6 -0
- letta/interfaces/openai_chat_completions_streaming_interface.py +0 -1
- letta/llm_api/anthropic.py +9 -1
- letta/llm_api/anthropic_client.py +8 -11
- letta/llm_api/aws_bedrock.py +10 -6
- letta/llm_api/llm_api_tools.py +3 -0
- letta/llm_api/openai_client.py +1 -1
- letta/orm/agent.py +14 -1
- letta/orm/job.py +3 -0
- letta/orm/provider.py +3 -1
- letta/schemas/agent.py +7 -0
- letta/schemas/embedding_config.py +8 -0
- letta/schemas/enums.py +0 -1
- letta/schemas/job.py +1 -0
- letta/schemas/providers.py +13 -5
- letta/server/rest_api/routers/v1/agents.py +76 -35
- letta/server/rest_api/routers/v1/providers.py +7 -7
- letta/server/rest_api/routers/v1/sources.py +39 -19
- letta/server/rest_api/routers/v1/tools.py +96 -31
- letta/services/agent_manager.py +8 -2
- letta/services/file_processor/chunker/llama_index_chunker.py +89 -1
- letta/services/file_processor/embedder/openai_embedder.py +6 -1
- letta/services/file_processor/parser/mistral_parser.py +2 -2
- letta/services/helpers/agent_manager_helper.py +44 -16
- letta/services/job_manager.py +35 -17
- letta/services/mcp/base_client.py +26 -1
- letta/services/mcp_manager.py +33 -18
- letta/services/provider_manager.py +30 -0
- letta/services/tool_executor/builtin_tool_executor.py +335 -43
- letta/services/tool_manager.py +25 -1
- letta/services/user_manager.py +1 -1
- letta/settings.py +3 -0
- {letta_nightly-0.8.4.dev20250619104255.dist-info → letta_nightly-0.8.5.dev20250620104328.dist-info}/METADATA +4 -3
- {letta_nightly-0.8.4.dev20250619104255.dist-info → letta_nightly-0.8.5.dev20250620104328.dist-info}/RECORD +44 -42
- {letta_nightly-0.8.4.dev20250619104255.dist-info → letta_nightly-0.8.5.dev20250620104328.dist-info}/LICENSE +0 -0
- {letta_nightly-0.8.4.dev20250619104255.dist-info → letta_nightly-0.8.5.dev20250620104328.dist-info}/WHEEL +0 -0
- {letta_nightly-0.8.4.dev20250619104255.dist-info → letta_nightly-0.8.5.dev20250620104328.dist-info}/entry_points.txt +0 -0
@@ -1,8 +1,13 @@
|
|
1
|
+
import asyncio
|
1
2
|
import json
|
2
|
-
|
3
|
-
from typing import Any, Dict, Literal, Optional
|
3
|
+
import time
|
4
|
+
from typing import Any, Dict, List, Literal, Optional
|
4
5
|
|
5
|
-
from
|
6
|
+
from pydantic import BaseModel
|
7
|
+
|
8
|
+
from letta.functions.prompts import FIRECRAWL_SEARCH_SYSTEM_PROMPT, get_firecrawl_search_user_prompt
|
9
|
+
from letta.functions.types import SearchTask
|
10
|
+
from letta.log import get_logger
|
6
11
|
from letta.otel.tracing import trace_method
|
7
12
|
from letta.schemas.agent import AgentState
|
8
13
|
from letta.schemas.sandbox_config import SandboxConfig
|
@@ -10,7 +15,34 @@ from letta.schemas.tool import Tool
|
|
10
15
|
from letta.schemas.tool_execution_result import ToolExecutionResult
|
11
16
|
from letta.schemas.user import User
|
12
17
|
from letta.services.tool_executor.tool_executor_base import ToolExecutor
|
13
|
-
from letta.settings import tool_settings
|
18
|
+
from letta.settings import model_settings, tool_settings
|
19
|
+
|
20
|
+
logger = get_logger(__name__)
|
21
|
+
|
22
|
+
|
23
|
+
class Citation(BaseModel):
|
24
|
+
"""A relevant text snippet identified by line numbers in a document."""
|
25
|
+
|
26
|
+
start_line: int # Starting line number (1-indexed)
|
27
|
+
end_line: int # Ending line number (1-indexed, inclusive)
|
28
|
+
|
29
|
+
|
30
|
+
class CitationWithText(BaseModel):
|
31
|
+
"""A citation with the actual extracted text."""
|
32
|
+
|
33
|
+
text: str # The actual extracted text from the lines
|
34
|
+
|
35
|
+
|
36
|
+
class DocumentAnalysis(BaseModel):
|
37
|
+
"""Analysis of a document's relevance to a search question."""
|
38
|
+
|
39
|
+
citations: List[Citation]
|
40
|
+
|
41
|
+
|
42
|
+
class DocumentAnalysisWithText(BaseModel):
|
43
|
+
"""Analysis with extracted text from line citations."""
|
44
|
+
|
45
|
+
citations: List[CitationWithText]
|
14
46
|
|
15
47
|
|
16
48
|
class LettaBuiltinToolExecutor(ToolExecutor):
|
@@ -34,7 +66,7 @@ class LettaBuiltinToolExecutor(ToolExecutor):
|
|
34
66
|
|
35
67
|
# Execute the appropriate function
|
36
68
|
function_args_copy = function_args.copy() # Make a copy to avoid modifying the original
|
37
|
-
function_response = await function_map[function_name](**function_args_copy)
|
69
|
+
function_response = await function_map[function_name](agent_state=agent_state, **function_args_copy)
|
38
70
|
|
39
71
|
return ToolExecutionResult(
|
40
72
|
status="success",
|
@@ -42,7 +74,7 @@ class LettaBuiltinToolExecutor(ToolExecutor):
|
|
42
74
|
agent_state=agent_state,
|
43
75
|
)
|
44
76
|
|
45
|
-
async def run_code(self, code: str, language: Literal["python", "js", "ts", "r", "java"]) -> str:
|
77
|
+
async def run_code(self, agent_state: "AgentState", code: str, language: Literal["python", "js", "ts", "r", "java"]) -> str:
|
46
78
|
from e2b_code_interpreter import AsyncSandbox
|
47
79
|
|
48
80
|
if tool_settings.e2b_api_key is None:
|
@@ -70,48 +102,308 @@ class LettaBuiltinToolExecutor(ToolExecutor):
|
|
70
102
|
out["error"] = err
|
71
103
|
return out
|
72
104
|
|
73
|
-
|
105
|
+
@trace_method
|
106
|
+
async def web_search(
|
107
|
+
self,
|
108
|
+
agent_state: "AgentState",
|
109
|
+
tasks: List[SearchTask],
|
110
|
+
limit: int = 3,
|
111
|
+
return_raw: bool = False,
|
112
|
+
) -> str:
|
74
113
|
"""
|
75
|
-
Search the web
|
114
|
+
Search the web with a list of query/question pairs and extract passages that answer the corresponding questions.
|
115
|
+
|
116
|
+
Examples:
|
117
|
+
tasks -> [
|
118
|
+
SearchTask(
|
119
|
+
query="Tesla Q1 2025 earnings report PDF",
|
120
|
+
question="What was Tesla's net profit in Q1 2025?"
|
121
|
+
),
|
122
|
+
SearchTask(
|
123
|
+
query="Letta API prebuilt tools core_memory_append",
|
124
|
+
question="What does the core_memory_append tool do in Letta?"
|
125
|
+
)
|
126
|
+
]
|
127
|
+
|
76
128
|
Args:
|
77
|
-
|
129
|
+
tasks (List[SearchTask]): A list of search tasks, each containing a `query` and a corresponding `question`.
|
130
|
+
limit (int, optional): Maximum number of URLs to fetch and analyse per task (must be > 0). Defaults to 3.
|
131
|
+
return_raw (bool, optional): If set to True, returns the raw content of the web pages.
|
132
|
+
This should be False unless otherwise specified by the user. Defaults to False.
|
133
|
+
|
78
134
|
Returns:
|
79
|
-
str:
|
135
|
+
str: A JSON-encoded string containing a list of search results.
|
136
|
+
Each result includes ranked snippets with their source URLs and relevance scores,
|
137
|
+
corresponding to each search task.
|
80
138
|
"""
|
81
|
-
|
139
|
+
# TODO: Temporary, maybe deprecate this field?
|
140
|
+
if return_raw:
|
141
|
+
logger.warning("WARNING! return_raw was set to True, we default to False always. Deprecate this field.")
|
142
|
+
return_raw = False
|
82
143
|
try:
|
83
|
-
from
|
144
|
+
from firecrawl import AsyncFirecrawlApp
|
84
145
|
except ImportError:
|
85
|
-
raise ImportError("
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
146
|
+
raise ImportError("firecrawl-py is not installed in the tool execution environment")
|
147
|
+
|
148
|
+
if not tasks:
|
149
|
+
return json.dumps({"error": "No search tasks provided."})
|
150
|
+
|
151
|
+
# Convert dict objects to SearchTask objects
|
152
|
+
search_tasks = []
|
153
|
+
for task in tasks:
|
154
|
+
if isinstance(task, dict):
|
155
|
+
search_tasks.append(SearchTask(**task))
|
156
|
+
else:
|
157
|
+
search_tasks.append(task)
|
158
|
+
|
159
|
+
logger.info(f"[DEBUG] Starting web search with {len(search_tasks)} tasks, limit={limit}, return_raw={return_raw}")
|
160
|
+
|
161
|
+
# Check if the API key exists on the agent state
|
162
|
+
agent_state_tool_env_vars = agent_state.get_agent_env_vars_as_dict()
|
163
|
+
firecrawl_api_key = agent_state_tool_env_vars.get("FIRECRAWL_API_KEY") or tool_settings.firecrawl_api_key
|
164
|
+
if not firecrawl_api_key:
|
165
|
+
raise ValueError("FIRECRAWL_API_KEY is not set in environment or on agent_state tool exec environment variables.")
|
166
|
+
|
167
|
+
# Track which API key source was used
|
168
|
+
api_key_source = "agent_environment" if agent_state_tool_env_vars.get("FIRECRAWL_API_KEY") else "system_settings"
|
169
|
+
|
170
|
+
if limit <= 0:
|
171
|
+
raise ValueError("limit must be greater than 0")
|
172
|
+
|
173
|
+
# Initialize Firecrawl client
|
174
|
+
app = AsyncFirecrawlApp(api_key=firecrawl_api_key)
|
175
|
+
|
176
|
+
# Process all search tasks in parallel
|
177
|
+
search_task_coroutines = [self._process_single_search_task(app, task, limit, return_raw, api_key_source) for task in search_tasks]
|
178
|
+
|
179
|
+
# Execute all searches concurrently
|
180
|
+
search_results = await asyncio.gather(*search_task_coroutines, return_exceptions=True)
|
181
|
+
|
182
|
+
# Build final response as a mapping of query -> result
|
183
|
+
final_results = {}
|
184
|
+
successful_tasks = 0
|
185
|
+
failed_tasks = 0
|
186
|
+
|
187
|
+
for i, result in enumerate(search_results):
|
188
|
+
query = search_tasks[i].query
|
189
|
+
if isinstance(result, Exception):
|
190
|
+
logger.error(f"Search task {i} failed: {result}")
|
191
|
+
failed_tasks += 1
|
192
|
+
final_results[query] = {"query": query, "question": search_tasks[i].question, "error": str(result)}
|
113
193
|
else:
|
114
|
-
|
115
|
-
|
194
|
+
successful_tasks += 1
|
195
|
+
final_results[query] = result
|
196
|
+
|
197
|
+
logger.info(f"[DEBUG] Web search completed: {successful_tasks} successful, {failed_tasks} failed")
|
198
|
+
|
199
|
+
# Build final response with api_key_source at top level
|
200
|
+
response = {"api_key_source": api_key_source, "results": final_results}
|
201
|
+
|
202
|
+
return json.dumps(response, indent=2, ensure_ascii=False)
|
203
|
+
|
204
|
+
@trace_method
|
205
|
+
async def _process_single_search_task(
|
206
|
+
self, app: "AsyncFirecrawlApp", task: SearchTask, limit: int, return_raw: bool, api_key_source: str
|
207
|
+
) -> Dict[str, Any]:
|
208
|
+
"""Process a single search task."""
|
209
|
+
from firecrawl import ScrapeOptions
|
210
|
+
|
211
|
+
logger.info(f"[DEBUG] Starting Firecrawl search for query: '{task.query}' with limit={limit}")
|
212
|
+
|
213
|
+
# Perform the search for this task
|
214
|
+
search_result = await app.search(task.query, limit=limit, scrape_options=ScrapeOptions(formats=["markdown"]))
|
215
|
+
|
216
|
+
logger.info(
|
217
|
+
f"[DEBUG] Firecrawl search completed for '{task.query}': {len(search_result.get('data', [])) if search_result else 0} results"
|
218
|
+
)
|
219
|
+
|
220
|
+
if not search_result or not search_result.get("data"):
|
221
|
+
return {"query": task.query, "question": task.question, "error": "No search results found."}
|
222
|
+
|
223
|
+
# If raw results requested, return them directly
|
224
|
+
if return_raw:
|
225
|
+
return {"query": task.query, "question": task.question, "raw_results": search_result}
|
226
|
+
|
227
|
+
# Check if OpenAI API key is available for semantic parsing
|
228
|
+
if model_settings.openai_api_key:
|
229
|
+
try:
|
230
|
+
from openai import AsyncOpenAI
|
231
|
+
|
232
|
+
logger.info(f"[DEBUG] Starting OpenAI analysis for '{task.query}'")
|
233
|
+
|
234
|
+
# Initialize OpenAI client
|
235
|
+
client = AsyncOpenAI(
|
236
|
+
api_key=model_settings.openai_api_key,
|
237
|
+
)
|
238
|
+
|
239
|
+
# Process each result with OpenAI concurrently
|
240
|
+
analysis_tasks = []
|
241
|
+
results_with_markdown = []
|
242
|
+
results_without_markdown = []
|
243
|
+
|
244
|
+
for result in search_result.get("data"):
|
245
|
+
if result.get("markdown"):
|
246
|
+
# Create async task for OpenAI analysis
|
247
|
+
analysis_task = self._analyze_document_with_openai(client, result["markdown"], task.query, task.question)
|
248
|
+
analysis_tasks.append(analysis_task)
|
249
|
+
results_with_markdown.append(result)
|
250
|
+
else:
|
251
|
+
results_without_markdown.append(result)
|
252
|
+
|
253
|
+
logger.info(f"[DEBUG] Starting parallel OpenAI analysis of {len(analysis_tasks)} documents for '{task.query}'")
|
254
|
+
|
255
|
+
# Fire off all OpenAI requests concurrently
|
256
|
+
analyses = await asyncio.gather(*analysis_tasks, return_exceptions=True)
|
257
|
+
|
258
|
+
logger.info(f"[DEBUG] Completed parallel OpenAI analysis of {len(analyses)} documents for '{task.query}'")
|
259
|
+
|
260
|
+
# Build processed results
|
261
|
+
processed_results = []
|
262
|
+
|
263
|
+
# Check if any analysis failed - if so, fall back to raw results
|
264
|
+
for result, analysis in zip(results_with_markdown, analyses):
|
265
|
+
if isinstance(analysis, Exception) or analysis is None:
|
266
|
+
logger.error(f"Analysis failed for {result.get('url')}, falling back to raw results")
|
267
|
+
return {"query": task.query, "question": task.question, "raw_results": search_result}
|
268
|
+
|
269
|
+
# All analyses succeeded, build processed results
|
270
|
+
for result, analysis in zip(results_with_markdown, analyses):
|
271
|
+
# Extract actual text from line number citations
|
272
|
+
analysis_with_text = None
|
273
|
+
if analysis and analysis.citations:
|
274
|
+
analysis_with_text = self._extract_text_from_line_citations(analysis, result["markdown"])
|
275
|
+
|
276
|
+
processed_results.append(
|
277
|
+
{
|
278
|
+
"url": result.get("url"),
|
279
|
+
"title": result.get("title"),
|
280
|
+
"description": result.get("description"),
|
281
|
+
"analysis": analysis_with_text.model_dump() if analysis_with_text else None,
|
282
|
+
}
|
283
|
+
)
|
284
|
+
|
285
|
+
# Add results without markdown
|
286
|
+
for result in results_without_markdown:
|
287
|
+
processed_results.append(
|
288
|
+
{"url": result.get("url"), "title": result.get("title"), "description": result.get("description"), "analysis": None}
|
289
|
+
)
|
290
|
+
|
291
|
+
# Build final response for this task
|
292
|
+
return self._build_final_response_dict(processed_results, task.query, task.question)
|
293
|
+
except Exception as e:
|
294
|
+
# Log error but continue with raw results
|
295
|
+
logger.error(f"Error with OpenAI processing for task '{task.query}': {e}")
|
296
|
+
|
297
|
+
# Return raw search results if OpenAI processing isn't available or fails
|
298
|
+
return {"query": task.query, "question": task.question, "raw_results": search_result}
|
299
|
+
|
300
|
+
@trace_method
|
301
|
+
async def _analyze_document_with_openai(self, client, markdown_content: str, query: str, question: str) -> Optional[DocumentAnalysis]:
|
302
|
+
"""Use OpenAI to analyze a document and extract relevant passages using line numbers."""
|
303
|
+
original_length = len(markdown_content)
|
304
|
+
|
305
|
+
# Create numbered markdown for the LLM to reference
|
306
|
+
numbered_lines = markdown_content.split("\n")
|
307
|
+
numbered_markdown = "\n".join([f"{i+1:4d}: {line}" for i, line in enumerate(numbered_lines)])
|
308
|
+
|
309
|
+
# Truncate if too long
|
310
|
+
max_content_length = 200000
|
311
|
+
truncated = False
|
312
|
+
if len(numbered_markdown) > max_content_length:
|
313
|
+
numbered_markdown = numbered_markdown[:max_content_length] + "..."
|
314
|
+
truncated = True
|
315
|
+
|
316
|
+
user_prompt = get_firecrawl_search_user_prompt(query, question, numbered_markdown)
|
317
|
+
|
318
|
+
logger.info(
|
319
|
+
f"[DEBUG] Starting OpenAI request with line numbers - Query: '{query}', Content: {original_length} chars (truncated: {truncated})"
|
320
|
+
)
|
321
|
+
|
322
|
+
# Time the OpenAI request
|
323
|
+
start_time = time.time()
|
324
|
+
|
325
|
+
response = await client.beta.chat.completions.parse(
|
326
|
+
model="gpt-4.1-mini-2025-04-14",
|
327
|
+
messages=[{"role": "system", "content": FIRECRAWL_SEARCH_SYSTEM_PROMPT}, {"role": "user", "content": user_prompt}],
|
328
|
+
response_format=DocumentAnalysis,
|
329
|
+
temperature=0.1,
|
330
|
+
max_tokens=300, # Limit output tokens - only need line numbers
|
331
|
+
)
|
332
|
+
|
333
|
+
end_time = time.time()
|
334
|
+
request_duration = end_time - start_time
|
335
|
+
|
336
|
+
# Get usage statistics and output length
|
337
|
+
usage = response.usage
|
338
|
+
parsed_result = response.choices[0].message.parsed
|
339
|
+
num_citations = len(parsed_result.citations) if parsed_result else 0
|
340
|
+
|
341
|
+
# Calculate output length (minimal now - just line numbers)
|
342
|
+
output_length = 0
|
343
|
+
if parsed_result and parsed_result.citations:
|
344
|
+
for citation in parsed_result.citations:
|
345
|
+
output_length += 20 # ~20 chars for line numbers only
|
346
|
+
|
347
|
+
logger.info(f"[TIMING] OpenAI request completed in {request_duration:.2f}s - Query: '{query}'")
|
348
|
+
logger.info(f"[TOKENS] Total: {usage.total_tokens} (prompt: {usage.prompt_tokens}, completion: {usage.completion_tokens})")
|
349
|
+
logger.info(f"[OUTPUT] Citations: {num_citations}, Output chars: {output_length} (line-number based)")
|
350
|
+
|
351
|
+
return parsed_result
|
352
|
+
|
353
|
+
def _extract_text_from_line_citations(self, analysis: DocumentAnalysis, original_markdown: str) -> DocumentAnalysisWithText:
|
354
|
+
"""Extract actual text from line number citations."""
|
355
|
+
lines = original_markdown.split("\n")
|
356
|
+
citations_with_text = []
|
357
|
+
|
358
|
+
for citation in analysis.citations:
|
359
|
+
try:
|
360
|
+
# Convert to 0-indexed and ensure bounds
|
361
|
+
start_idx = max(0, citation.start_line - 1)
|
362
|
+
end_idx = min(len(lines), citation.end_line)
|
363
|
+
|
364
|
+
# Extract the lines
|
365
|
+
extracted_lines = lines[start_idx:end_idx]
|
366
|
+
extracted_text = "\n".join(extracted_lines)
|
367
|
+
|
368
|
+
citations_with_text.append(CitationWithText(text=extracted_text))
|
369
|
+
|
370
|
+
except Exception as e:
|
371
|
+
logger.info(f"[DEBUG] Failed to extract text for citation lines {citation.start_line}-{citation.end_line}: {e}")
|
372
|
+
# Fall back to including the citation with empty text
|
373
|
+
citations_with_text.append(CitationWithText(text=""))
|
374
|
+
|
375
|
+
return DocumentAnalysisWithText(citations=citations_with_text)
|
376
|
+
|
377
|
+
@trace_method
|
378
|
+
def _build_final_response_dict(self, processed_results: List[Dict], query: str, question: str) -> Dict[str, Any]:
|
379
|
+
"""Build the final response dictionary from all processed results."""
|
380
|
+
|
381
|
+
# Build sources array
|
382
|
+
sources = []
|
383
|
+
total_snippets = 0
|
384
|
+
|
385
|
+
for result in processed_results:
|
386
|
+
source = {"url": result.get("url"), "title": result.get("title"), "description": result.get("description")}
|
387
|
+
|
388
|
+
if result.get("analysis") and result["analysis"].get("citations"):
|
389
|
+
analysis = result["analysis"]
|
390
|
+
source["citations"] = analysis["citations"]
|
391
|
+
total_snippets += len(analysis["citations"])
|
392
|
+
else:
|
393
|
+
source["citations"] = []
|
394
|
+
|
395
|
+
sources.append(source)
|
396
|
+
|
397
|
+
# Build final response structure
|
398
|
+
response = {
|
399
|
+
"query": query,
|
400
|
+
"question": question,
|
401
|
+
"total_sources": len(sources),
|
402
|
+
"total_citations": total_snippets,
|
403
|
+
"sources": sources,
|
404
|
+
}
|
405
|
+
|
406
|
+
if total_snippets == 0:
|
407
|
+
response["message"] = "No relevant passages found that directly answer the question."
|
116
408
|
|
117
|
-
return
|
409
|
+
return response
|
letta/services/tool_manager.py
CHANGED
@@ -239,7 +239,31 @@ class ToolManager:
|
|
239
239
|
|
240
240
|
@enforce_types
|
241
241
|
@trace_method
|
242
|
-
async def list_tools_async(
|
242
|
+
async def list_tools_async(
|
243
|
+
self, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50, upsert_base_tools: bool = True
|
244
|
+
) -> List[PydanticTool]:
|
245
|
+
"""List all tools with optional pagination."""
|
246
|
+
tools = await self._list_tools_async(actor=actor, after=after, limit=limit)
|
247
|
+
|
248
|
+
# Check if all base tools are present if we requested all the tools w/o cursor
|
249
|
+
# TODO: This is a temporary hack to resolve this issue
|
250
|
+
# TODO: This requires a deeper rethink about how we keep all our internal tools up-to-date
|
251
|
+
if not after and upsert_base_tools:
|
252
|
+
existing_tool_names = {tool.name for tool in tools}
|
253
|
+
missing_base_tools = LETTA_TOOL_SET - existing_tool_names
|
254
|
+
|
255
|
+
# If any base tools are missing, upsert all base tools
|
256
|
+
if missing_base_tools:
|
257
|
+
logger.info(f"Missing base tools detected: {missing_base_tools}. Upserting all base tools.")
|
258
|
+
await self.upsert_base_tools_async(actor=actor)
|
259
|
+
# Re-fetch the tools list after upserting base tools
|
260
|
+
tools = await self._list_tools_async(actor=actor, after=after, limit=limit)
|
261
|
+
|
262
|
+
return tools
|
263
|
+
|
264
|
+
@enforce_types
|
265
|
+
@trace_method
|
266
|
+
async def _list_tools_async(self, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50) -> List[PydanticTool]:
|
243
267
|
"""List all tools with optional pagination."""
|
244
268
|
tools_to_delete = []
|
245
269
|
async with db_registry.async_session() as session:
|
letta/services/user_manager.py
CHANGED
@@ -13,8 +13,8 @@ from letta.otel.tracing import trace_method
|
|
13
13
|
from letta.schemas.user import User as PydanticUser
|
14
14
|
from letta.schemas.user import UserUpdate
|
15
15
|
from letta.server.db import db_registry
|
16
|
-
from letta.utils import enforce_types
|
17
16
|
from letta.settings import settings
|
17
|
+
from letta.utils import enforce_types
|
18
18
|
|
19
19
|
logger = get_logger(__name__)
|
20
20
|
|
letta/settings.py
CHANGED
@@ -18,6 +18,9 @@ class ToolSettings(BaseSettings):
|
|
18
18
|
# Tavily search
|
19
19
|
tavily_api_key: Optional[str] = None
|
20
20
|
|
21
|
+
# Firecrawl search
|
22
|
+
firecrawl_api_key: Optional[str] = None
|
23
|
+
|
21
24
|
# Local Sandbox configurations
|
22
25
|
tool_exec_dir: Optional[str] = None
|
23
26
|
tool_sandbox_timeout: float = 180
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: letta-nightly
|
3
|
-
Version: 0.8.
|
3
|
+
Version: 0.8.5.dev20250620104328
|
4
4
|
Summary: Create LLM agents with long-term memory and custom tools
|
5
5
|
License: Apache License
|
6
6
|
Author: Letta Team
|
@@ -34,6 +34,7 @@ Requires-Dist: autoflake (>=2.3.0,<3.0.0) ; extra == "dev" or extra == "all"
|
|
34
34
|
Requires-Dist: black[jupyter] (>=24.2.0,<25.0.0) ; extra == "dev" or extra == "all"
|
35
35
|
Requires-Dist: boto3 (>=1.36.24,<2.0.0) ; extra == "bedrock"
|
36
36
|
Requires-Dist: brotli (>=1.1.0,<2.0.0)
|
37
|
+
Requires-Dist: certifi (>=2025.6.15,<2026.0.0)
|
37
38
|
Requires-Dist: colorama (>=0.4.6,<0.5.0)
|
38
39
|
Requires-Dist: composio-core (>=0.7.7,<0.8.0)
|
39
40
|
Requires-Dist: datamodel-code-generator[http] (>=0.25.0,<0.26.0)
|
@@ -43,7 +44,7 @@ Requires-Dist: docstring-parser (>=0.16,<0.17)
|
|
43
44
|
Requires-Dist: e2b-code-interpreter (>=1.0.3,<2.0.0) ; extra == "cloud-tool-sandbox"
|
44
45
|
Requires-Dist: faker (>=36.1.0,<37.0.0)
|
45
46
|
Requires-Dist: fastapi (>=0.115.6,<0.116.0) ; extra == "server" or extra == "desktop" or extra == "all"
|
46
|
-
Requires-Dist: firecrawl-py (>=
|
47
|
+
Requires-Dist: firecrawl-py (>=2.8.0,<3.0.0) ; extra == "external-tools"
|
47
48
|
Requires-Dist: google-genai (>=1.15.0,<2.0.0) ; extra == "google"
|
48
49
|
Requires-Dist: granian[reload,uvloop] (>=2.3.2,<3.0.0) ; extra == "experimental" or extra == "all"
|
49
50
|
Requires-Dist: grpcio (>=1.68.1,<2.0.0)
|
@@ -55,7 +56,7 @@ Requires-Dist: isort (>=5.13.2,<6.0.0) ; extra == "dev" or extra == "all"
|
|
55
56
|
Requires-Dist: jinja2 (>=3.1.5,<4.0.0)
|
56
57
|
Requires-Dist: langchain (>=0.3.7,<0.4.0) ; extra == "external-tools" or extra == "desktop" or extra == "all"
|
57
58
|
Requires-Dist: langchain-community (>=0.3.7,<0.4.0) ; extra == "external-tools" or extra == "desktop" or extra == "all"
|
58
|
-
Requires-Dist: letta_client (>=0.1.
|
59
|
+
Requires-Dist: letta_client (>=0.1.160,<0.2.0)
|
59
60
|
Requires-Dist: llama-index (>=0.12.2,<0.13.0)
|
60
61
|
Requires-Dist: llama-index-embeddings-openai (>=0.3.1,<0.4.0)
|
61
62
|
Requires-Dist: locust (>=2.31.5,<3.0.0) ; extra == "dev" or extra == "desktop" or extra == "all"
|