letta-nightly 0.7.20.dev20250521104258__py3-none-any.whl → 0.7.21.dev20250521233415__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.dev20250521233415.dist-info}/METADATA +3 -2
  63. {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250521233415.dist-info}/RECORD +66 -59
  64. {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250521233415.dist-info}/LICENSE +0 -0
  65. {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250521233415.dist-info}/WHEEL +0 -0
  66. {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250521233415.dist-info}/entry_points.txt +0 -0
@@ -2,6 +2,7 @@ from datetime import datetime
2
2
  from typing import List, Literal, Optional
3
3
 
4
4
  from sqlalchemy import select
5
+ from sqlalchemy.ext.asyncio import AsyncSession
5
6
  from sqlalchemy.orm import Session
6
7
 
7
8
  from letta.orm.errors import NoResultFound
@@ -12,6 +13,7 @@ from letta.schemas.openai.chat_completion_response import UsageStatistics
12
13
  from letta.schemas.step import Step as PydanticStep
13
14
  from letta.schemas.user import User as PydanticUser
14
15
  from letta.server.db import db_registry
16
+ from letta.services.helpers.noop_helper import singleton
15
17
  from letta.tracing import get_trace_id
16
18
  from letta.utils import enforce_types
17
19
 
@@ -57,12 +59,14 @@ class StepManager:
57
59
  actor: PydanticUser,
58
60
  agent_id: str,
59
61
  provider_name: str,
62
+ provider_category: str,
60
63
  model: str,
61
64
  model_endpoint: Optional[str],
62
65
  context_window_limit: int,
63
66
  usage: UsageStatistics,
64
67
  provider_id: Optional[str] = None,
65
68
  job_id: Optional[str] = None,
69
+ step_id: Optional[str] = None,
66
70
  ) -> PydanticStep:
67
71
  step_data = {
68
72
  "origin": None,
@@ -70,6 +74,7 @@ class StepManager:
70
74
  "agent_id": agent_id,
71
75
  "provider_id": provider_id,
72
76
  "provider_name": provider_name,
77
+ "provider_category": provider_category,
73
78
  "model": model,
74
79
  "model_endpoint": model_endpoint,
75
80
  "context_window_limit": context_window_limit,
@@ -81,6 +86,8 @@ class StepManager:
81
86
  "tid": None,
82
87
  "trace_id": get_trace_id(), # Get the current trace ID
83
88
  }
89
+ if step_id:
90
+ step_data["id"] = step_id
84
91
  with db_registry.session() as session:
85
92
  if job_id:
86
93
  self._verify_job_access(session, job_id, actor, access=["write"])
@@ -88,6 +95,48 @@ class StepManager:
88
95
  new_step.create(session)
89
96
  return new_step.to_pydantic()
90
97
 
98
+ @enforce_types
99
+ async def log_step_async(
100
+ self,
101
+ actor: PydanticUser,
102
+ agent_id: str,
103
+ provider_name: str,
104
+ provider_category: str,
105
+ model: str,
106
+ model_endpoint: Optional[str],
107
+ context_window_limit: int,
108
+ usage: UsageStatistics,
109
+ provider_id: Optional[str] = None,
110
+ job_id: Optional[str] = None,
111
+ step_id: Optional[str] = None,
112
+ ) -> PydanticStep:
113
+ step_data = {
114
+ "origin": None,
115
+ "organization_id": actor.organization_id,
116
+ "agent_id": agent_id,
117
+ "provider_id": provider_id,
118
+ "provider_name": provider_name,
119
+ "provider_category": provider_category,
120
+ "model": model,
121
+ "model_endpoint": model_endpoint,
122
+ "context_window_limit": context_window_limit,
123
+ "completion_tokens": usage.completion_tokens,
124
+ "prompt_tokens": usage.prompt_tokens,
125
+ "total_tokens": usage.total_tokens,
126
+ "job_id": job_id,
127
+ "tags": [],
128
+ "tid": None,
129
+ "trace_id": get_trace_id(), # Get the current trace ID
130
+ }
131
+ if step_id:
132
+ step_data["id"] = step_id
133
+ async with db_registry.async_session() as session:
134
+ if job_id:
135
+ await self._verify_job_access_async(session, job_id, actor, access=["write"])
136
+ new_step = StepModel(**step_data)
137
+ await new_step.create_async(session)
138
+ return new_step.to_pydantic()
139
+
91
140
  @enforce_types
