letta-nightly 0.8.4.dev20250619104255__py3-none-any.whl → 0.8.5.dev20250619180801__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 (44) hide show
  1. letta/__init__.py +1 -1
  2. letta/agents/letta_agent.py +54 -20
  3. letta/agents/voice_agent.py +47 -31
  4. letta/constants.py +1 -1
  5. letta/data_sources/redis_client.py +11 -6
  6. letta/functions/function_sets/builtin.py +35 -11
  7. letta/functions/prompts.py +26 -0
  8. letta/functions/types.py +6 -0
  9. letta/interfaces/openai_chat_completions_streaming_interface.py +0 -1
  10. letta/llm_api/anthropic.py +9 -1
  11. letta/llm_api/anthropic_client.py +8 -11
  12. letta/llm_api/aws_bedrock.py +10 -6
  13. letta/llm_api/llm_api_tools.py +3 -0
  14. letta/llm_api/openai_client.py +1 -1
  15. letta/orm/agent.py +14 -1
  16. letta/orm/job.py +3 -0
  17. letta/orm/provider.py +3 -1
  18. letta/schemas/agent.py +7 -0
  19. letta/schemas/embedding_config.py +8 -0
  20. letta/schemas/enums.py +0 -1
  21. letta/schemas/job.py +1 -0
  22. letta/schemas/providers.py +13 -5
  23. letta/server/rest_api/routers/v1/agents.py +76 -35
  24. letta/server/rest_api/routers/v1/providers.py +7 -7
  25. letta/server/rest_api/routers/v1/sources.py +39 -19
  26. letta/server/rest_api/routers/v1/tools.py +96 -31
  27. letta/services/agent_manager.py +8 -2
  28. letta/services/file_processor/chunker/llama_index_chunker.py +89 -1
  29. letta/services/file_processor/embedder/openai_embedder.py +6 -1
  30. letta/services/file_processor/parser/mistral_parser.py +2 -2
  31. letta/services/helpers/agent_manager_helper.py +44 -16
  32. letta/services/job_manager.py +35 -17
  33. letta/services/mcp/base_client.py +26 -1
  34. letta/services/mcp_manager.py +33 -18
  35. letta/services/provider_manager.py +30 -0
  36. letta/services/tool_executor/builtin_tool_executor.py +335 -43
  37. letta/services/tool_manager.py +25 -1
  38. letta/services/user_manager.py +1 -1
  39. letta/settings.py +3 -0
  40. {letta_nightly-0.8.4.dev20250619104255.dist-info → letta_nightly-0.8.5.dev20250619180801.dist-info}/METADATA +4 -3
  41. {letta_nightly-0.8.4.dev20250619104255.dist-info → letta_nightly-0.8.5.dev20250619180801.dist-info}/RECORD +44 -42
  42. {letta_nightly-0.8.4.dev20250619104255.dist-info → letta_nightly-0.8.5.dev20250619180801.dist-info}/LICENSE +0 -0
  43. {letta_nightly-0.8.4.dev20250619104255.dist-info → letta_nightly-0.8.5.dev20250619180801.dist-info}/WHEEL +0 -0
  44. {letta_nightly-0.8.4.dev20250619104255.dist-info → letta_nightly-0.8.5.dev20250619180801.dist-info}/entry_points.txt +0 -0
@@ -1,8 +1,13 @@
1
+ import asyncio
1
2
  import json
2
- from textwrap import shorten
3
- from typing import Any, Dict, Literal, Optional
3
+ import time
4
+ from typing import Any, Dict, List, Literal, Optional
4
5
 
5
- from letta.constants import WEB_SEARCH_CLIP_CONTENT, WEB_SEARCH_INCLUDE_SCORE, WEB_SEARCH_SEPARATOR
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
- async def web_search(agent_state: "AgentState", query: str) -> str:
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 for information.
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
- query (str): The query to search the web for.
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: The search results.
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 tavily import AsyncTavilyClient
144
+ from firecrawl import AsyncFirecrawlApp
84
145
  except ImportError:
85
- raise ImportError("tavily is not installed in the tool execution environment")
86
-
87
- # Check if the API key exists
88
- if tool_settings.tavily_api_key is None:
89
- raise ValueError("TAVILY_API_KEY is not set")
90
-
91
- # Instantiate client and search
92
- tavily_client = AsyncTavilyClient(api_key=tool_settings.tavily_api_key)
93
- search_results = await tavily_client.search(query=query, auto_parameters=True)
94
-
95
- results = search_results.get("results", [])
96
- if not results:
97
- return "No search results found."
98
-
99
- # ---- format for the LLM -------------------------------------------------
100
- formatted_blocks = []
101
- for idx, item in enumerate(results, start=1):
102
- title = item.get("title") or "Untitled"
103
- url = item.get("url") or "Unknown URL"
104
- # keep each content snippet reasonably short so you don’t blow up context
105
- content = (
106
- shorten(item.get("content", "").strip(), width=600, placeholder=" …")
107
- if WEB_SEARCH_CLIP_CONTENT
108
- else item.get("content", "").strip()
109
- )
110
- score = item.get("score")
111
- if WEB_SEARCH_INCLUDE_SCORE:
112
- block = f"\nRESULT {idx}:\n" f"Title: {title}\n" f"URL: {url}\n" f"Relevance score: {score:.4f}\n" f"Content: {content}\n"
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
- block = f"\nRESULT {idx}:\n" f"Title: {title}\n" f"URL: {url}\n" f"Content: {content}\n"
115
- formatted_blocks.append(block)
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 WEB_SEARCH_SEPARATOR.join(formatted_blocks)
409
+ return response
@@ -239,7 +239,31 @@ class ToolManager:
239
239
 
240
240
  @enforce_types
241
241
  @trace_method
242
- async def list_tools_async(self, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50) -> List[PydanticTool]:
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:
@@ -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.4.dev20250619104255
3
+ Version: 0.8.5.dev20250619180801
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 (>=1.15.0,<2.0.0)
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.155,<0.2.0)
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"