letta-nightly 0.7.20.dev20250521104258__py3-none-any.whl → 0.7.21.dev20250522104246__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 (66) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +290 -3
  3. letta/agents/base_agent.py +0 -55
  4. letta/agents/helpers.py +5 -0
  5. letta/agents/letta_agent.py +314 -64
  6. letta/agents/letta_agent_batch.py +102 -55
  7. letta/agents/voice_agent.py +5 -5
  8. letta/client/client.py +9 -18
  9. letta/constants.py +55 -1
  10. letta/functions/function_sets/builtin.py +27 -0
  11. letta/groups/sleeptime_multi_agent_v2.py +1 -1
  12. letta/interfaces/anthropic_streaming_interface.py +10 -1
  13. letta/interfaces/openai_streaming_interface.py +9 -2
  14. letta/llm_api/anthropic.py +21 -2
  15. letta/llm_api/anthropic_client.py +33 -6
  16. letta/llm_api/google_ai_client.py +136 -423
  17. letta/llm_api/google_vertex_client.py +173 -22
  18. letta/llm_api/llm_api_tools.py +27 -0
  19. letta/llm_api/llm_client.py +1 -1
  20. letta/llm_api/llm_client_base.py +32 -21
  21. letta/llm_api/openai.py +57 -0
  22. letta/llm_api/openai_client.py +7 -11
  23. letta/memory.py +0 -1
  24. letta/orm/__init__.py +1 -0
  25. letta/orm/enums.py +1 -0
  26. letta/orm/provider_trace.py +26 -0
  27. letta/orm/step.py +1 -0
  28. letta/schemas/provider_trace.py +43 -0
  29. letta/schemas/providers.py +210 -65
  30. letta/schemas/step.py +1 -0
  31. letta/schemas/tool.py +4 -0
  32. letta/server/db.py +37 -19
  33. letta/server/rest_api/routers/v1/__init__.py +2 -0
  34. letta/server/rest_api/routers/v1/agents.py +57 -34
  35. letta/server/rest_api/routers/v1/blocks.py +3 -3
  36. letta/server/rest_api/routers/v1/identities.py +24 -26
  37. letta/server/rest_api/routers/v1/jobs.py +3 -3
  38. letta/server/rest_api/routers/v1/llms.py +13 -8
  39. letta/server/rest_api/routers/v1/sandbox_configs.py +6 -6
  40. letta/server/rest_api/routers/v1/tags.py +3 -3
  41. letta/server/rest_api/routers/v1/telemetry.py +18 -0
  42. letta/server/rest_api/routers/v1/tools.py +6 -6
  43. letta/server/rest_api/streaming_response.py +105 -0
  44. letta/server/rest_api/utils.py +4 -0
  45. letta/server/server.py +140 -1
  46. letta/services/agent_manager.py +251 -18
  47. letta/services/block_manager.py +52 -37
  48. letta/services/helpers/noop_helper.py +10 -0
  49. letta/services/identity_manager.py +43 -38
  50. letta/services/job_manager.py +29 -0
  51. letta/services/message_manager.py +111 -0
  52. letta/services/sandbox_config_manager.py +36 -0
  53. letta/services/step_manager.py +146 -0
  54. letta/services/telemetry_manager.py +58 -0
  55. letta/services/tool_executor/tool_execution_manager.py +49 -5
  56. letta/services/tool_executor/tool_execution_sandbox.py +47 -0
  57. letta/services/tool_executor/tool_executor.py +236 -7
  58. letta/services/tool_manager.py +160 -1
  59. letta/services/tool_sandbox/e2b_sandbox.py +65 -3
  60. letta/settings.py +10 -2
  61. letta/tracing.py +5 -5
  62. {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250522104246.dist-info}/METADATA +3 -2
  63. {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250522104246.dist-info}/RECORD +66 -59
  64. {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250522104246.dist-info}/LICENSE +0 -0
  65. {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250522104246.dist-info}/WHEEL +0 -0
  66. {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250522104246.dist-info}/entry_points.txt +0 -0
@@ -1,35 +1,64 @@
1
+ import asyncio
2
+ import json
1
3
  import math
2
4
  import traceback
3
5
  from abc import ABC, abstractmethod
4
- from typing import Any, Dict, Optional
6
+ from textwrap import shorten
7
+ from typing import Any, Dict, List, Literal, Optional
5
8
 