92
141
  def get_step(self, step_id: str, actor: PydanticUser) -> PydanticStep:
93
142
  with db_registry.session() as session:
@@ -147,3 +196,100 @@ class StepManager:
147
196
  if not job:
148
197
  raise NoResultFound(f"Job with id {job_id} does not exist or user does not have access")
149
198
  return job
199
+
200
+ async def _verify_job_access_async(
201
+ self,
202
+ session: AsyncSession,
203
+ job_id: str,
204
+ actor: PydanticUser,
205
+ access: List[Literal["read", "write", "delete"]] = ["read"],
206
+ ) -> JobModel:
207
+ """
208
+ Verify that a job exists and the user has the required access asynchronously.
209
+
210
+ Args:
211
+ session: The async database session
212
+ job_id: The ID of the job to verify
213
+ actor: The user making the request
214
+
215
+ Returns:
216
+ The job if it exists and the user has access
217
+
218
+ Raises:
219
+ NoResultFound: If the job does not exist or user does not have access
220
+ """
221
+ job_query = select(JobModel).where(JobModel.id == job_id)
222
+ job_query = JobModel.apply_access_predicate(job_query, actor, access, AccessType.USER)
223
+ result = await session.execute(job_query)
224
+ job = result.scalar_one_or_none()
225
+ if not job:
226
+ raise NoResultFound(f"Job with id {job_id} does not exist or user does not have access")
227
+ return job
228
+
229
+
230
+ @singleton
231
+ class NoopStepManager(StepManager):
232
+ """
233
+ Noop implementation of StepManager.
234
+ Temporarily used for migrations, but allows for different implementations in the future.
235
+ Will not allow for writes, but will still allow for reads.
236
+ """
237
+
238
+ @enforce_types
239
+ def log_step(
240
+ self,
241
+ actor: PydanticUser,
242
+ agent_id: str,
243
+ provider_name: str,
244
+ provider_category: str,
245
+ model: str,
246
+ model_endpoint: Optional[str],
247
+ context_window_limit: int,
248
+ usage: UsageStatistics,
249
+ provider_id: Optional[str] = None,
250
+ job_id: Optional[str] = None,
251
+ step_id: Optional[str] = None,
252
+ ) -> PydanticStep:
253
+ return
254
+
255
+ @enforce_types
256
+ async def log_step_async(
257
+ self,
258
+ actor: PydanticUser,
259
+ agent_id: str,
260
+ provider_name: str,
261
+ provider_category: str,
262
+ model: str,
263
+ model_endpoint: Optional[str],
264
+ context_window_limit: int,
265
+ usage: UsageStatistics,
266
+ provider_id: Optional[str] = None,
267
+ job_id: Optional[str] = None,
268
+ step_id: Optional[str] = None,
269
+ ) -> PydanticStep:
270
+ step_data = {
271
+ "origin": None,
272
+ "organization_id": actor.organization_id,
273
+ "agent_id": agent_id,
274
+ "provider_id": provider_id,
275
+ "provider_name": provider_name,
276
+ "provider_category": provider_category,
277
+ "model": model,
278
+ "model_endpoint": model_endpoint,
279
+ "context_window_limit": context_window_limit,
280
+ "completion_tokens": usage.completion_tokens,
281
+ "prompt_tokens": usage.prompt_tokens,
282
+ "total_tokens": usage.total_tokens,
283
+ "job_id": job_id,
284
+ "tags": [],
285
+ "tid": None,
286
+ "trace_id": get_trace_id(), # Get the current trace ID
287
+ }
288
+ if step_id:
289
+ step_data["id"] = step_id
290
+ async with db_registry.async_session() as session:
291
+ if job_id:
292
+ await self._verify_job_access_async(session, job_id, actor, access=["write"])
293
+ new_step = StepModel(**step_data)
294
+ await new_step.create_async(session)
295
+ return new_step.to_pydantic()
@@ -0,0 +1,58 @@
1
+ from letta.helpers.json_helpers import json_dumps, json_loads
2
+ from letta.orm.provider_trace import ProviderTrace as ProviderTraceModel
3
+ from letta.schemas.provider_trace import ProviderTrace as PydanticProviderTrace
4
+ from letta.schemas.provider_trace import ProviderTraceCreate
5
+ from letta.schemas.step import Step as PydanticStep
6
+ from letta.schemas.user import User as PydanticUser
7
+ from letta.server.db import db_registry
8
+ from letta.services.helpers.noop_helper import singleton
9
+ from letta.utils import enforce_types
10
+
11
+
12
+ class TelemetryManager:
13
+ @enforce_types
14
+ async def get_provider_trace_by_step_id_async(
15
+ self,
16
+ step_id: str,
17
+ actor: PydanticUser,
18
+ ) -> PydanticProviderTrace:
19
+ async with db_registry.async_session() as session:
20
+ provider_trace = await ProviderTraceModel.read_async(db_session=session, step_id=step_id, actor=actor)
21
+ return provider_trace.to_pydantic()
22
+
23
+ @enforce_types
24
+ async def create_provider_trace_async(self, actor: PydanticUser, provider_trace_create: ProviderTraceCreate) -> PydanticProviderTrace:
25
+ async with db_registry.async_session() as session:
26
+ provider_trace = ProviderTraceModel(**provider_trace_create.model_dump())
27
+ if provider_trace_create.request_json:
28
+ request_json_str = json_dumps(provider_trace_create.request_json)
29
+ provider_trace.request_json = json_loads(request_json_str)
30
+
31
+ if provider_trace_create.response_json:
32
+ response_json_str = json_dumps(provider_trace_create.response_json)
33
+ provider_trace.response_json = json_loads(response_json_str)
34
+ await provider_trace.create_async(session, actor=actor)
35
+ return provider_trace.to_pydantic()
36
+
37
+ @enforce_types
38
+ def create_provider_trace(self, actor: PydanticUser, provider_trace_create: ProviderTraceCreate) -> PydanticProviderTrace:
39
+ with db_registry.session() as session:
40
+ provider_trace = ProviderTraceModel(**provider_trace_create.model_dump())
41
+ provider_trace.create(session, actor=actor)
42
+ return provider_trace.to_pydantic()
43
+
44
+
45
+ @singleton
46
+ class NoopTelemetryManager(TelemetryManager):
47
+ """
48
+ Noop implementation of TelemetryManager.
49
+ """
50
+
51
+ async def create_provider_trace_async(self, actor: PydanticUser, provider_trace_create: ProviderTraceCreate) -> PydanticProviderTrace:
52
+ return
53
+
54
+ async def get_provider_trace_by_step_id_async(self, step_id: str, actor: PydanticUser) -> PydanticStep:
55
+ return
56
+
57
+ def create_provider_trace(self, actor: PydanticUser, provider_trace_create: ProviderTraceCreate) -> PydanticProviderTrace:
58
+ return
@@ -8,9 +8,14 @@ from letta.schemas.sandbox_config import SandboxConfig
8
8
  from letta.schemas.tool import Tool
