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
@@ -145,7 +145,7 @@ class LettaAgentBatch(BaseAgent):
145
145
  agent_mapping = {
146
146
  agent_state.id: agent_state
147
147
  for agent_state in await self.agent_manager.get_agents_by_ids_async(
148
- agent_ids=[request.agent_id for request in batch_requests], actor=self.actor
148
+ agent_ids=[request.agent_id for request in batch_requests], include_relationships=["tools", "memory"], actor=self.actor
149
149
  )
150
150
  }
151
151
 
@@ -267,64 +267,121 @@ class LettaAgentBatch(BaseAgent):
267
267
 
268
268
  @trace_method
269
269
  async def _collect_resume_context(self, llm_batch_id: str) -> _ResumeContext:
270
- # NOTE: We only continue for items with successful results
271
- batch_items = await self.batch_manager.list_llm_batch_items_async(llm_batch_id=llm_batch_id, request_status=JobStatus.completed)
272
-
273
- agent_ids = []
274
- provider_results = {}
275
- request_status_updates: List[RequestStatusUpdateInfo] = []
270
+ """
271
+ Collect context for resuming operations from completed batch items.
276
272
 
277
- for item in batch_items:
278
- aid = item.agent_id
279
- agent_ids.append(aid)
280
- provider_results[aid] = item.batch_request_result.result
273
+ Args:
274
+ llm_batch_id: The ID of the batch to collect context for
281
275
 
282
- agent_states = await self.agent_manager.get_agents_by_ids_async(agent_ids, actor=self.actor)
283
- agent_state_map = {agent.id: agent for agent in agent_states}
276
+ Returns:
277
+ _ResumeContext object containing all necessary data for resumption
278
+ """
279
+ # Fetch only completed batch items
280
+ batch_items = await self.batch_manager.list_llm_batch_items_async(llm_batch_id=llm_batch_id, request_status=JobStatus.completed)
284
281
 