6
9
  from letta.constants import (
7
10
  COMPOSIO_ENTITY_ENV_VAR_KEY,
8
11
  CORE_MEMORY_LINE_NUMBER_WARNING,
9
12
  READ_ONLY_BLOCK_EDIT_ERROR,
10
13
  RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE,
14
+ WEB_SEARCH_CLIP_CONTENT,
15
+ WEB_SEARCH_INCLUDE_SCORE,
16
+ WEB_SEARCH_SEPARATOR,
11
17
  )
12
18
  from letta.functions.ast_parsers import coerce_dict_args_by_annotations, get_function_annotations_from_source
13
19
  from letta.functions.composio_helpers import execute_composio_action_async, generate_composio_action_from_func_name
14
20
  from letta.helpers.composio_helpers import get_composio_api_key
15
21
  from letta.helpers.json_helpers import json_dumps
22
+ from letta.log import get_logger
16
23
  from letta.schemas.agent import AgentState
24
+ from letta.schemas.enums import MessageRole
25
+ from letta.schemas.letta_message import AssistantMessage
26
+ from letta.schemas.letta_message_content import TextContent
27
+ from letta.schemas.message import MessageCreate
17
28
  from letta.schemas.sandbox_config import SandboxConfig
18
29
  from letta.schemas.tool import Tool
19
30
  from letta.schemas.tool_execution_result import ToolExecutionResult
20
31
  from letta.schemas.user import User
21
32
  from letta.services.agent_manager import AgentManager
33
+ from letta.services.block_manager import BlockManager
22
34
  from letta.services.message_manager import MessageManager
23
35
  from letta.services.passage_manager import PassageManager
24
36
  from letta.services.tool_sandbox.e2b_sandbox import AsyncToolSandboxE2B
25
37
  from letta.services.tool_sandbox.local_sandbox import AsyncToolSandboxLocal
26
38
  from letta.settings import tool_settings
39
+ from letta.tracing import trace_method
27
40
  from letta.utils import get_friendly_error_msg
28
41
 
42
+ logger = get_logger(__name__)
43
+
29
44
 
30
45
  class ToolExecutor(ABC):
31
46
  """Abstract base class for tool executors."""
32
47
 
48
+ def __init__(
49
+ self,
50
+ message_manager: MessageManager,
51
+ agent_manager: AgentManager,
52
+ block_manager: BlockManager,
53
+ passage_manager: PassageManager,
54
+ actor: User,
55
+ ):
56
+ self.message_manager = message_manager
57
+ self.agent_manager = agent_manager
58
+ self.block_manager = block_manager
59
+ self.passage_manager = passage_manager
60
+ self.actor = actor
61
+
33
62
  @abstractmethod