9
9
  from letta.schemas.tool_execution_result import ToolExecutionResult
10
10
  from letta.schemas.user import User
11
+ from letta.services.agent_manager import AgentManager
12
+ from letta.services.block_manager import BlockManager
13
+ from letta.services.message_manager import MessageManager
14
+ from letta.services.passage_manager import PassageManager
11
15
  from letta.services.tool_executor.tool_executor import (
12
16
  ExternalComposioToolExecutor,
13
17
  ExternalMCPToolExecutor,
18
+ LettaBuiltinToolExecutor,
14
19
  LettaCoreToolExecutor,
15
20
  LettaMultiAgentToolExecutor,
16
21
  SandboxToolExecutor,
@@ -28,15 +33,30 @@ class ToolExecutorFactory:
28
33
  ToolType.LETTA_MEMORY_CORE: LettaCoreToolExecutor,
29
34
  ToolType.LETTA_SLEEPTIME_CORE: LettaCoreToolExecutor,
30
35
  ToolType.LETTA_MULTI_AGENT_CORE: LettaMultiAgentToolExecutor,
36
+ ToolType.LETTA_BUILTIN: LettaBuiltinToolExecutor,
31
37
  ToolType.EXTERNAL_COMPOSIO: ExternalComposioToolExecutor,
32
38
  ToolType.EXTERNAL_MCP: ExternalMCPToolExecutor,
33
39
  }