285
- name_map, args_map, cont_map = {}, {}, {}
286
- for aid in agent_ids:
287
- # status bookkeeping
288
- pr = provider_results[aid]
289
- status = (
290
- JobStatus.completed
291
- if isinstance(pr, BetaMessageBatchSucceededResult)
292
- else (
293
- JobStatus.failed
294
- if isinstance(pr, BetaMessageBatchErroredResult)
295
- else JobStatus.cancelled if isinstance(pr, BetaMessageBatchCanceledResult) else JobStatus.expired
296
- )
282
+ # Exit early if no items to process
283
+ if not batch_items:
284
+ return _ResumeContext(
285
+ batch_items=[],
286
+ agent_ids=[],
287
+ agent_state_map={},
288
+ provider_results={},
289
+ tool_call_name_map={},
290
+ tool_call_args_map={},
291
+ should_continue_map={},
292
+ request_status_updates=[],
297
293
  )
298
- request_status_updates.append(RequestStatusUpdateInfo(llm_batch_id=llm_batch_id, agent_id=aid, request_status=status))
299
294
 
300
- # translate provider‑specific response OpenAI‑style tool call (unchanged)
301
- llm_client = LLMClient.create(
302
- provider_type=item.llm_config.model_endpoint_type,
303
- put_inner_thoughts_first=True,
304
- actor=self.actor,
305
- )
306
- tool_call = (
307
- llm_client.convert_response_to_chat_completion(
308
- response_data=pr.message.model_dump(), input_messages=[], llm_config=item.llm_config
309
- )
310
- .choices[0]
311
- .message.tool_calls[0]
312
- )
295
+ # Extract agent IDs and organize items by agent ID
296
+ agent_ids = [item.agent_id for item in batch_items]
297
+ batch_item_map = {item.agent_id: item for item in batch_items}
313
298
 
314
- name, args, cont = self._extract_tool_call_and_decide_continue(tool_call, item.step_state)
315
- name_map[aid], args_map[aid], cont_map[aid] = name, args, cont
299
+ # Collect provider results
300
+ provider_results = {item.agent_id: item.batch_request_result.result for item in batch_items}
301
+
302
+ # Fetch agent states in a single call
303
+ agent_states = await self.agent_manager.get_agents_by_ids_async(
304
+ agent_ids=agent_ids, include_relationships=["tools", "memory"], actor=self.actor
305
+ )
306
+ agent_state_map = {agent.id: agent for agent in agent_states}
307
+
308
+ # Process each agent's results
309
+ tool_call_results = self._process_agent_results(
310
+ agent_ids=agent_ids, batch_item_map=batch_item_map, provider_results=provider_results, llm_batch_id=llm_batch_id
311
+ )
316
312
 
317
313
  return _ResumeContext(
318
314
  batch_items=batch_items,
319
315
  agent_ids=agent_ids,
320
316
  agent_state_map=agent_state_map,
321
317
  provider_results=provider_results,
322
- tool_call_name_map=name_map,
323
- tool_call_args_map=args_map,
324
- should_continue_map=cont_map,
325
- request_status_updates=request_status_updates,
318
+ tool_call_name_map=tool_call_results.name_map,
319
+ tool_call_args_map=tool_call_results.args_map,
320
+ should_continue_map=tool_call_results.cont_map,
321
+ request_status_updates=tool_call_results.status_updates,
322
+ )
323
+
324
+ def _process_agent_results(self, agent_ids, batch_item_map, provider_results, llm_batch_id):
325
+ """
326
+ Process the results for each agent, extracting tool calls and determining continuation status.
327
+
328
+ Returns:
329
+ A namedtuple containing name_map, args_map, cont_map, and status_updates
330
+ """
331
+ from collections import namedtuple
332
+
333
+ ToolCallResults = namedtuple("ToolCallResults", ["name_map", "args_map", "cont_map", "status_updates"])
334
+
335
+ name_map, args_map, cont_map = {}, {}, {}
336
+ request_status_updates = []
337
+
338
+ for aid in agent_ids:
339
+ item = batch_item_map[aid]
340
+ result = provider_results[aid]
341
+
342
+ # Determine job status based on result type
343
+ status = self._determine_job_status(result)
344
+ request_status_updates.append(RequestStatusUpdateInfo(llm_batch_id=llm_batch_id, agent_id=aid, request_status=status))
345
+
346
+ # Process tool calls
347
+ name, args, cont = self._extract_tool_call_from_result(item, result)
348
+ name_map[aid], args_map[aid], cont_map[aid] = name, args, cont
349
+
350
+ return ToolCallResults(name_map, args_map, cont_map, request_status_updates)
351
+
352
+ def _determine_job_status(self, result):
353
+ """Determine job status based on result type"""
354
+ if isinstance(result, BetaMessageBatchSucceededResult):
355
+ return JobStatus.completed
356
+ elif isinstance(result, BetaMessageBatchErroredResult):
357
+ return JobStatus.failed
358
+ elif isinstance(result, BetaMessageBatchCanceledResult):
359
+ return JobStatus.cancelled
360
+ else:
361
+ return JobStatus.expired
362
+
363
+ def _extract_tool_call_from_result(self, item, result):
364
+ """Extract tool call information from a result"""
365
+ llm_client = LLMClient.create(
366
+ provider_type=item.llm_config.model_endpoint_type,
367
+ put_inner_thoughts_first=True,
368
+ actor=self.actor,
369
+ )
370
+
371
+ # If result isn't a successful type, we can't extract a tool call
372
+ if not isinstance(result, BetaMessageBatchSucceededResult):
373
+ return None, None, False
374
+
375
+ tool_call = (
376
+ llm_client.convert_response_to_chat_completion(
377
+ response_data=result.message.model_dump(), input_messages=[], llm_config=item.llm_config
378
+ )
379
+ .choices[0]
380
+ .message.tool_calls[0]
326
381
  )
327
382
 
383
+ return self._extract_tool_call_and_decide_continue(tool_call, item.step_state)
384
+
328
385
  def _update_request_statuses(self, updates: List[RequestStatusUpdateInfo]) -> None:
329
386
  if updates:
330
387
  self.batch_manager.bulk_update_llm_batch_items_request_status_by_agent(updates=updates)
@@ -556,16 +613,6 @@ class LettaAgentBatch(BaseAgent):
556
613
  in_context_messages = await self._rebuild_memory_async(current_in_context_messages + new_in_context_messages, agent_state)
557
614
  return in_context_messages
558
615
 
559
- # TODO: Make this a bullk function
560
- def _rebuild_memory(
561
- self,
562
- in_context_messages: List[Message],
563
- agent_state: AgentState,
564
- num_messages: int | None = None,
565
- num_archival_memories: int | None = None,
566
- ) -> List[Message]:
567
- return super()._rebuild_memory(in_context_messages, agent_state)
568
-
569
616
  # Not used in batch.
570
617
  async def step(self, input_messages: List[MessageCreate], max_steps: int = 10) -> LettaResponse:
571
618
  raise NotImplementedError
@@ -154,7 +154,7 @@ class VoiceAgent(BaseAgent):
154
154
  # TODO: Define max steps here
155
155
  for _ in range(max_steps):
156
156
  # Rebuild memory each loop
157
- in_context_messages = self._rebuild_memory(in_context_messages, agent_state)
157
+ in_context_messages = await self._rebuild_memory_async(in_context_messages, agent_state)
158
158
  openai_messages = convert_in_context_letta_messages_to_openai(in_context_messages, exclude_system_messages=True)
159
159
  openai_messages.extend(in_memory_message_history)
160
160
 
@@ -292,14 +292,14 @@ class VoiceAgent(BaseAgent):
292
292
  agent_id=self.agent_id, message_ids=[m.id for m in new_in_context_messages], actor=self.actor
293
293
  )