34
63
  def execute(
35
64
  self,
@@ -493,17 +522,113 @@ class LettaCoreToolExecutor(ToolExecutor):
493
522
  class LettaMultiAgentToolExecutor(ToolExecutor):
494
523
  """Executor for LETTA multi-agent core tools."""
495
524
 
496
- # TODO: Implement
497
- # def execute(self, function_name: str, function_args: dict, agent: "Agent", tool: Tool) -> ToolExecutionResult:
498
- # callable_func = get_function_from_module(LETTA_MULTI_AGENT_TOOL_MODULE_NAME, function_name)
499
- # function_args["self"] = agent # need to attach self to arg since it's dynamically linked
500
- # function_response = callable_func(**function_args)
501
- # return ToolExecutionResult(func_return=function_response)
525
+ async def execute(
526
+ self,
527
+ function_name: str,
528
+ function_args: dict,
529
+ agent_state: AgentState,
530
+ tool: Tool,
531
+ actor: User,
532
+ sandbox_config: Optional[SandboxConfig] = None,
533
+ sandbox_env_vars: Optional[Dict[str, Any]] = None,
534
+ ) -> ToolExecutionResult:
535
+ function_map = {
536
+ "send_message_to_agent_and_wait_for_reply": self.send_message_to_agent_and_wait_for_reply,
537
+ "send_message_to_agent_async": self.send_message_to_agent_async,
538
+ "send_message_to_agents_matching_tags": self.send_message_to_agents_matching_tags,
539
+ }
540
+
541
+ if function_name not in function_map:
542
+ raise ValueError(f"Unknown function: {function_name}")
543
+
544
+ # Execute the appropriate function
545
+ function_args_copy = function_args.copy() # Make a copy to avoid modifying the original
546
+ function_response = await function_map[function_name](agent_state, **function_args_copy)
547
+ return ToolExecutionResult(
548
+ status="success",
549
+ func_return=function_response,
550
+ )
551
+
552
+ async def send_message_to_agent_and_wait_for_reply(self, agent_state: AgentState, message: str, other_agent_id: str) -> str:
553
+ augmented_message = (
554
+ f"[Incoming message from agent with ID '{agent_state.id}' - to reply to this message, "
555
+ f"make sure to use the 'send_message' at the end, and the system will notify the sender of your response] "
556
+ f"{message}"
557
+ )
558
+
559
+ return str(await self._process_agent(agent_id=other_agent_id, message=augmented_message))
560
+
561
+ async def send_message_to_agent_async(self, agent_state: AgentState, message: str, other_agent_id: str) -> str:
562
+ # 1) Build the prefixed system‐message
563
+ prefixed = (
564
+ f"[Incoming message from agent with ID '{agent_state.id}' - "
565
+ f"to reply to this message, make sure to use the "
566
+ f"'send_message_to_agent_async' tool, or the agent will not receive your message] "
567
+ f"{message}"
568
+ )
569
+
570
+ task = asyncio.create_task(self._process_agent(agent_id=other_agent_id, message=prefixed))
571
+
572
+ task.add_done_callback(lambda t: (logger.error(f"Async send_message task failed: {t.exception()}") if t.exception() else None))
573
+
574
+ return "Successfully sent message"
575
+
576
+ async def send_message_to_agents_matching_tags(
577
+ self, agent_state: AgentState, message: str, match_all: List[str], match_some: List[str]
578
+ ) -> str:
579
+ # Find matching agents
580
+ matching_agents = self.agent_manager.list_agents_matching_tags(actor=self.actor, match_all=match_all, match_some=match_some)
581
+ if not matching_agents:
582
+ return str([])
583
+
584
+ augmented_message = (
585
+ "[Incoming message from external Letta agent - to reply to this message, "
586
+ "make sure to use the 'send_message' at the end, and the system will notify "
587
+ "the sender of your response] "
588
+ f"{message}"
589
+ )
590
+
591
+ tasks = [
592
+ asyncio.create_task(self._process_agent(agent_id=agent_state.id, message=augmented_message)) for agent_state in matching_agents
593
+ ]
594
+ results = await asyncio.gather(*tasks)
595
+ return str(results)
596
+
597
+ async def _process_agent(self, agent_id: str, message: str) -> Dict[str, Any]:
598
+ from letta.agents.letta_agent import LettaAgent
599
+
600
+ try:
601
+ letta_agent = LettaAgent(
602
+ agent_id=agent_id,
603
+ message_manager=self.message_manager,
604
+ agent_manager=self.agent_manager,
605
+ block_manager=self.block_manager,
606
+ passage_manager=self.passage_manager,
607
+ actor=self.actor,
608
+ )
609
+
610
+ letta_response = await letta_agent.step([MessageCreate(role=MessageRole.system, content=[TextContent(text=message)])])
611
+ messages = letta_response.messages
612
+
613
+ send_message_content = [message.content for message in messages if isinstance(message, AssistantMessage)]
614
+
615
+ return {
616
+ "agent_id": agent_id,
617
+ "response": send_message_content if send_message_content else ["<no response>"],
618
+ }
619
+
620
+ except Exception as e:
621
+ return {
622
+ "agent_id": agent_id,
623
+ "error": str(e),
624
+ "type": type(e).__name__,
625
+ }
502
626
 
503
627
 
504
628
  class ExternalComposioToolExecutor(ToolExecutor):
505
629
  """Executor for external Composio tools."""
506
630
 
631
+ @trace_method
507
632
  async def execute(
508
633
  self,
509
634
  function_name: str,
@@ -595,6 +720,7 @@ class ExternalMCPToolExecutor(ToolExecutor):
595
720
  class SandboxToolExecutor(ToolExecutor):
596
721
  """Executor for sandboxed tools."""
597
722
 
723
+ @trace_method
598
724
  async def execute(
599
725
  self,
600
726
  function_name: str,
@@ -674,3 +800,106 @@ class SandboxToolExecutor(ToolExecutor):
674
800
  func_return=error_message,
675
801
  stderr=[stderr],
676
802
  )
803
+
804
+
805
+ class LettaBuiltinToolExecutor(ToolExecutor):
806
+ """Executor for built in Letta tools."""
807
+
808
+ @trace_method
809
+ async def execute(
810
+ self,
811
+ function_name: str,
812
+ function_args: dict,
813
+ agent_state: AgentState,
814
+ tool: Tool,
815
+ actor: User,
816
+ sandbox_config: Optional[SandboxConfig] = None,
817
+ sandbox_env_vars: Optional[Dict[str, Any]] = None,
818
+ ) -> ToolExecutionResult:
819
+ function_map = {"run_code": self.run_code, "web_search": self.web_search}
820
+
821
+ if function_name not in function_map:
822
+ raise ValueError(f"Unknown function: {function_name}")
823
+
824
+ # Execute the appropriate function
825
+ function_args_copy = function_args.copy() # Make a copy to avoid modifying the original
826
+ function_response = await function_map[function_name](**function_args_copy)
827
+
828
+ return ToolExecutionResult(
829
+ status="success",
830
+ func_return=function_response,
831
+ )
832
+
833
+ async def run_code(self, code: str, language: Literal["python", "js", "ts", "r", "java"]) -> str:
834
+ from e2b_code_interpreter import AsyncSandbox
835
+
836
+ if tool_settings.e2b_api_key is None:
837
+ raise ValueError("E2B_API_KEY is not set")
838
+
839
+ sbx = await AsyncSandbox.create(api_key=tool_settings.e2b_api_key)
840
+ params = {"code": code}
841
+ if language != "python":
842
+ # Leave empty for python
843
+ params["language"] = language
844
+
845
+ res = self._llm_friendly_result(await sbx.run_code(**params))
846
+ return json.dumps(res, ensure_ascii=False)
847
+
848
+ def _llm_friendly_result(self, res):
849
+ out = {
850
+ "results": [r.text if hasattr(r, "text") else str(r) for r in res.results],
851
+ "logs": {
852
+ "stdout": getattr(res.logs, "stdout", []),
853
+ "stderr": getattr(res.logs, "stderr", []),
854
+ },
855
+ }
856
+ err = getattr(res, "error", None)
857
+ if err is not None:
858
+ out["error"] = err
859
+ return out
860
+
861
+ async def web_search(agent_state: "AgentState", query: str) -> str:
862
+ """
863
+ Search the web for information.
864
+ Args:
865
+ query (str): The query to search the web for.
866
+ Returns:
867
+ str: The search results.
868
+ """
869
+
870
+ try:
871
+ from tavily import AsyncTavilyClient
872
+ except ImportError:
873
+ raise ImportError("tavily is not installed in the tool execution environment")
874
+
875
+ # Check if the API key exists
876
+ if tool_settings.tavily_api_key is None:
877
+ raise ValueError("TAVILY_API_KEY is not set")
878
+
879
+ # Instantiate client and search
880
+ tavily_client = AsyncTavilyClient(api_key=tool_settings.tavily_api_key)
881
+ search_results = await tavily_client.search(query=query, auto_parameters=True)
882
+
883
+ results = search_results.get("results", [])
884
+ if not results:
885
+ return "No search results found."
886
+
887
+ # ---- format for the LLM -------------------------------------------------
888
+ formatted_blocks = []
889
+ for idx, item in enumerate(results, start=1):
890
+ title = item.get("title") or "Untitled"
891
+ url = item.get("url") or "Unknown URL"
892
+ # keep each content snippet reasonably short so you don’t blow up context
893
+ content = (
894
+ shorten(item.get("content", "").strip(), width=600, placeholder=" …")
895
+ if WEB_SEARCH_CLIP_CONTENT
896
+ else item.get("content", "").strip()
897
+ )
898
+ score = item.get("score")
899
+ if WEB_SEARCH_INCLUDE_SCORE:
900
+ block = f"\nRESULT {idx}:\n" f"Title: {title}\n" f"URL: {url}\n" f"Relevance score: {score:.4f}\n" f"Content: {content}\n"
901
+ else:
902
+ block = f"\nRESULT {idx}:\n" f"Title: {title}\n" f"URL: {url}\n" f"Content: {content}\n"
903
+ formatted_blocks.append(block)
904
+
905
+ return WEB_SEARCH_SEPARATOR.join(formatted_blocks)
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import importlib
2
3
  import warnings
3
4
  from typing import List, Optional
@@ -9,6 +10,7 @@ from letta.constants import (
9
10
  BASE_TOOLS,
10
11
  BASE_VOICE_SLEEPTIME_CHAT_TOOLS,
11
12
  BASE_VOICE_SLEEPTIME_TOOLS,
13
+ BUILTIN_TOOLS,
12
14
  LETTA_TOOL_SET,
13
15
  MCP_TOOL_TAG_NAME_PREFIX,
14
16
  MULTI_AGENT_TOOLS,
@@ -59,6 +61,32 @@ class ToolManager:
59
61
 
60
62
  return tool
61
63
 
64
+ @enforce_types
65
+ async def create_or_update_tool_async(self, pydantic_tool: PydanticTool, actor: PydanticUser) -> PydanticTool:
66
+ """Create a new tool based on the ToolCreate schema."""
67
+ tool_id = await self.get_tool_id_by_name_async(tool_name=pydantic_tool.name, actor=actor)
68
+ if tool_id:
69
+ # Put to dict and remove fields that should not be reset
70
+ update_data = pydantic_tool.model_dump(exclude_unset=True, exclude_none=True)
71
+
72
+ # If there's anything to update
73
+ if update_data:
74
+ # In case we want to update the tool type
75
+ # Useful if we are shuffling around base tools
76
+ updated_tool_type = None
77
+ if "tool_type" in update_data:
78
+ updated_tool_type = update_data.get("tool_type")
79
+ tool = await self.update_tool_by_id_async(tool_id, ToolUpdate(**update_data), actor, updated_tool_type=updated_tool_type)
80
+ else:
81
+ printd(
82
+ f"`create_or_update_tool` was called with user_id={actor.id}, organization_id={actor.organization_id}, name={pydantic_tool.name}, but found existing tool with nothing to update."
83
+ )
84
+ tool = await self.get_tool_by_id_async(tool_id, actor=actor)
85
+ else:
86
+ tool = await self.create_tool_async(pydantic_tool, actor=actor)
87
+
88
+ return tool
89
+
62
90
  @enforce_types
63
91
  def create_or_update_mcp_tool(self, tool_create: ToolCreate, mcp_server_name: str, actor: PydanticUser) -> PydanticTool:
64
92
  metadata = {MCP_TOOL_TAG_NAME_PREFIX: {"server_name": mcp_server_name}}
@@ -96,6 +124,21 @@ class ToolManager:
96
124
  tool.create(session, actor=actor) # Re-raise other database-related errors
97
125
  return tool.to_pydantic()
98
126
 
127
+ @enforce_types
128
+ async def create_tool_async(self, pydantic_tool: PydanticTool, actor: PydanticUser) -> PydanticTool:
129
+ """Create a new tool based on the ToolCreate schema."""
130
+ async with db_registry.async_session() as session:
131
+ # Set the organization id at the ORM layer
132
+ pydantic_tool.organization_id = actor.organization_id
133
+ # Auto-generate description if not provided
134
+ if pydantic_tool.description is None:
135
+ pydantic_tool.description = pydantic_tool.json_schema.get("description", None)
136
+ tool_data = pydantic_tool.model_dump(to_orm=True)
137
+
138
+ tool = ToolModel(**tool_data)
139
+ await tool.create_async(session, actor=actor) # Re-raise other database-related errors
140
+ return tool.to_pydantic()
141
+
99
142
  @enforce_types
100
143
  def get_tool_by_id(self, tool_id: str, actor: PydanticUser) -> PydanticTool:
101
144
  """Fetch a tool by its ID."""
@@ -105,6 +148,15 @@ class ToolManager:
105
148
  # Convert the SQLAlchemy Tool object to PydanticTool
106
149
  return tool.to_pydantic()
107
150
 
151
+ @enforce_types
152
+ async def get_tool_by_id_async(self, tool_id: str, actor: PydanticUser) -> PydanticTool:
153
+ """Fetch a tool by its ID."""
154
+ async with db_registry.async_session() as session:
155
+ # Retrieve tool by id using the Tool model's read method
156
+ tool = await ToolModel.read_async(db_session=session, identifier=tool_id, actor=actor)
157
+ # Convert the SQLAlchemy Tool object to PydanticTool
158
+ return tool.to_pydantic()
159
+
108
160
  @enforce_types
109
161
  def get_tool_by_name(self, tool_name: str, actor: PydanticUser) -> Optional[PydanticTool]:
110
162
  """Retrieve a tool by its name and a user. We derive the organization from the user, and retrieve that tool."""
@@ -135,6 +187,16 @@ class ToolManager:
135
187
  except NoResultFound:
136
188
  return None
137
189
 
190
+ @enforce_types
191
+ async def get_tool_id_by_name_async(self, tool_name: str, actor: PydanticUser) -> Optional[str]:
192
+ """Retrieve a tool by its name and a user. We derive the organization from the user, and retrieve that tool."""
193
+ try:
194
+ async with db_registry.async_session() as session:
195
+ tool = await ToolModel.read_async(db_session=session, name=tool_name, actor=actor)
196
+ return tool.id
197
+ except NoResultFound:
198
+ return None
199
+
138
200
  @enforce_types
139
201
  async def list_tools_async(self, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50) -> List[PydanticTool]:
140
202
  """List all tools with optional pagination."""
@@ -204,6 +266,35 @@ class ToolManager:
204
266
  # Save the updated tool to the database
205
267
  return tool.update(db_session=session, actor=actor).to_pydantic()
206
268
 
269
+ @enforce_types
270
+ async def update_tool_by_id_async(
271
+ self, tool_id: str, tool_update: ToolUpdate, actor: PydanticUser, updated_tool_type: Optional[ToolType] = None
272
+ ) -> PydanticTool:
273
+ """Update a tool by its ID with the given ToolUpdate object."""
274
+ async with db_registry.async_session() as session:
275
+ # Fetch the tool by ID
276
+ tool = await ToolModel.read_async(db_session=session, identifier=tool_id, actor=actor)
277
+
278
+ # Update tool attributes with only the fields that were explicitly set
279
+ update_data = tool_update.model_dump(to_orm=True, exclude_none=True)
280
+ for key, value in update_data.items():
281
+ setattr(tool, key, value)
282
+
283
+ # If source code is changed and a new json_schema is not provided, we want to auto-refresh the schema
284
+ if "source_code" in update_data.keys() and "json_schema" not in update_data.keys():
285
+ pydantic_tool = tool.to_pydantic()
286
+ new_schema = derive_openai_json_schema(source_code=pydantic_tool.source_code)
287
+
288
+ tool.json_schema = new_schema
289
+ tool.name = new_schema["name"]
290
+
291
+ if updated_tool_type:
292
+ tool.tool_type = updated_tool_type
293
+
294
+ # Save the updated tool to the database
295
+ tool = await tool.update_async(db_session=session, actor=actor)
296
+ return tool.to_pydantic()
297
+
207
298
  @enforce_types
208
299
  def delete_tool_by_id(self, tool_id: str, actor: PydanticUser) -> None:
209
300
  """Delete a tool by its ID."""
@@ -218,7 +309,7 @@ class ToolManager:
218
309
  def upsert_base_tools(self, actor: PydanticUser) -> List[PydanticTool]:
219
310
  """Add default tools in base.py and multi_agent.py"""
220
311
  functions_to_schema = {}
221
- module_names = ["base", "multi_agent", "voice"]
312
+ module_names = ["base", "multi_agent", "voice", "builtin"]
222
313
 
223
314
  for module_name in module_names:
224
315
  full_module_name = f"letta.functions.function_sets.{module_name}"
@@ -254,6 +345,9 @@ class ToolManager:
254
345
  elif name in BASE_VOICE_SLEEPTIME_TOOLS or name in BASE_VOICE_SLEEPTIME_CHAT_TOOLS:
255
346
  tool_type = ToolType.LETTA_VOICE_SLEEPTIME_CORE
256
347
  tags = [tool_type.value]
348
+ elif name in BUILTIN_TOOLS:
349
+ tool_type = ToolType.LETTA_BUILTIN
350
+ tags = [tool_type.value]
257
351
  else:
258
352
  raise ValueError(
259
353
  f"Tool name {name} is not in the list of base tool names: {BASE_TOOLS + BASE_MEMORY_TOOLS + MULTI_AGENT_TOOLS + BASE_SLEEPTIME_TOOLS + BASE_VOICE_SLEEPTIME_TOOLS + BASE_VOICE_SLEEPTIME_CHAT_TOOLS}"
@@ -275,3 +369,68 @@ class ToolManager:
275
369
 
276
370
  # TODO: Delete any base tools that are stale
277
371
  return tools
372
+
373
+ @enforce_types
374
+ async def upsert_base_tools_async(self, actor: PydanticUser) -> List[PydanticTool]:
375
+ """Add default tools in base.py and multi_agent.py"""
376
+ functions_to_schema = {}
377
+ module_names = ["base", "multi_agent", "voice", "builtin"]
378
+
379
+ for module_name in module_names:
380
+ full_module_name = f"letta.functions.function_sets.{module_name}"
381
+ try:
382
+ module = importlib.import_module(full_module_name)
383
+ except Exception as e:
384
+ # Handle other general exceptions
385
+ raise e
386
+
387
+ try:
388
+ # Load the function set
389
+ functions_to_schema.update(load_function_set(module))
390
+ except ValueError as e:
391
+ err = f"Error loading function set '{module_name}': {e}"
392
+ warnings.warn(err)
393
+
394
+ # create tool in db
395
+ tools = []
396
+ for name, schema in functions_to_schema.items():
397
+ if name in LETTA_TOOL_SET:
398
+ if name in BASE_TOOLS:
399
+ tool_type = ToolType.LETTA_CORE
400
+ tags = [tool_type.value]
401
+ elif name in BASE_MEMORY_TOOLS:
402
+ tool_type = ToolType.LETTA_MEMORY_CORE
403
+ tags = [tool_type.value]
404
+ elif name in MULTI_AGENT_TOOLS:
405
+ tool_type = ToolType.LETTA_MULTI_AGENT_CORE
406
+ tags = [tool_type.value]
407
+ elif name in BASE_SLEEPTIME_TOOLS:
408
+ tool_type = ToolType.LETTA_SLEEPTIME_CORE
409
+ tags = [tool_type.value]
410
+ elif name in BASE_VOICE_SLEEPTIME_TOOLS or name in BASE_VOICE_SLEEPTIME_CHAT_TOOLS:
411
+ tool_type = ToolType.LETTA_VOICE_SLEEPTIME_CORE
412
+ tags = [tool_type.value]
413
+ elif name in BUILTIN_TOOLS:
414
+ tool_type = ToolType.LETTA_BUILTIN
415
+ tags = [tool_type.value]
416
+ else:
417
+ raise ValueError(
418
+ f"Tool name {name} is not in the list of base tool names: {BASE_TOOLS + BASE_MEMORY_TOOLS + MULTI_AGENT_TOOLS + BASE_SLEEPTIME_TOOLS + BASE_VOICE_SLEEPTIME_TOOLS + BASE_VOICE_SLEEPTIME_CHAT_TOOLS}"
419
+ )
420
+
421
+ # create to tool
422
+ tools.append(
423
+ self.create_or_update_tool_async(
424
+ PydanticTool(
425
+ name=name,
426
+ tags=tags,
427
+ source_type="python",
428
+ tool_type=tool_type,
429
+ return_char_limit=BASE_FUNCTION_RETURN_CHAR_LIMIT,
430
+ ),
431
+ actor=actor,
432
+ )
433
+ )
434
+
435
+ # TODO: Delete any base tools that are stale
436
+ return await asyncio.gather(*tools)
@@ -6,6 +6,7 @@ from letta.schemas.sandbox_config import SandboxConfig, SandboxType
6
6
  from letta.schemas.tool import Tool
7
7
  from letta.schemas.tool_execution_result import ToolExecutionResult
8
8
  from letta.services.tool_sandbox.base import AsyncToolSandboxBase
9
+ from letta.tracing import log_event, trace_method
9
10
  from letta.utils import get_friendly_error_msg
10
11
 
11
12
  logger = get_logger(__name__)
@@ -27,6 +28,7 @@ class AsyncToolSandboxE2B(AsyncToolSandboxBase):
27
28
  super().__init__(tool_name, args, user, tool_object, sandbox_config=sandbox_config, sandbox_env_vars=sandbox_env_vars)
28
29
  self.force_recreate = force_recreate
29
30
 
31
+ @trace_method
30
32
  async def run(
31
33
  self,
32
34
  agent_state: Optional[AgentState] = None,
@@ -44,6 +46,7 @@ class AsyncToolSandboxE2B(AsyncToolSandboxBase):
44
46
 
45
47
  return result
46
48
 
49
+ @trace_method
47
50
  async def run_e2b_sandbox(
48
51
  self, agent_state: Optional[AgentState] = None, additional_env_vars: Optional[Dict] = None
49
52
  ) -> ToolExecutionResult:
@@ -81,10 +84,21 @@ class AsyncToolSandboxE2B(AsyncToolSandboxBase):
81
84
  env_vars.update(additional_env_vars)
82
85
  code = self.generate_execution_script(agent_state=agent_state)
83
86
 
87
+ log_event(
88
+ "e2b_execution_started",
89
+ {"tool": self.tool_name, "sandbox_id": e2b_sandbox.sandbox_id, "code": code, "env_vars": env_vars},
90
+ )
84
91
  execution = await e2b_sandbox.run_code(code, envs=env_vars)
85
-
86
92
  if execution.results:
87
93
  func_return, agent_state = self.parse_best_effort(execution.results[0].text)
94
+ log_event(
95
+ "e2b_execution_succeeded",
96
+ {
97
+ "tool": self.tool_name,
98
+ "sandbox_id": e2b_sandbox.sandbox_id,
99
+ "func_return": func_return,
100
+ },
101
+ )
88
102
  elif execution.error:
89
103
  logger.error(f"Executing tool {self.tool_name} raised a {execution.error.name} with message: \n{execution.error.value}")
90
104
  logger.error(f"Traceback from e2b sandbox: \n{execution.error.traceback}")
@@ -92,7 +106,25 @@ class AsyncToolSandboxE2B(AsyncToolSandboxBase):
92
106
  function_name=self.tool_name, exception_name=execution.error.name, exception_message=execution.error.value
93
107
  )
94
108
  execution.logs.stderr.append(execution.error.traceback)
109
+ log_event(
110
+ "e2b_execution_failed",
111
+ {
112
+ "tool": self.tool_name,
113
+ "sandbox_id": e2b_sandbox.sandbox_id,
114
+ "error_type": execution.error.name,
115
+ "error_message": execution.error.value,
116
+ "func_return": func_return,
117
+ },
118
+ )
95
119
  else:
120
+ log_event(
121
+ "e2b_execution_empty",
122
+ {
123
+ "tool": self.tool_name,
124
+ "sandbox_id": e2b_sandbox.sandbox_id,
125
+ "status": "no_results_no_error",
126
+ },
127
+ )
96
128
  raise ValueError(f"Tool {self.tool_name} returned execution with None")
97
129
 
98
130
  return ToolExecutionResult(
@@ -110,24 +142,54 @@ class AsyncToolSandboxE2B(AsyncToolSandboxBase):
110
142
  exception_class = builtins_dict.get(e2b_execution.error.name, Exception)
111
143
  return exception_class(e2b_execution.error.value)
112
144
 
145
+ @trace_method
113
146
  async def create_e2b_sandbox_with_metadata_hash(self, sandbox_config: SandboxConfig) -> "Sandbox":
114
147
  from e2b_code_interpreter import AsyncSandbox
115
148
 
116
149
  state_hash = sandbox_config.fingerprint()
117
150
  e2b_config = sandbox_config.get_e2b_config()
118
151
 
152
+ log_event(
153
+ "e2b_sandbox_create_started",
154
+ {
155
+ "sandbox_fingerprint": state_hash,
156
+ "e2b_config": e2b_config.model_dump(),
157
+ },
158
+ )
159
+
119
160
  if e2b_config.template:
120
161
  sbx = await AsyncSandbox.create(sandbox_config.get_e2b_config().template, metadata={self.METADATA_CONFIG_STATE_KEY: state_hash})
121
162
  else:
122
- # no template
123
163
  sbx = await AsyncSandbox.create(
124
164
  metadata={self.METADATA_CONFIG_STATE_KEY: state_hash}, **e2b_config.model_dump(exclude={"pip_requirements"})
125
165
  )
126
166
 
127
- # install pip requirements
167
+ log_event(
168
+ "e2b_sandbox_create_finished",
169
+ {
170
+ "sandbox_id": sbx.sandbox_id,
171
+ "sandbox_fingerprint": state_hash,
172
+ },
173
+ )
174
+
128
175
  if e2b_config.pip_requirements:
129
176
  for package in e2b_config.pip_requirements:
177
+ log_event(
178
+ "e2b_pip_install_started",
179
+ {
180
+ "sandbox_id": sbx.sandbox_id,
181
+ "package": package,
182
+ },
183
+ )
130
184
  await sbx.commands.run(f"pip install {package}")
185
+ log_event(
186
+ "e2b_pip_install_finished",
187
+ {
188
+ "sandbox_id": sbx.sandbox_id,
189
+ "package": package,
190
+ },
191
+ )
192
+
131
193
  return sbx
132
194
 
133
195
  async def list_running_e2b_sandboxes(self):