34
40
 
35
41
  @classmethod
36
- def get_executor(cls, tool_type: ToolType) -> ToolExecutor:
42
+ def get_executor(
43
+ cls,
44
+ tool_type: ToolType,
45
+ message_manager: MessageManager,
46
+ agent_manager: AgentManager,
47
+ block_manager: BlockManager,
48
+ passage_manager: PassageManager,
49
+ actor: User,
50
+ ) -> ToolExecutor:
37
51
  """Get the appropriate executor for the given tool type."""
38
52
  executor_class = cls._executor_map.get(tool_type, SandboxToolExecutor)
39
- return executor_class()
53
+ return executor_class(
54
+ message_manager=message_manager,
55
+ agent_manager=agent_manager,
56
+ block_manager=block_manager,
57
+ passage_manager=passage_manager,
58
+ actor=actor,
59
+ )
40
60
 
41
61
 
42
62
  class ToolExecutionManager:
@@ -44,11 +64,19 @@ class ToolExecutionManager:
44
64
 
45
65
  def __init__(
46
66
  self,
67
+ message_manager: MessageManager,
68
+ agent_manager: AgentManager,
69
+ block_manager: BlockManager,
70
+ passage_manager: PassageManager,
47
71
  agent_state: AgentState,
48
72
  actor: User,
49
73
  sandbox_config: Optional[SandboxConfig] = None,
50
74
  sandbox_env_vars: Optional[Dict[str, Any]] = None,
51
75
  ):
76
+ self.message_manager = message_manager
77
+ self.agent_manager = agent_manager
78
+ self.block_manager = block_manager
79
+ self.passage_manager = passage_manager
52
80
  self.agent_state = agent_state
53
81
  self.logger = get_logger(__name__)
54
82
  self.actor = actor
@@ -68,7 +96,14 @@ class ToolExecutionManager:
68
96
  Tuple containing the function response and sandbox run result (if applicable)
69
97
  """
70
98
  try:
71
- executor = ToolExecutorFactory.get_executor(tool.tool_type)
99
+ executor = ToolExecutorFactory.get_executor(
100
+ tool.tool_type,
101
+ message_manager=self.message_manager,
102
+ agent_manager=self.agent_manager,
103
+ block_manager=self.block_manager,
104
+ passage_manager=self.passage_manager,
105
+ actor=self.actor,
106
+ )
72
107
  return executor.execute(
73
108
  function_name,
74
109
  function_args,
@@ -98,9 +133,18 @@ class ToolExecutionManager:
98
133
  Execute a tool asynchronously and persist any state changes.
99
134
  """
100
135
  try:
101
- executor = ToolExecutorFactory.get_executor(tool.tool_type)
136
+ executor = ToolExecutorFactory.get_executor(
137
+ tool.tool_type,
138
+ message_manager=self.message_manager,
139
+ agent_manager=self.agent_manager,
140
+ block_manager=self.block_manager,
141
+ passage_manager=self.passage_manager,
142
+ actor=self.actor,
143
+ )
102
144
  # TODO: Extend this async model to composio
103
- if isinstance(executor, (SandboxToolExecutor, ExternalComposioToolExecutor)):
145
+ if isinstance(
146
+ executor, (SandboxToolExecutor, ExternalComposioToolExecutor, LettaBuiltinToolExecutor, LettaMultiAgentToolExecutor)
147
+ ):
104
148
  result = await executor.execute(function_name, function_args, self.agent_state, tool, self.actor)
105
149
  else:
106
150
  result = executor.execute(function_name, function_args, self.agent_state, tool, self.actor)
@@ -73,6 +73,7 @@ class ToolExecutionSandbox:
73
73
  self.force_recreate = force_recreate