294
294
 
295
- def _rebuild_memory(
295
+ async def _rebuild_memory_async(
296
296
  self,
297
297
  in_context_messages: List[Message],
298
298
  agent_state: AgentState,
299
299
  num_messages: int | None = None,
300
300
  num_archival_memories: int | None = None,
301
301
  ) -> List[Message]:
302
- return super()._rebuild_memory(
302
+ return await super()._rebuild_memory_async(
303
303
  in_context_messages, agent_state, num_messages=self.num_messages, num_archival_memories=self.num_archival_memories
304
304
  )
305
305
 
@@ -438,7 +438,7 @@ class VoiceAgent(BaseAgent):
438
438
  if start_date and end_date and start_date > end_date:
439
439
  start_date, end_date = end_date, start_date
440
440
 
441
- archival_results = self.agent_manager.list_passages(
441
+ archival_results = await self.agent_manager.list_passages_async(
442
442
  actor=self.actor,
443
443
  agent_id=self.agent_id,
444
444
  query_text=archival_query,
@@ -457,7 +457,7 @@ class VoiceAgent(BaseAgent):
457
457
  keyword_results = {}
458
458
  if convo_keyword_queries:
459
459
  for keyword in convo_keyword_queries:
460
- messages = self.message_manager.list_messages_for_agent(
460
+ messages = await self.message_manager.list_messages_for_agent_async(
461
461
  agent_id=self.agent_id,
462
462
  actor=self.actor,
463
463
  query_text=keyword,
letta/client/client.py CHANGED
@@ -2773,11 +2773,8 @@ class LocalClient(AbstractClient):
2773
2773
 
2774
2774
  # humans / personas
2775
2775
 
2776
- def get_block_id(self, name: str, label: str) -> str:
2777
- block = self.server.block_manager.get_blocks(actor=self.user, template_name=name, label=label, is_template=True)
2778
- if not block:
2779
- return None
2780
- return block[0].id
2776
+ def get_block_id(self, name: str, label: str) -> str | None:
2777
+ return None
2781
2778
 
2782
2779
  def create_human(self, name: str, text: str):
2783
2780
  """
@@ -2812,7 +2809,7 @@ class LocalClient(AbstractClient):
2812
2809
  Returns:
2813
2810
  humans (List[Human]): List of human blocks
2814
2811
  """
2815
- return self.server.block_manager.get_blocks(actor=self.user, label="human", is_template=True)
2812
+ return []
2816
2813
 
2817
2814
  def list_personas(self) -> List[Persona]:
2818
2815
  """
@@ -2821,7 +2818,7 @@ class LocalClient(AbstractClient):
2821
2818
  Returns:
2822
2819
  personas (List[Persona]): List of persona blocks
2823
2820
  """
2824
- return self.server.block_manager.get_blocks(actor=self.user, label="persona", is_template=True)
2821
+ return []
2825
2822
 
2826
2823
  def update_human(self, human_id: str, text: str):
2827
2824
  """
@@ -2879,7 +2876,7 @@ class LocalClient(AbstractClient):
2879
2876
  assert id, f"Human ID must be provided"
2880
2877
  return Human(**self.server.block_manager.get_block_by_id(id, actor=self.user).model_dump())
2881
2878
 
2882
- def get_persona_id(self, name: str) -> str:
2879
+ def get_persona_id(self, name: str) -> str | None:
2883
2880
  """
2884
2881
  Get the ID of a persona block template
2885
2882
 
@@ -2889,12 +2886,9 @@ class LocalClient(AbstractClient):
2889
2886
  Returns:
2890
2887
  id (str): ID of the persona block
2891
2888
  """
2892
- persona = self.server.block_manager.get_blocks(actor=self.user, template_name=name, label="persona", is_template=True)
2893
- if not persona:
2894
- return None
2895
- return persona[0].id
2889
+ return None
2896
2890
 
2897
- def get_human_id(self, name: str) -> str:
2891
+ def get_human_id(self, name: str) -> str | None:
2898
2892
  """
2899
2893
  Get the ID of a human block template
2900
2894
 
@@ -2904,10 +2898,7 @@ class LocalClient(AbstractClient):
2904
2898
  Returns:
2905
2899
  id (str): ID of the human block
2906
2900
  """
2907
- human = self.server.block_manager.get_blocks(actor=self.user, template_name=name, label="human", is_template=True)
2908
- if not human:
2909
- return None
2910
- return human[0].id
2901
+ return None
2911
2902
 
2912
2903
  def delete_persona(self, id: str):
2913
2904
  """
@@ -3381,7 +3372,7 @@ class LocalClient(AbstractClient):
3381
3372
  Returns:
3382
3373
  blocks (List[Block]): List of blocks
3383
3374
  """
3384
- return self.server.block_manager.get_blocks(actor=self.user, label=label, is_template=templates_only)
3375
+ return []
3385
3376
 
3386
3377
  def create_block(
3387
3378
  self, label: str, value: str, limit: Optional[int] = None, template_name: Optional[str] = None, is_template: bool = False
letta/constants.py CHANGED
@@ -19,6 +19,7 @@ MCP_TOOL_TAG_NAME_PREFIX = "mcp" # full format, mcp:server_name
19
19
  LETTA_CORE_TOOL_MODULE_NAME = "letta.functions.function_sets.base"
20
20
  LETTA_MULTI_AGENT_TOOL_MODULE_NAME = "letta.functions.function_sets.multi_agent"
21
21
  LETTA_VOICE_TOOL_MODULE_NAME = "letta.functions.function_sets.voice"
22
+ LETTA_BUILTIN_TOOL_MODULE_NAME = "letta.functions.function_sets.builtin"
22
23
 
23
24
 
24
25
  # String in the error message for when the context window is too large
@@ -83,9 +84,19 @@ BASE_VOICE_SLEEPTIME_TOOLS = [
83
84
  ]
84
85
  # Multi agent tools
85
86
  MULTI_AGENT_TOOLS = ["send_message_to_agent_and_wait_for_reply", "send_message_to_agents_matching_tags", "send_message_to_agent_async"]
87
+
88
+ # Built in tools
89
+ BUILTIN_TOOLS = ["run_code", "web_search"]
90
+
86
91
  # Set of all built-in Letta tools
87
92
  LETTA_TOOL_SET = set(
88
- BASE_TOOLS + BASE_MEMORY_TOOLS + MULTI_AGENT_TOOLS + BASE_SLEEPTIME_TOOLS + BASE_VOICE_SLEEPTIME_TOOLS + BASE_VOICE_SLEEPTIME_CHAT_TOOLS
93
+ BASE_TOOLS
94
+ + BASE_MEMORY_TOOLS
95
+ + MULTI_AGENT_TOOLS
96
+ + BASE_SLEEPTIME_TOOLS
97
+ + BASE_VOICE_SLEEPTIME_TOOLS
98
+ + BASE_VOICE_SLEEPTIME_CHAT_TOOLS
99
+ + BUILTIN_TOOLS
89
100
  )
90
101
 
91
102
  # The name of the tool used to send message to the user
@@ -179,6 +190,45 @@ LLM_MAX_TOKENS = {
179
190
  "gpt-3.5-turbo-0613": 4096, # legacy
180
191
  "gpt-3.5-turbo-16k-0613": 16385, # legacy
181
192
  "gpt-3.5-turbo-0301": 4096, # legacy
193
+ "gemini-1.0-pro-vision-latest": 12288,
194
+ "gemini-pro-vision": 12288,
195
+ "gemini-1.5-pro-latest": 2000000,
196
+ "gemini-1.5-pro-001": 2000000,
197
+ "gemini-1.5-pro-002": 2000000,
198
+ "gemini-1.5-pro": 2000000,
199
+ "gemini-1.5-flash-latest": 1000000,
200
+ "gemini-1.5-flash-001": 1000000,
201
+ "gemini-1.5-flash-001-tuning": 16384,
202
+ "gemini-1.5-flash": 1000000,
203
+ "gemini-1.5-flash-002": 1000000,
204
+ "gemini-1.5-flash-8b": 1000000,
205
+ "gemini-1.5-flash-8b-001": 1000000,
206
+ "gemini-1.5-flash-8b-latest": 1000000,
207
+ "gemini-1.5-flash-8b-exp-0827": 1000000,
208
+ "gemini-1.5-flash-8b-exp-0924": 1000000,
209
+ "gemini-2.5-pro-exp-03-25": 1048576,
210
+ "gemini-2.5-pro-preview-03-25": 1048576,
211
+ "gemini-2.5-flash-preview-04-17": 1048576,
212
+ "gemini-2.5-flash-preview-05-20": 1048576,
213
+ "gemini-2.5-flash-preview-04-17-thinking": 1048576,
214
+ "gemini-2.5-pro-preview-05-06": 1048576,
215
+ "gemini-2.0-flash-exp": 1048576,
216
+ "gemini-2.0-flash": 1048576,
217
+ "gemini-2.0-flash-001": 1048576,
218
+ "gemini-2.0-flash-exp-image-generation": 1048576,
219
+ "gemini-2.0-flash-lite-001": 1048576,
220
+ "gemini-2.0-flash-lite": 1048576,
221
+ "gemini-2.0-flash-preview-image-generation": 32768,
222
+ "gemini-2.0-flash-lite-preview-02-05": 1048576,
223
+ "gemini-2.0-flash-lite-preview": 1048576,
224
+ "gemini-2.0-pro-exp": 1048576,
225
+ "gemini-2.0-pro-exp-02-05": 1048576,
226
+ "gemini-exp-1206": 1048576,
227
+ "gemini-2.0-flash-thinking-exp-01-21": 1048576,
228
+ "gemini-2.0-flash-thinking-exp": 1048576,
229
+ "gemini-2.0-flash-thinking-exp-1219": 1048576,
230
+ "gemini-2.5-flash-preview-tts": 32768,
231
+ "gemini-2.5-pro-preview-tts": 65536,
182
232
  }
183
233
  # The error message that Letta will receive
184
234
  # MESSAGE_SUMMARY_WARNING_STR = f"Warning: the conversation history will soon reach its maximum length and be trimmed. Make sure to save any important information from the conversation to your memory before it is removed."
@@ -230,3 +280,7 @@ RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE = 5
230
280
 
231
281
  MAX_FILENAME_LENGTH = 255
232
282
  RESERVED_FILENAMES = {"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "LPT1", "LPT2"}
283
+
284
+ WEB_SEARCH_CLIP_CONTENT = False
285
+ WEB_SEARCH_INCLUDE_SCORE = False
286
+ WEB_SEARCH_SEPARATOR = "\n" + "-" * 40 + "\n"
@@ -0,0 +1,27 @@
1
+ from typing import Literal
2
+
3
+
4
+ async def web_search(query: str) -> str:
5
+ """
6
+ Search the web for information.
7
+ Args:
8
+ query (str): The query to search the web for.
9
+ Returns:
10
+ str: The search results.
11
+ """
12
+
13
+ raise NotImplementedError("This is only available on the latest agent architecture. Please contact the Letta team.")
14
+
15
+
16
+ def run_code(code: str, language: Literal["python", "js", "ts", "r", "java"]) -> str:
17
+ """
18
+ Run code in a sandbox. Supports Python, Javascript, Typescript, R, and Java.
19
+
20
+ Args:
21
+ code (str): The code to run.
22
+ language (Literal["python", "js", "ts", "r", "java"]): The language of the code.
23
+ Returns:
24
+ str: The output of the code, the stdout, the stderr, and error traces (if any).
25
+ """
26
+
27
+ raise NotImplementedError("This is only available on the latest agent architecture. Please contact the Letta team.")
@@ -190,7 +190,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
190
190
  prior_messages = []
191
191
  if self.group.sleeptime_agent_frequency:
192
192
  try:
193
- prior_messages = self.message_manager.list_messages_for_agent(
193
+ prior_messages = await self.message_manager.list_messages_for_agent_async(
194
194
  agent_id=foreground_agent_id,
195
195
  actor=self.actor,
196
196
  after=last_processed_message_id,
@@ -1,3 +1,4 @@
1
+ import json
1
2
  from datetime import datetime, timezone
2
3
  from enum import Enum
3
4
  from typing import AsyncGenerator, List, Union
@@ -74,6 +75,7 @@ class AnthropicStreamingInterface:
74
75
  # usage trackers
75
76
  self.input_tokens = 0
76
77
  self.output_tokens = 0
78
+ self.model = None
77
79
 
78
80
  # reasoning object trackers
79
81
  self.reasoning_messages = []
@@ -88,7 +90,13 @@ class AnthropicStreamingInterface:
88
90
 
89
91
  def get_tool_call_object(self) -> ToolCall:
90
92
  """Useful for agent loop"""
91
- return ToolCall(id=self.tool_call_id, function=FunctionCall(arguments=self.accumulated_tool_call_args, name=self.tool_call_name))
93
+ # hack for tool rules
94
+ tool_input = json.loads(self.accumulated_tool_call_args)
95
+ if "id" in tool_input and tool_input["id"].startswith("toolu_") and "function" in tool_input:
96
+ arguments = str(json.dumps(tool_input["function"]["arguments"], indent=2))
97
+ else:
98
+ arguments = self.accumulated_tool_call_args
99
+ return ToolCall(id=self.tool_call_id, function=FunctionCall(arguments=arguments, name=self.tool_call_name))
92
100
 
93
101
  def _check_inner_thoughts_complete(self, combined_args: str) -> bool:
94
102
  """
@@ -311,6 +319,7 @@ class AnthropicStreamingInterface:
311
319
  self.message_id = event.message.id
312
320
  self.input_tokens += event.message.usage.input_tokens
313
321
  self.output_tokens += event.message.usage.output_tokens
322
+ self.model = event.message.model
314
323
  elif isinstance(event, BetaRawMessageDeltaEvent):
315
324
  self.output_tokens += event.usage.output_tokens
316
325
  elif isinstance(event, BetaRawMessageStopEvent):
@@ -40,6 +40,9 @@ class OpenAIStreamingInterface:
40
40
  self.letta_assistant_message_id = Message.generate_id()
41
41
  self.letta_tool_message_id = Message.generate_id()
42
42
 
43
+ self.message_id = None
44
+ self.model = None
45
+
43
46
  # token counters
44
47
  self.input_tokens = 0
45
48
  self.output_tokens = 0
@@ -69,10 +72,14 @@ class OpenAIStreamingInterface:
69
72
  prev_message_type = None
70
73
  message_index = 0
71
74
  async for chunk in stream:
75
+ if not self.model or not self.message_id:
76
+ self.model = chunk.model
77
+ self.message_id = chunk.id
78
+
72
79
  # track usage
73
80
  if chunk.usage:
74
- self.input_tokens += len(chunk.usage.prompt_tokens)
75
- self.output_tokens += len(chunk.usage.completion_tokens)
81
+ self.input_tokens += chunk.usage.prompt_tokens
82
+ self.output_tokens += chunk.usage.completion_tokens
76
83
 
77
84
  if chunk.choices:
78
85
  choice = chunk.choices[0]
@@ -134,13 +134,13 @@ def anthropic_check_valid_api_key(api_key: Union[str, None]) -> None:
134
134
 
135
135
 
136
136
  def antropic_get_model_context_window(url: str, api_key: Union[str, None], model: str) -> int:
137
- for model_dict in anthropic_get_model_list(url=url, api_key=api_key):
137
+ for model_dict in anthropic_get_model_list(api_key=api_key):
138
138
  if model_dict["name"] == model:
139
139
  return model_dict["context_window"]
140
140
  raise ValueError(f"Can't find model '{model}' in Anthropic model list")
141
141
 
142
142
 
143
- def anthropic_get_model_list(url: str, api_key: Union[str, None]) -> dict:
143
+ def anthropic_get_model_list(api_key: Optional[str]) -> dict:
144
144
  """https://docs.anthropic.com/claude/docs/models-overview"""
145
145
 
146
146
  # NOTE: currently there is no GET /models, so we need to hardcode
@@ -159,6 +159,25 @@ def anthropic_get_model_list(url: str, api_key: Union[str, None]) -> dict:
159
159
  return models_json["data"]
160
160
 
161
161
 
162
+ async def anthropic_get_model_list_async(api_key: Optional[str]) -> dict:
163
+ """https://docs.anthropic.com/claude/docs/models-overview"""
164
+
165
+ # NOTE: currently there is no GET /models, so we need to hardcode
166
+ # return MODEL_LIST
167
+
168
+ if api_key:
169
+ anthropic_client = anthropic.AsyncAnthropic(api_key=api_key)
170
+ elif model_settings.anthropic_api_key:
171
+ anthropic_client = anthropic.AsyncAnthropic()
172
+ else:
173
+ raise ValueError("No API key provided")
174
+
175
+ models = await anthropic_client.models.list()
176
+ models_json = models.model_dump()
177
+ assert "data" in models_json, f"Anthropic model query response missing 'data' field: {models_json}"
178
+ return models_json["data"]
179
+
180
+
162
181
  def convert_tools_to_anthropic_format(tools: List[Tool]) -> List[dict]:
163
182
  """See: https://docs.anthropic.com/claude/docs/tool-use
164
183
 
@@ -35,6 +35,7 @@ from letta.schemas.openai.chat_completion_response import ChatCompletionResponse
35
35
  from letta.schemas.openai.chat_completion_response import Message as ChoiceMessage
36
36
  from letta.schemas.openai.chat_completion_response import ToolCall, UsageStatistics
37
37
  from letta.services.provider_manager import ProviderManager
38
+ from letta.settings import model_settings
38
39
  from letta.tracing import trace_method
39
40
 
40
41
  DUMMY_FIRST_USER_MESSAGE = "User initializing bootup sequence."
@@ -120,8 +121,16 @@ class AnthropicClient(LLMClientBase):
120
121
  override_key = ProviderManager().get_override_key(llm_config.provider_name, actor=self.actor)
121
122
 
122
123
  if async_client:
123
- return anthropic.AsyncAnthropic(api_key=override_key) if override_key else anthropic.AsyncAnthropic()
124
- return anthropic.Anthropic(api_key=override_key) if override_key else anthropic.Anthropic()
124
+ return (
125
+ anthropic.AsyncAnthropic(api_key=override_key, max_retries=model_settings.anthropic_max_retries)
126
+ if override_key
127
+ else anthropic.AsyncAnthropic(max_retries=model_settings.anthropic_max_retries)
128
+ )
129
+ return (
130
+ anthropic.Anthropic(api_key=override_key, max_retries=model_settings.anthropic_max_retries)
131
+ if override_key
132
+ else anthropic.Anthropic(max_retries=model_settings.anthropic_max_retries)
133
+ )
125
134
 
126
135
  @trace_method
127
136
  def build_request_data(
@@ -239,6 +248,24 @@ class AnthropicClient(LLMClientBase):
239
248
 
240
249
  return data
241
250
 
251
+ async def count_tokens(self, messages: List[dict] = None, model: str = None, tools: List[Tool] = None) -> int:
252
+ client = anthropic.AsyncAnthropic()
253
+ if messages and len(messages) == 0:
254
+ messages = None
255
+ if tools and len(tools) > 0:
256
+ anthropic_tools = convert_tools_to_anthropic_format(tools)
257
+ else:
258
+ anthropic_tools = None
259
+ result = await client.beta.messages.count_tokens(
260
+ model=model or "claude-3-7-sonnet-20250219",
261
+ messages=messages or [{"role": "user", "content": "hi"}],
262
+ tools=anthropic_tools or [],
263
+ )
264
+ token_count = result.input_tokens
265
+ if messages is None:
266
+ token_count -= 8
267
+ return token_count
268
+
242
269
  def handle_llm_error(self, e: Exception) -> Exception:
243
270
  if isinstance(e, anthropic.APIConnectionError):
244
271
  logger.warning(f"[Anthropic] API connection error: {e.__cause__}")
@@ -369,11 +396,11 @@ class AnthropicClient(LLMClientBase):
369
396
  content = strip_xml_tags(string=content_part.text, tag="thinking")
370
397
  if content_part.type == "tool_use":
371
398
  # hack for tool rules
372
- input = json.loads(json.dumps(content_part.input))
373
- if "id" in input and input["id"].startswith("toolu_") and "function" in input:
374
- arguments = str(input["function"]["arguments"])
399
+ tool_input = json.loads(json.dumps(content_part.input))
400
+ if "id" in tool_input and tool_input["id"].startswith("toolu_") and "function" in tool_input:
401
+ arguments = str(tool_input["function"]["arguments"])
375
402
  else:
376
- arguments = json.dumps(content_part.input, indent=2)
403
+ arguments = json.dumps(tool_input, indent=2)
377
404
  tool_calls = [
378
405
  ToolCall(
379
406
  id=content_part.id,