letta-nightly 0.7.29.dev20250602104315__py3-none-any.whl → 0.8.0.dev20250604104349__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 +7 -1
- letta/agent.py +16 -9
- letta/agents/base_agent.py +1 -0
- letta/agents/ephemeral_summary_agent.py +104 -0
- letta/agents/helpers.py +35 -3
- letta/agents/letta_agent.py +492 -176
- letta/agents/letta_agent_batch.py +22 -16
- letta/agents/prompts/summary_system_prompt.txt +62 -0
- letta/agents/voice_agent.py +22 -7
- letta/agents/voice_sleeptime_agent.py +13 -8
- letta/constants.py +33 -1
- letta/data_sources/connectors.py +52 -36
- letta/errors.py +4 -0
- letta/functions/ast_parsers.py +13 -30
- letta/functions/function_sets/base.py +3 -1
- letta/functions/functions.py +2 -0
- letta/functions/mcp_client/base_client.py +151 -97
- letta/functions/mcp_client/sse_client.py +49 -31
- letta/functions/mcp_client/stdio_client.py +107 -106
- letta/functions/schema_generator.py +22 -22
- letta/groups/helpers.py +3 -4
- letta/groups/sleeptime_multi_agent.py +4 -4
- letta/groups/sleeptime_multi_agent_v2.py +22 -0
- letta/helpers/composio_helpers.py +16 -0
- letta/helpers/converters.py +20 -0
- letta/helpers/datetime_helpers.py +1 -6
- letta/helpers/tool_rule_solver.py +2 -1
- letta/interfaces/anthropic_streaming_interface.py +17 -2
- letta/interfaces/openai_chat_completions_streaming_interface.py +1 -0
- letta/interfaces/openai_streaming_interface.py +18 -2
- letta/jobs/llm_batch_job_polling.py +1 -1
- letta/jobs/scheduler.py +1 -1
- letta/llm_api/anthropic_client.py +24 -3
- letta/llm_api/google_ai_client.py +0 -15
- letta/llm_api/google_vertex_client.py +6 -5
- letta/llm_api/llm_client_base.py +15 -0
- letta/llm_api/openai.py +2 -2
- letta/llm_api/openai_client.py +60 -8
- letta/orm/__init__.py +2 -0
- letta/orm/agent.py +45 -43
- letta/orm/base.py +0 -2
- letta/orm/block.py +1 -0
- letta/orm/custom_columns.py +13 -0
- letta/orm/enums.py +5 -0
- letta/orm/file.py +3 -1
- letta/orm/files_agents.py +68 -0
- letta/orm/mcp_server.py +48 -0
- letta/orm/message.py +1 -0
- letta/orm/organization.py +11 -2
- letta/orm/passage.py +25 -10
- letta/orm/sandbox_config.py +5 -2
- letta/orm/sqlalchemy_base.py +171 -110
- letta/prompts/system/memgpt_base.txt +6 -1
- letta/prompts/system/memgpt_v2_chat.txt +57 -0
- letta/prompts/system/sleeptime.txt +2 -0
- letta/prompts/system/sleeptime_v2.txt +28 -0
- letta/schemas/agent.py +87 -20
- letta/schemas/block.py +7 -1
- letta/schemas/file.py +57 -0
- letta/schemas/mcp.py +74 -0
- letta/schemas/memory.py +5 -2
- letta/schemas/message.py +9 -0
- letta/schemas/openai/openai.py +0 -6
- letta/schemas/providers.py +33 -4
- letta/schemas/tool.py +26 -21
- letta/schemas/tool_execution_result.py +5 -0
- letta/server/db.py +23 -8
- letta/server/rest_api/app.py +73 -56
- letta/server/rest_api/interface.py +4 -4
- letta/server/rest_api/routers/v1/agents.py +132 -47
- letta/server/rest_api/routers/v1/blocks.py +3 -2
- letta/server/rest_api/routers/v1/embeddings.py +3 -3
- letta/server/rest_api/routers/v1/groups.py +3 -3
- letta/server/rest_api/routers/v1/jobs.py +14 -17
- letta/server/rest_api/routers/v1/organizations.py +10 -10
- letta/server/rest_api/routers/v1/providers.py +12 -10
- letta/server/rest_api/routers/v1/runs.py +3 -3
- letta/server/rest_api/routers/v1/sandbox_configs.py +12 -12
- letta/server/rest_api/routers/v1/sources.py +108 -43
- letta/server/rest_api/routers/v1/steps.py +8 -6
- letta/server/rest_api/routers/v1/tools.py +134 -95
- letta/server/rest_api/utils.py +12 -1
- letta/server/server.py +272 -73
- letta/services/agent_manager.py +246 -313
- letta/services/block_manager.py +30 -9
- letta/services/context_window_calculator/__init__.py +0 -0
- letta/services/context_window_calculator/context_window_calculator.py +150 -0
- letta/services/context_window_calculator/token_counter.py +82 -0
- letta/services/file_processor/__init__.py +0 -0
- letta/services/file_processor/chunker/__init__.py +0 -0
- letta/services/file_processor/chunker/llama_index_chunker.py +29 -0
- letta/services/file_processor/embedder/__init__.py +0 -0
- letta/services/file_processor/embedder/openai_embedder.py +84 -0
- letta/services/file_processor/file_processor.py +123 -0
- letta/services/file_processor/parser/__init__.py +0 -0
- letta/services/file_processor/parser/base_parser.py +9 -0
- letta/services/file_processor/parser/mistral_parser.py +54 -0
- letta/services/file_processor/types.py +0 -0
- letta/services/files_agents_manager.py +184 -0
- letta/services/group_manager.py +118 -0
- letta/services/helpers/agent_manager_helper.py +76 -21
- letta/services/helpers/tool_execution_helper.py +3 -0
- letta/services/helpers/tool_parser_helper.py +100 -0
- letta/services/identity_manager.py +44 -42
- letta/services/job_manager.py +21 -10
- letta/services/mcp/base_client.py +5 -2
- letta/services/mcp/sse_client.py +3 -5
- letta/services/mcp/stdio_client.py +3 -5
- letta/services/mcp_manager.py +281 -0
- letta/services/message_manager.py +40 -26
- letta/services/organization_manager.py +55 -19
- letta/services/passage_manager.py +211 -13
- letta/services/provider_manager.py +48 -2
- letta/services/sandbox_config_manager.py +105 -0
- letta/services/source_manager.py +4 -5
- letta/services/step_manager.py +9 -6
- letta/services/summarizer/summarizer.py +50 -23
- letta/services/telemetry_manager.py +7 -0
- letta/services/tool_executor/tool_execution_manager.py +11 -52
- letta/services/tool_executor/tool_execution_sandbox.py +4 -34
- letta/services/tool_executor/tool_executor.py +107 -105
- letta/services/tool_manager.py +56 -17
- letta/services/tool_sandbox/base.py +39 -92
- letta/services/tool_sandbox/e2b_sandbox.py +16 -11
- letta/services/tool_sandbox/local_sandbox.py +51 -23
- letta/services/user_manager.py +36 -3
- letta/settings.py +10 -3
- letta/templates/__init__.py +0 -0
- letta/templates/sandbox_code_file.py.j2 +47 -0
- letta/templates/template_helper.py +16 -0
- letta/tracing.py +30 -1
- letta/types/__init__.py +7 -0
- letta/utils.py +25 -1
- {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/METADATA +7 -2
- {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/RECORD +138 -112
- {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/entry_points.txt +0 -0
@@ -27,6 +27,7 @@ from letta.schemas.llm_batch_job import LLMBatchItem
|
|
27
27
|
from letta.schemas.message import Message, MessageCreate
|
28
28
|
from letta.schemas.openai.chat_completion_response import ToolCall as OpenAIToolCall
|
29
29
|
from letta.schemas.sandbox_config import SandboxConfig, SandboxType
|
30
|
+
from letta.schemas.tool_execution_result import ToolExecutionResult
|
30
31
|
from letta.schemas.user import User
|
31
32
|
from letta.server.rest_api.utils import create_heartbeat_system_message, create_letta_messages_from_llm_response
|
32
33
|
from letta.services.agent_manager import AgentManager
|
@@ -66,15 +67,17 @@ class _ResumeContext:
|
|
66
67
|
request_status_updates: List[RequestStatusUpdateInfo]
|
67
68
|
|
68
69
|
|
69
|
-
async def execute_tool_wrapper(params: ToolExecutionParams) ->
|
70
|
+
async def execute_tool_wrapper(params: ToolExecutionParams) -> tuple[str, ToolExecutionResult]:
|
70
71
|
"""
|
71
72
|
Executes the tool in an out‑of‑process worker and returns:
|
72
73
|
(agent_id, (tool_result:str, success_flag:bool))
|
73
74
|
"""
|
75
|
+
from letta.schemas.tool_execution_result import ToolExecutionResult
|
76
|
+
|
74
77
|
# locate the tool on the agent
|
75
78
|
target_tool = next((t for t in params.agent_state.tools if t.name == params.tool_call_name), None)
|
76
79
|
if not target_tool:
|
77
|
-
return params.agent_id, (f"Tool not found: {params.tool_call_name}",
|
80
|
+
return params.agent_id, ToolExecutionResult(func_return=f"Tool not found: {params.tool_call_name}", status="error")
|
78
81
|
|
79
82
|
try:
|
80
83
|
mgr = ToolExecutionManager(
|
@@ -88,9 +91,9 @@ async def execute_tool_wrapper(params: ToolExecutionParams) -> Tuple[str, Tuple[
|
|
88
91
|
function_args=params.tool_args,
|
89
92
|
tool=target_tool,
|
90
93
|
)
|
91
|
-
return params.agent_id,
|
94
|
+
return params.agent_id, tool_execution_result
|
92
95
|
except Exception as e:
|
93
|
-
return params.agent_id, (f"Failed to call tool. Error: {e}",
|
96
|
+
return params.agent_id, ToolExecutionResult(func_return=f"Failed to call tool. Error: {e}", status="error")
|
94
97
|
|
95
98
|
|
96
99
|
# TODO: Limitations ->
|
@@ -245,7 +248,7 @@ class LettaAgentBatch(BaseAgent):
|
|
245
248
|
await self._mark_steps_complete_async(llm_batch_id, ctx.agent_ids)
|
246
249
|
|
247
250
|
log_event(name="prepare_next")
|
248
|
-
next_reqs, next_step_state = self.
|
251
|
+
next_reqs, next_step_state = await self._prepare_next_iteration_async(exec_results, ctx, msg_map)
|
249
252
|
if len(next_reqs) == 0:
|
250
253
|
await self.job_manager.update_job_by_id_async(
|
251
254
|
job_id=letta_batch_id, job_update=JobUpdate(status=JobStatus.completed), actor=self.actor
|
@@ -393,7 +396,7 @@ class LettaAgentBatch(BaseAgent):
|
|
393
396
|
return cfg, env
|
394
397
|
|
395
398
|
@trace_method
|
396
|
-
async def _execute_tools(self, ctx: _ResumeContext) -> Sequence[
|
399
|
+
async def _execute_tools(self, ctx: _ResumeContext) -> Sequence[tuple[str, ToolExecutionResult]]:
|
397
400
|
sbx_cfg, sbx_env = await self._build_sandbox()
|
398
401
|
rethink_memory_tool_name = "rethink_memory"
|
399
402
|
tool_params = []
|
@@ -424,7 +427,7 @@ class LettaAgentBatch(BaseAgent):
|
|
424
427
|
return await pool.map(execute_tool_wrapper, tool_params)
|
425
428
|
|
426
429
|
@trace_method
|
427
|
-
async def _bulk_rethink_memory_async(self, params: List[ToolExecutionParams]) -> Sequence[
|
430
|
+
async def _bulk_rethink_memory_async(self, params: List[ToolExecutionParams]) -> Sequence[tuple[str, ToolExecutionResult]]:
|
428
431
|
updates = {}
|
429
432
|
result = []
|
430
433
|
for param in params:
|
@@ -443,7 +446,7 @@ class LettaAgentBatch(BaseAgent):
|
|
443
446
|
updates[block_id] = new_value
|
444
447
|
|
445
448
|
# TODO: This is quite ugly and confusing - this is mostly to align with the returns of other tools
|
446
|
-
result.append((param.agent_id, (""
|
449
|
+
result.append((param.agent_id, ToolExecutionResult(status="success")))
|
447
450
|
|
448
451
|
await self.block_manager.bulk_update_block_values_async(updates=updates, actor=self.actor)
|
449
452
|
|
@@ -451,7 +454,7 @@ class LettaAgentBatch(BaseAgent):
|
|
451
454
|
|
452
455
|
async def _persist_tool_messages(
|
453
456
|
self,
|
454
|
-
exec_results: Sequence[Tuple[str,
|
457
|
+
exec_results: Sequence[Tuple[str, "ToolExecutionResult"]],
|
455
458
|
ctx: _ResumeContext,
|
456
459
|
) -> Dict[str, List[Message]]:
|
457
460
|
# TODO: This is redundant, we should have this ready on the ctx
|
@@ -459,14 +462,15 @@ class LettaAgentBatch(BaseAgent):
|
|
459
462
|
agent_item_map: Dict[str, LLMBatchItem] = {item.agent_id: item for item in ctx.batch_items}
|
460
463
|
|
461
464
|
msg_map: Dict[str, List[Message]] = {}
|
462
|
-
for aid,
|
465
|
+
for aid, tool_exec_result in exec_results:
|
463
466
|
msgs = self._create_tool_call_messages(
|
464
467
|
llm_batch_item_id=agent_item_map[aid].id,
|
465
468
|
agent_state=ctx.agent_state_map[aid],
|
466
469
|
tool_call_name=ctx.tool_call_name_map[aid],
|
467
470
|
tool_call_args=ctx.tool_call_args_map[aid],
|
468
|
-
tool_exec_result=
|
469
|
-
success_flag=
|
471
|
+
tool_exec_result=tool_exec_result.func_return,
|
472
|
+
success_flag=tool_exec_result.success_flag,
|
473
|
+
tool_exec_result_obj=tool_exec_result,
|
470
474
|
reasoning_content=None,
|
471
475
|
)
|
472
476
|
msg_map[aid] = msgs
|
@@ -480,16 +484,16 @@ class LettaAgentBatch(BaseAgent):
|
|
480
484
|
]
|
481
485
|
await self.batch_manager.bulk_update_llm_batch_items_step_status_by_agent_async(updates)
|
482
486
|
|
483
|
-
def
|
487
|
+
async def _prepare_next_iteration_async(
|
484
488
|
self,
|
485
|
-
exec_results: Sequence[Tuple[str,
|
489
|
+
exec_results: Sequence[Tuple[str, "ToolExecutionResult"]],
|
486
490
|
ctx: _ResumeContext,
|
487
491
|
msg_map: Dict[str, List[Message]],
|
488
492
|
) -> Tuple[List[LettaBatchRequest], Dict[str, AgentStepState]]:
|
489
493
|
# who continues?
|
490
494
|
continues = [aid for aid, cont in ctx.should_continue_map.items() if cont]
|
491
495
|
|
492
|
-
success_flag_map = {aid:
|
496
|
+
success_flag_map = {aid: result.success_flag for aid, result in exec_results}
|
493
497
|
|
494
498
|
batch_reqs: List[LettaBatchRequest] = []
|
495
499
|
for aid in continues:
|
@@ -509,7 +513,7 @@ class LettaAgentBatch(BaseAgent):
|
|
509
513
|
for aid, new_msgs in msg_map.items():
|
510
514
|
ast = ctx.agent_state_map[aid]
|
511
515
|
if not ast.message_buffer_autoclear:
|
512
|
-
self.agent_manager.
|
516
|
+
await self.agent_manager.set_in_context_messages_async(
|
513
517
|
agent_id=aid,
|
514
518
|
message_ids=ast.message_ids + [m.id for m in new_msgs],
|
515
519
|
actor=self.actor,
|
@@ -528,6 +532,7 @@ class LettaAgentBatch(BaseAgent):
|
|
528
532
|
tool_call_name: str,
|
529
533
|
tool_call_args: Dict[str, Any],
|
530
534
|
tool_exec_result: str,
|
535
|
+
tool_exec_result_obj: "ToolExecutionResult",
|
531
536
|
success_flag: bool,
|
532
537
|
reasoning_content: Optional[List[Union[TextContent, ReasoningContent, RedactedReasoningContent, OmittedReasoningContent]]] = None,
|
533
538
|
) -> List[Message]:
|
@@ -541,6 +546,7 @@ class LettaAgentBatch(BaseAgent):
|
|
541
546
|
tool_call_id=tool_call_id,
|
542
547
|
function_call_success=success_flag,
|
543
548
|
function_response=tool_exec_result,
|
549
|
+
tool_execution_result=tool_exec_result_obj,
|
544
550
|
actor=self.actor,
|
545
551
|
add_heartbeat_request_system_message=False,
|
546
552
|
reasoning_content=reasoning_content,
|
@@ -0,0 +1,62 @@
|
|
1
|
+
You are a memory-recall assistant that preserves conversational context as messages exit the AI's context window.
|
2
|
+
|
3
|
+
<core_function>
|
4
|
+
Extract and preserve information that would be lost when messages are evicted, enabling continuity across conversations.
|
5
|
+
</core_function>
|
6
|
+
|
7
|
+
<detail_adaptation>
|
8
|
+
Analyze content type and apply appropriate detail level:
|
9
|
+
|
10
|
+
<high_detail>
|
11
|
+
Apply to: episodic content, code, artifacts, documents, technical discussions
|
12
|
+
- Capture specific facts, sequences, and technical details
|
13
|
+
- Preserve exact names, dates, numbers, specifications
|
14
|
+
- Document code snippets, artifact IDs, document structures
|
15
|
+
- Note precise steps in procedures or narratives
|
16
|
+
- Include verbatim quotes for critical commitments
|
17
|
+
</high_detail>
|
18
|
+
|
19
|
+
<medium_detail>
|
20
|
+
Apply to: ongoing projects, established preferences, multi-message threads
|
21
|
+
- Summarize key decisions, milestones, progress
|
22
|
+
- Record personal preferences and patterns
|
23
|
+
- Track commitments and action items
|
24
|
+
- Maintain project context and dependencies
|
25
|
+
</medium_detail>
|
26
|
+
|
27
|
+
<low_detail>
|
28
|
+
Apply to: high-level discussions, philosophical topics, general preferences
|
29
|
+
- Capture main themes and conclusions
|
30
|
+
- Note relationship dynamics and communication style
|
31
|
+
- Summarize positions and general goals
|
32
|
+
- Record broad aspirations
|
33
|
+
</low_detail>
|
34
|
+
</detail_adaptation>
|
35
|
+
|
36
|
+
<information_priority>
|
37
|
+
<critical>Commitments, deadlines, medical/legal information, explicit requests</critical>
|
38
|
+
<important>Personal details, project status, technical specifications, decisions</important>
|
39
|
+
<contextual>Preferences, opinions, relationship dynamics, emotional tone</contextual>
|
40
|
+
<background>General topics, themes, conversational patterns</background>
|
41
|
+
</information_priority>
|
42
|
+
|
43
|
+
<format_rules>
|
44
|
+
- Use bullet points for discrete facts
|
45
|
+
- Write prose for narratives or complex relationships
|
46
|
+
- **Bold** key terms and identifiers
|
47
|
+
- Include temporal markers: [ongoing], [mentioned DATE], [since TIME]
|
48
|
+
- Group under clear headers when multiple topics present
|
49
|
+
- Use consistent terminology for searchability
|
50
|
+
</format_rules>
|
51
|
+
|
52
|
+
<exclusions>
|
53
|
+
- Information in remaining context
|
54
|
+
- Generic pleasantries
|
55
|
+
- Inferrable details
|
56
|
+
- Redundant restatements
|
57
|
+
- Conversational filler
|
58
|
+
</exclusions>
|
59
|
+
|
60
|
+
<critical_reminder>
|
61
|
+
Your notes are the sole record of evicted messages. Every word should enable future continuity.
|
62
|
+
</critical_reminder>
|
letta/agents/voice_agent.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import json
|
2
2
|
import uuid
|
3
3
|
from datetime import datetime, timedelta, timezone
|
4
|
-
from typing import Any, AsyncGenerator, Dict, List, Optional
|
4
|
+
from typing import Any, AsyncGenerator, Dict, List, Optional
|
5
5
|
|
6
6
|
import openai
|
7
7
|
|
@@ -118,6 +118,7 @@ class VoiceAgent(BaseAgent):
|
|
118
118
|
Main streaming loop that yields partial tokens.
|
119
119
|
Whenever we detect a tool call, we yield from _handle_ai_response as well.
|
120
120
|
"""
|
121
|
+
print("CALL STREAM")
|
121
122
|
if len(input_messages) != 1 or input_messages[0].role != MessageRole.user:
|
122
123
|
raise ValueError(f"Voice Agent was invoked with multiple input messages or message did not have role `user`: {input_messages}")
|
123
124
|
|
@@ -238,14 +239,17 @@ class VoiceAgent(BaseAgent):
|
|
238
239
|
)
|
239
240
|
in_memory_message_history.append(assistant_tool_call_msg.model_dump())
|
240
241
|
|
241
|
-
|
242
|
+
tool_execution_result = await self._execute_tool(
|
242
243
|
user_query=user_query,
|
243
244
|
tool_name=tool_call_name,
|
244
245
|
tool_args=tool_args,
|
245
246
|
agent_state=agent_state,
|
246
247
|
)
|
248
|
+
tool_result = tool_execution_result.func_return
|
249
|
+
success_flag = tool_execution_result.success_flag
|
247
250
|
|
248
251
|
# 3. Provide function_call response back into the conversation
|
252
|
+
# TODO: fix this tool format
|
249
253
|
tool_message = ToolMessage(
|
250
254
|
content=json.dumps({"result": tool_result}),
|
251
255
|
tool_call_id=tool_call_id,
|
@@ -267,6 +271,7 @@ class VoiceAgent(BaseAgent):
|
|
267
271
|
tool_call_id=tool_call_id,
|
268
272
|
function_call_success=success_flag,
|
269
273
|
function_response=tool_result,
|
274
|
+
tool_execution_result=tool_execution_result,
|
270
275
|
actor=self.actor,
|
271
276
|
add_heartbeat_request_system_message=True,
|
272
277
|
)
|
@@ -388,10 +393,14 @@ class VoiceAgent(BaseAgent):
|
|
388
393
|
for t in tools
|
389
394
|
]
|
390
395
|
|
391
|
-
async def _execute_tool(self, user_query: str, tool_name: str, tool_args: dict, agent_state: AgentState) ->
|
396
|
+
async def _execute_tool(self, user_query: str, tool_name: str, tool_args: dict, agent_state: AgentState) -> "ToolExecutionResult":
|
392
397
|
"""
|
393
398
|
Executes a tool and returns (result, success_flag).
|
394
399
|
"""
|
400
|
+
from letta.schemas.tool_execution_result import ToolExecutionResult
|
401
|
+
|
402
|
+
print("EXECUTING TOOL")
|
403
|
+
|
395
404
|
# Special memory case
|
396
405
|
if tool_name == "search_memory":
|
397
406
|
tool_result = await self._search_memory(
|
@@ -401,11 +410,17 @@ class VoiceAgent(BaseAgent):
|
|
401
410
|
end_minutes_ago=tool_args["end_minutes_ago"],
|
402
411
|
agent_state=agent_state,
|
403
412
|
)
|
404
|
-
return
|
413
|
+
return ToolExecutionResult(
|
414
|
+
func_return=tool_result,
|
415
|
+
status="success",
|
416
|
+
)
|
405
417
|
else:
|
406
418
|
target_tool = next((x for x in agent_state.tools if x.name == tool_name), None)
|
407
419
|
if not target_tool:
|
408
|
-
return
|
420
|
+
return ToolExecutionResult(
|
421
|
+
func_return=f"Tool not found: {tool_name}",
|
422
|
+
status="error",
|
423
|
+
)
|
409
424
|
|
410
425
|
try:
|
411
426
|
tool_result, _ = execute_external_tool(
|
@@ -416,9 +431,9 @@ class VoiceAgent(BaseAgent):
|
|
416
431
|
actor=self.actor,
|
417
432
|
allow_agent_state_modifications=False,
|
418
433
|
)
|
419
|
-
return tool_result,
|
434
|
+
return ToolExecutionResult(func_return=tool_result, status="success")
|
420
435
|
except Exception as e:
|
421
|
-
return f"Failed to call tool. Error: {e}",
|
436
|
+
return ToolExecutionResult(func_return=f"Failed to call tool. Error: {e}", status="error")
|
422
437
|
|
423
438
|
async def _search_memory(
|
424
439
|
self,
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import AsyncGenerator, List, Tuple, Union
|
1
|
+
from typing import AsyncGenerator, List, Optional, Tuple, Union
|
2
2
|
|
3
3
|
from letta.agents.helpers import _create_letta_response, serialize_message_history
|
4
4
|
from letta.agents.letta_agent import LettaAgent
|
@@ -89,20 +89,23 @@ class VoiceSleeptimeAgent(LettaAgent):
|
|
89
89
|
)
|
90
90
|
|
91
91
|
@trace_method
|
92
|
-
async def _execute_tool(self, tool_name: str, tool_args: dict, agent_state: AgentState
|
92
|
+
async def _execute_tool(self, tool_name: str, tool_args: dict, agent_state: AgentState, agent_step_span: Optional["Span"] = None):
|
93
93
|
"""
|
94
94
|
Executes a tool and returns (result, success_flag).
|
95
95
|
"""
|
96
|
+
from letta.schemas.tool_execution_result import ToolExecutionResult
|
97
|
+
|
96
98
|
# Special memory case
|
97
99
|
target_tool = next((x for x in agent_state.tools if x.name == tool_name), None)
|
98
100
|
if not target_tool:
|
99
|
-
return f"Tool not found: {tool_name}"
|
101
|
+
return ToolExecutionResult(status="error", func_return=f"Tool not found: {tool_name}")
|
100
102
|
|
101
103
|
try:
|
102
104
|
if target_tool.name == "rethink_user_memory" and target_tool.tool_type == ToolType.LETTA_VOICE_SLEEPTIME_CORE:
|
103
|
-
|
105
|
+
func_return, success_flag = self.rethink_user_memory(agent_state=agent_state, **tool_args)
|
106
|
+
return ToolExecutionResult(func_return=func_return, status="success" if success_flag else "error")
|
104
107
|
elif target_tool.name == "finish_rethinking_memory" and target_tool.tool_type == ToolType.LETTA_VOICE_SLEEPTIME_CORE:
|
105
|
-
return "",
|
108
|
+
return ToolExecutionResult(func_return="", status="success")
|
106
109
|
elif target_tool.name == "store_memories" and target_tool.tool_type == ToolType.LETTA_VOICE_SLEEPTIME_CORE:
|
107
110
|
chunks = tool_args.get("chunks", [])
|
108
111
|
results = [self.store_memory(agent_state=self.convo_agent_state, **chunk_args) for chunk_args in chunks]
|
@@ -110,12 +113,14 @@ class VoiceSleeptimeAgent(LettaAgent):
|
|
110
113
|
aggregated_result = next((res for res, _ in results if res is not None), None)
|
111
114
|
aggregated_success = all(success for _, success in results)
|
112
115
|
|
113
|
-
return
|
116
|
+
return ToolExecutionResult(
|
117
|
+
func_return=aggregated_result, status="success" if aggregated_success else "error"
|
118
|
+
) # Note that here we store to the convo agent's archival memory
|
114
119
|
else:
|
115
120
|
result = f"Voice sleeptime agent tried invoking invalid tool with type {target_tool.tool_type}: {target_tool}"
|
116
|
-
return result,
|
121
|
+
return ToolExecutionResult(func_return=result, status="error")
|
117
122
|
except Exception as e:
|
118
|
-
return f"Failed to call tool. Error: {e}",
|
123
|
+
return ToolExecutionResult(func_return=f"Failed to call tool. Error: {e}", status="error")
|
119
124
|
|
120
125
|
def rethink_user_memory(self, new_memory: str, agent_state: AgentState) -> Tuple[str, bool]:
|
121
126
|
if agent_state.memory.get_block(self.target_block_label) is None:
|
letta/constants.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import os
|
2
|
+
import re
|
2
3
|
from logging import CRITICAL, DEBUG, ERROR, INFO, NOTSET, WARN, WARNING
|
3
4
|
|
4
5
|
LETTA_DIR = os.path.join(os.path.expanduser("~"), ".letta")
|
@@ -36,6 +37,9 @@ TOOL_CALL_ID_MAX_LEN = 29
|
|
36
37
|
# minimum context window size
|
37
38
|
MIN_CONTEXT_WINDOW = 4096
|
38
39
|
|
40
|
+
# number of concurrent embedding requests to sent
|
41
|
+
EMBEDDING_BATCH_SIZE = 200
|
42
|
+
|
39
43
|
# Voice Sleeptime message buffer lengths
|
40
44
|
DEFAULT_MAX_MESSAGE_BUFFER_LENGTH = 30
|
41
45
|
DEFAULT_MIN_MESSAGE_BUFFER_LENGTH = 15
|
@@ -56,12 +60,23 @@ DEFAULT_PERSONA = "sam_pov"
|
|
56
60
|
DEFAULT_HUMAN = "basic"
|
57
61
|
DEFAULT_PRESET = "memgpt_chat"
|
58
62
|
|
63
|
+
DEFAULT_PERSONA_BLOCK_DESCRIPTION = "The persona block: Stores details about your current persona, guiding how you behave and respond. This helps you to maintain consistency and personality in your interactions."
|
64
|
+
DEFAULT_HUMAN_BLOCK_DESCRIPTION = "The human block: Stores key details about the person you are conversing with, allowing for more personalized and friend-like conversation."
|
65
|
+
|
59
66
|
SEND_MESSAGE_TOOL_NAME = "send_message"
|
60
67
|
# Base tools that cannot be edited, as they access agent state directly
|
61
68
|
# Note that we don't include "conversation_search_date" for now
|
62
69
|
BASE_TOOLS = [SEND_MESSAGE_TOOL_NAME, "conversation_search", "archival_memory_insert", "archival_memory_search"]
|
63
70
|
# Base memory tools CAN be edited, and are added by default by the server
|
64
71
|
BASE_MEMORY_TOOLS = ["core_memory_append", "core_memory_replace"]
|
72
|
+
# New v2 collection of the base memory tools (effecitvely same as sleeptime set), to pair with memgpt_v2 prompt
|
73
|
+
BASE_MEMORY_TOOLS_V2 = [
|
74
|
+
"memory_replace",
|
75
|
+
"memory_insert",
|
76
|
+
# NOTE: leaving these ones out to simply the set? Can have these reserved for sleep-time
|
77
|
+
# "memory_rethink",
|
78
|
+
# "memory_finish_edits",
|
79
|
+
]
|
65
80
|
# Base tools if the memgpt agent has enable_sleeptime on
|
66
81
|
BASE_SLEEPTIME_CHAT_TOOLS = [SEND_MESSAGE_TOOL_NAME, "conversation_search", "archival_memory_search"]
|
67
82
|
# Base memory tools for sleeptime agent
|
@@ -85,6 +100,15 @@ BASE_VOICE_SLEEPTIME_TOOLS = [
|
|
85
100
|
# Multi agent tools
|
86
101
|
MULTI_AGENT_TOOLS = ["send_message_to_agent_and_wait_for_reply", "send_message_to_agents_matching_tags", "send_message_to_agent_async"]
|
87
102
|
|
103
|
+
# Used to catch if line numbers are pushed in
|
104
|
+
# MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX = re.compile(r"^Line \d+: ", re.MULTILINE)
|
105
|
+
# More "robust" version that handles different kinds of whitespace
|
106
|
+
# shared constant for both memory_insert and memory_replace
|
107
|
+
MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX = re.compile(
|
108
|
+
r"^[ \t]*Line[ \t]+\d+[ \t]*:", # allow any leading whitespace and flexible spacing
|
109
|
+
re.MULTILINE,
|
110
|
+
)
|
111
|
+
|
88
112
|
# Built in tools
|
89
113
|
BUILTIN_TOOLS = ["run_code", "web_search"]
|
90
114
|
|
@@ -99,6 +123,13 @@ LETTA_TOOL_SET = set(
|
|
99
123
|
+ BUILTIN_TOOLS
|
100
124
|
)
|
101
125
|
|
126
|
+
|
127
|
+
def FUNCTION_RETURN_VALUE_TRUNCATED(return_str, return_char: int, return_char_limit: int):
|
128
|
+
return (
|
129
|
+
f"{return_str}... [NOTE: function output was truncated since it exceeded the character limit: {return_char} > {return_char_limit}]"
|
130
|
+
)
|
131
|
+
|
132
|
+
|
102
133
|
# The name of the tool used to send message to the user
|
103
134
|
# May not be relevant in cases where the agent has multiple ways to message to user (send_imessage, send_discord_mesasge, ...)
|
104
135
|
# or in cases where the agent has no concept of messaging a user (e.g. a workflow agent)
|
@@ -108,6 +139,7 @@ DEFAULT_MESSAGE_TOOL_KWARG = "message"
|
|
108
139
|
PRE_EXECUTION_MESSAGE_ARG = "pre_exec_msg"
|
109
140
|
|
110
141
|
REQUEST_HEARTBEAT_PARAM = "request_heartbeat"
|
142
|
+
REQUEST_HEARTBEAT_DESCRIPTION = "Request an immediate heartbeat after function execution. Set to `True` if you want to send a follow-up message or run a follow-up function."
|
111
143
|
|
112
144
|
|
113
145
|
# Structured output models
|
@@ -258,7 +290,7 @@ MAX_ERROR_MESSAGE_CHAR_LIMIT = 500
|
|
258
290
|
CORE_MEMORY_PERSONA_CHAR_LIMIT: int = 5000
|
259
291
|
CORE_MEMORY_HUMAN_CHAR_LIMIT: int = 5000
|
260
292
|
CORE_MEMORY_BLOCK_CHAR_LIMIT: int = 5000
|
261
|
-
|
293
|
+
CORE_MEMORY_SOURCE_CHAR_LIMIT: int = 5000
|
262
294
|
# Function return limits
|
263
295
|
FUNCTION_RETURN_CHAR_LIMIT = 6000 # ~300 words
|
264
296
|
BASE_FUNCTION_RETURN_CHAR_LIMIT = 1000000 # very high (we rely on implementation)
|
letta/data_sources/connectors.py
CHANGED
@@ -2,6 +2,7 @@ from typing import Dict, Iterator, List, Tuple
|
|
2
2
|
|
3
3
|
import typer
|
4
4
|
|
5
|
+
from letta.constants import EMBEDDING_BATCH_SIZE
|
5
6
|
from letta.data_sources.connectors_helper import assert_all_files_exist_locally, extract_metadata_from_files, get_filenames_in_dir
|
6
7
|
from letta.embeddings import embedding_model
|
7
8
|
from letta.schemas.file import FileMetadata
|
@@ -40,43 +41,37 @@ class DataConnector:
|
|
40
41
|
async def load_data(
|
41
42
|
connector: DataConnector, source: Source, passage_manager: PassageManager, source_manager: SourceManager, actor: "User"
|
42
43
|
):
|
44
|
+
from letta.llm_api.llm_client import LLMClient
|
45
|
+
from letta.schemas.embedding_config import EmbeddingConfig
|
46
|
+
|
43
47
|
"""Load data from a connector (generates file and passages) into a specified source_id, associated with a user_id."""
|
44
48
|
embedding_config = source.embedding_config
|
45
49
|
|
46
|
-
# embedding model
|
47
|
-
embed_model = embedding_model(embedding_config)
|
48
|
-
|
49
50
|
# insert passages/file
|
50
|
-
|
51
|
+
texts = []
|
51
52
|
embedding_to_document_name = {}
|
52
53
|
passage_count = 0
|
53
54
|
file_count = 0
|
54
|
-
for file_metadata in connector.find_files(source):
|
55
|
-
file_count += 1
|
56
|
-
await source_manager.create_file(file_metadata, actor)
|
57
55
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
typer.secho(
|
63
|
-
f"Warning: Llama index parser returned empty string, skipping insert of passage with metadata '{passage_metadata}' into VectorDB. You can usually ignore this warning.",
|
64
|
-
fg=typer.colors.YELLOW,
|
65
|
-
)
|
66
|
-
continue
|
56
|
+
async def generate_embeddings(texts: List[str], embedding_config: EmbeddingConfig) -> List[Passage]:
|
57
|
+
passages = []
|
58
|
+
if embedding_config.embedding_endpoint_type == "openai":
|
59
|
+
texts.append(passage_text)
|
67
60
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
f"Warning: Failed to get embedding for {passage_text} (error: {str(e)}), skipping insert into VectorDB.",
|
74
|
-
fg=typer.colors.YELLOW,
|
75
|
-
)
|
76
|
-
continue
|
61
|
+
client = LLMClient.create(
|
62
|
+
provider_type=embedding_config.embedding_endpoint_type,
|
63
|
+
actor=actor,
|
64
|
+
)
|
65
|
+
embeddings = await client.request_embeddings(texts, embedding_config)
|
77
66
|
|
67
|
+
else:
|
68
|
+
embed_model = embedding_model(embedding_config)
|
69
|
+
embeddings = [embed_model.get_text_embedding(text) for text in texts]
|
70
|
+
|
71
|
+
# collate passage and embedding
|
72
|
+
for text, embedding in zip(texts, embeddings):
|
78
73
|
passage = Passage(
|
79
|
-
text=
|
74
|
+
text=text,
|
80
75
|
file_id=file_metadata.id,
|
81
76
|
source_id=source.id,
|
82
77
|
metadata=passage_metadata,
|
@@ -84,7 +79,6 @@ async def load_data(
|
|
84
79
|
embedding_config=source.embedding_config,
|
85
80
|
embedding=embedding,
|
86
81
|
)
|
87
|
-
|
88
82
|
hashable_embedding = tuple(passage.embedding)
|
89
83
|
file_name = file_metadata.file_name
|
90
84
|
if hashable_embedding in embedding_to_document_name:
|
@@ -96,16 +90,38 @@ async def load_data(
|
|
96
90
|
|
97
91
|
passages.append(passage)
|
98
92
|
embedding_to_document_name[hashable_embedding] = file_name
|
99
|
-
|
100
|
-
|
101
|
-
|
93
|
+
return passages
|
94
|
+
|
95
|
+
for file_metadata in connector.find_files(source):
|
96
|
+
file_count += 1
|
97
|
+
await source_manager.create_file(file_metadata, actor)
|
98
|
+
|
99
|
+
# generate passages
|
100
|
+
for passage_text, passage_metadata in connector.generate_passages(file_metadata, chunk_size=embedding_config.embedding_chunk_size):
|
101
|
+
# for some reason, llama index parsers sometimes return empty strings
|
102
|
+
if len(passage_text) == 0:
|
103
|
+
typer.secho(
|
104
|
+
f"Warning: Llama index parser returned empty string, skipping insert of passage with metadata '{passage_metadata}' into VectorDB. You can usually ignore this warning.",
|
105
|
+
fg=typer.colors.YELLOW,
|
106
|
+
)
|
107
|
+
continue
|
108
|
+
|
109
|
+
# get embedding
|
110
|
+
texts.append(passage_text)
|
111
|
+
if len(texts) >= EMBEDDING_BATCH_SIZE:
|
112
|
+
passages = await generate_embeddings(texts, embedding_config)
|
113
|
+
texts = []
|
114
|
+
else:
|
115
|
+
continue
|
102
116
|
|
103
|
-
|
104
|
-
|
117
|
+
# insert passages into passage store
|
118
|
+
await passage_manager.create_many_passages_async(passages, actor)
|
119
|
+
passage_count += len(passages)
|
105
120
|
|
106
|
-
|
107
|
-
|
108
|
-
|
121
|
+
# final remaining
|
122
|
+
if len(texts) > 0:
|
123
|
+
passages = await generate_embeddings(texts, embedding_config)
|
124
|
+
await passage_manager.create_many_passages_async(passages, actor)
|
109
125
|
passage_count += len(passages)
|
110
126
|
|
111
127
|
return passage_count, file_count
|
@@ -128,7 +144,7 @@ class DirectoryConnector(DataConnector):
|
|
128
144
|
self.recursive = recursive
|
129
145
|
self.extensions = extensions
|
130
146
|
|
131
|
-
if self.recursive
|
147
|
+
if self.recursive:
|
132
148
|
assert self.input_directory is not None, "Must provide input directory if recursive is True."
|
133
149
|
|
134
150
|
def find_files(self, source: Source) -> Iterator[FileMetadata]:
|
letta/errors.py
CHANGED
@@ -88,6 +88,10 @@ class LLMPermissionDeniedError(LLMError):
|
|
88
88
|
"""Error when permission is denied by LLM service"""
|
89
89
|
|
90
90
|
|
91
|
+
class LLMContextWindowExceededError(LLMError):
|
92
|
+
"""Error when the context length is exceeded."""
|
93
|
+
|
94
|
+
|
91
95
|
class LLMNotFoundError(LLMError):
|
92
96
|
"""Error when requested resource is not found"""
|
93
97
|
|