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.
- letta/__init__.py +1 -1
- letta/agent.py +290 -3
- letta/agents/base_agent.py +0 -55
- letta/agents/helpers.py +5 -0
- letta/agents/letta_agent.py +314 -64
- letta/agents/letta_agent_batch.py +102 -55
- letta/agents/voice_agent.py +5 -5
- letta/client/client.py +9 -18
- letta/constants.py +55 -1
- letta/functions/function_sets/builtin.py +27 -0
- letta/groups/sleeptime_multi_agent_v2.py +1 -1
- letta/interfaces/anthropic_streaming_interface.py +10 -1
- letta/interfaces/openai_streaming_interface.py +9 -2
- letta/llm_api/anthropic.py +21 -2
- letta/llm_api/anthropic_client.py +33 -6
- letta/llm_api/google_ai_client.py +136 -423
- letta/llm_api/google_vertex_client.py +173 -22
- letta/llm_api/llm_api_tools.py +27 -0
- letta/llm_api/llm_client.py +1 -1
- letta/llm_api/llm_client_base.py +32 -21
- letta/llm_api/openai.py +57 -0
- letta/llm_api/openai_client.py +7 -11
- letta/memory.py +0 -1
- letta/orm/__init__.py +1 -0
- letta/orm/enums.py +1 -0
- letta/orm/provider_trace.py +26 -0
- letta/orm/step.py +1 -0
- letta/schemas/provider_trace.py +43 -0
- letta/schemas/providers.py +210 -65
- letta/schemas/step.py +1 -0
- letta/schemas/tool.py +4 -0
- letta/server/db.py +37 -19
- letta/server/rest_api/routers/v1/__init__.py +2 -0
- letta/server/rest_api/routers/v1/agents.py +57 -34
- letta/server/rest_api/routers/v1/blocks.py +3 -3
- letta/server/rest_api/routers/v1/identities.py +24 -26
- letta/server/rest_api/routers/v1/jobs.py +3 -3
- letta/server/rest_api/routers/v1/llms.py +13 -8
- letta/server/rest_api/routers/v1/sandbox_configs.py +6 -6
- letta/server/rest_api/routers/v1/tags.py +3 -3
- letta/server/rest_api/routers/v1/telemetry.py +18 -0
- letta/server/rest_api/routers/v1/tools.py +6 -6
- letta/server/rest_api/streaming_response.py +105 -0
- letta/server/rest_api/utils.py +4 -0
- letta/server/server.py +140 -1
- letta/services/agent_manager.py +251 -18
- letta/services/block_manager.py +52 -37
- letta/services/helpers/noop_helper.py +10 -0
- letta/services/identity_manager.py +43 -38
- letta/services/job_manager.py +29 -0
- letta/services/message_manager.py +111 -0
- letta/services/sandbox_config_manager.py +36 -0
- letta/services/step_manager.py +146 -0
- letta/services/telemetry_manager.py +58 -0
- letta/services/tool_executor/tool_execution_manager.py +49 -5
- letta/services/tool_executor/tool_execution_sandbox.py +47 -0
- letta/services/tool_executor/tool_executor.py +236 -7
- letta/services/tool_manager.py +160 -1
- letta/services/tool_sandbox/e2b_sandbox.py +65 -3
- letta/settings.py +10 -2
- letta/tracing.py +5 -5
- {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250522104246.dist-info}/METADATA +3 -2
- {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250522104246.dist-info}/RECORD +66 -59
- {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250522104246.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250522104246.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250522104246.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
|
-
|
271
|
-
|
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
|
-
|
278
|
-
|
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
|
-
|
283
|
-
|
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
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
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
|
-
|
301
|
-
|
302
|
-
|
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
|
-
|
315
|
-
|
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=
|
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
|
letta/agents/voice_agent.py
CHANGED
@@ -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.
|
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
|
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().
|
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.
|
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.
|
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
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
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.
|
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
|
-
|
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 +=
|
75
|
-
self.output_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]
|
letta/llm_api/anthropic.py
CHANGED
@@ -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(
|
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(
|
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
|
124
|
-
|
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
|
-
|
373
|
-
if "id" in
|
374
|
-
arguments = str(
|
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(
|
403
|
+
arguments = json.dumps(tool_input, indent=2)
|
377
404
|
tool_calls = [
|
378
405
|
ToolCall(
|
379
406
|
id=content_part.id,
|