74
74
  self.force_recreate_venv = force_recreate_venv
75
75
 
76
+ @trace_method
76
77
  def run(
77
78
  self,
78
79
  agent_state: Optional[AgentState] = None,
@@ -321,6 +322,7 @@ class ToolExecutionSandbox:
321
322
 
322
323
  # e2b sandbox specific functions
323
324
 
325
+ @trace_method
324
326
  def run_e2b_sandbox(
325
327
  self,
326
328
  agent_state: Optional[AgentState] = None,
@@ -352,10 +354,22 @@ class ToolExecutionSandbox:
352
354
  if additional_env_vars:
353
355
  env_vars.update(additional_env_vars)
354
356
  code = self.generate_execution_script(agent_state=agent_state)
357
+ log_event(
358
+ "e2b_execution_started",
359
+ {"tool": self.tool_name, "sandbox_id": sbx.sandbox_id, "code": code, "env_vars": env_vars},
360
+ )
355
361
  execution = sbx.run_code(code, envs=env_vars)
356
362
 
357
363
  if execution.results:
358
364
  func_return, agent_state = self.parse_best_effort(execution.results[0].text)
365
+ log_event(
366
+ "e2b_execution_succeeded",
367
+ {
368
+ "tool": self.tool_name,
369
+ "sandbox_id": sbx.sandbox_id,
370
+ "func_return": func_return,
371
+ },
372
+ )
359
373
  elif execution.error:
360
374
  logger.error(f"Executing tool {self.tool_name} raised a {execution.error.name} with message: \n{execution.error.value}")
361
375
  logger.error(f"Traceback from e2b sandbox: \n{execution.error.traceback}")
@@ -363,7 +377,25 @@ class ToolExecutionSandbox:
363
377
  function_name=self.tool_name, exception_name=execution.error.name, exception_message=execution.error.value
364
378
  )
365
379
  execution.logs.stderr.append(execution.error.traceback)
380
+ log_event(
381
+ "e2b_execution_failed",
382
+ {
383
+ "tool": self.tool_name,
384
+ "sandbox_id": sbx.sandbox_id,
385
+ "error_type": execution.error.name,
386
+ "error_message": execution.error.value,
387
+ "func_return": func_return,
388
+ },
389
+ )
366
390
  else:
391
+ log_event(
392
+ "e2b_execution_empty",
393
+ {
394
+ "tool": self.tool_name,
395
+ "sandbox_id": sbx.sandbox_id,
396
+ "status": "no_results_no_error",
397
+ },
398
+ )
367
399
  raise ValueError(f"Tool {self.tool_name} returned execution with None")
368
400
 
369
401
  return ToolExecutionResult(
@@ -395,16 +427,31 @@ class ToolExecutionSandbox:
395
427
 
396
428
  return None
397
429
 
430
+ @trace_method
398
431
  def create_e2b_sandbox_with_metadata_hash(self, sandbox_config: SandboxConfig) -> "Sandbox":
399
432
  from e2b_code_interpreter import Sandbox
400
433
 
401
434
  state_hash = sandbox_config.fingerprint()
402
435
  e2b_config = sandbox_config.get_e2b_config()
436
+ log_event(
437
+ "e2b_sandbox_create_started",
438
+ {
439
+ "sandbox_fingerprint": state_hash,
440
+ "e2b_config": e2b_config.model_dump(),
441
+ },
442
+ )
403
443
  if e2b_config.template:
404
444
  sbx = Sandbox(sandbox_config.get_e2b_config().template, metadata={self.METADATA_CONFIG_STATE_KEY: state_hash})
405
445
  else:
406
446
  # no template
407
447
  sbx = Sandbox(metadata={self.METADATA_CONFIG_STATE_KEY: state_hash}, **e2b_config.model_dump(exclude={"pip_requirements"}))
448
+ log_event(
449
+ "e2b_sandbox_create_finished",
450
+ {
451
+ "sandbox_id": sbx.sandbox_id,
452
+ "sandbox_fingerprint": state_hash,
453
+ },
454
+ )
408
455
 
409
456
  # install pip requirements
410
457
  if e2b_config.pip_requirements: