letta-nightly 0.7.6.dev20250430104233__py3-none-any.whl → 0.7.8.dev20250501064110__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 +8 -12
- letta/agents/exceptions.py +6 -0
- letta/agents/helpers.py +1 -1
- letta/agents/letta_agent.py +48 -35
- letta/agents/letta_agent_batch.py +6 -2
- letta/agents/voice_agent.py +41 -59
- letta/agents/{ephemeral_memory_agent.py → voice_sleeptime_agent.py} +106 -129
- letta/client/client.py +3 -3
- letta/constants.py +18 -2
- letta/functions/composio_helpers.py +100 -0
- letta/functions/function_sets/base.py +0 -10
- letta/functions/function_sets/voice.py +92 -0
- letta/functions/functions.py +4 -2
- letta/functions/helpers.py +19 -101
- letta/groups/helpers.py +1 -0
- letta/groups/sleeptime_multi_agent.py +5 -1
- letta/helpers/message_helper.py +21 -4
- letta/helpers/tool_execution_helper.py +1 -1
- letta/interfaces/anthropic_streaming_interface.py +165 -158
- letta/interfaces/openai_chat_completions_streaming_interface.py +1 -1
- letta/llm_api/anthropic.py +15 -10
- letta/llm_api/anthropic_client.py +5 -1
- letta/llm_api/google_vertex_client.py +1 -1
- letta/llm_api/llm_api_tools.py +7 -0
- letta/llm_api/llm_client.py +12 -2
- letta/llm_api/llm_client_base.py +4 -0
- letta/llm_api/openai.py +9 -3
- letta/llm_api/openai_client.py +18 -4
- letta/memory.py +3 -1
- letta/orm/enums.py +1 -0
- letta/orm/group.py +2 -0
- letta/orm/provider.py +10 -0
- letta/personas/examples/voice_memory_persona.txt +5 -0
- letta/prompts/system/voice_chat.txt +29 -0
- letta/prompts/system/voice_sleeptime.txt +74 -0
- letta/schemas/agent.py +14 -2
- letta/schemas/enums.py +11 -0
- letta/schemas/group.py +37 -2
- letta/schemas/llm_config.py +1 -0
- letta/schemas/llm_config_overrides.py +2 -2
- letta/schemas/message.py +4 -3
- letta/schemas/providers.py +75 -213
- letta/schemas/tool.py +8 -12
- letta/server/rest_api/app.py +12 -0
- letta/server/rest_api/chat_completions_interface.py +1 -1
- letta/server/rest_api/interface.py +8 -10
- letta/server/rest_api/{optimistic_json_parser.py → json_parser.py} +62 -26
- letta/server/rest_api/routers/v1/agents.py +1 -1
- letta/server/rest_api/routers/v1/embeddings.py +4 -3
- letta/server/rest_api/routers/v1/llms.py +4 -3
- letta/server/rest_api/routers/v1/providers.py +4 -1
- letta/server/rest_api/routers/v1/voice.py +0 -2
- letta/server/rest_api/utils.py +22 -33
- letta/server/server.py +91 -37
- letta/services/agent_manager.py +14 -7
- letta/services/group_manager.py +61 -0
- letta/services/helpers/agent_manager_helper.py +69 -12
- letta/services/message_manager.py +2 -2
- letta/services/passage_manager.py +13 -4
- letta/services/provider_manager.py +25 -14
- letta/services/summarizer/summarizer.py +20 -15
- letta/services/tool_executor/tool_execution_manager.py +1 -1
- letta/services/tool_executor/tool_executor.py +3 -3
- letta/services/tool_manager.py +32 -7
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/METADATA +4 -5
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/RECORD +70 -64
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/entry_points.txt +0 -0
letta/functions/helpers.py
CHANGED
@@ -6,10 +6,9 @@ from random import uniform
|
|
6
6
|
from typing import Any, Dict, List, Optional, Type, Union
|
7
7
|
|
8
8
|
import humps
|
9
|
-
from composio.constants import DEFAULT_ENTITY_ID
|
10
9
|
from pydantic import BaseModel, Field, create_model
|
11
10
|
|
12
|
-
from letta.constants import
|
11
|
+
from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
|
13
12
|
from letta.functions.interface import MultiAgentMessagingInterface
|
14
13
|
from letta.orm.errors import NoResultFound
|
15
14
|
from letta.schemas.enums import MessageRole
|
@@ -21,34 +20,6 @@ from letta.server.rest_api.utils import get_letta_server
|
|
21
20
|
from letta.settings import settings
|
22
21
|
|
23
22
|
|
24
|
-
# TODO: This is kind of hacky, as this is used to search up the action later on composio's side
|
25
|
-
# TODO: So be very careful changing/removing these pair of functions
|
26
|
-
def generate_func_name_from_composio_action(action_name: str) -> str:
|
27
|
-
"""
|
28
|
-
Generates the composio function name from the composio action.
|
29
|
-
|
30
|
-
Args:
|
31
|
-
action_name: The composio action name
|
32
|
-
|
33
|
-
Returns:
|
34
|
-
function name
|
35
|
-
"""
|
36
|
-
return action_name.lower()
|
37
|
-
|
38
|
-
|
39
|
-
def generate_composio_action_from_func_name(func_name: str) -> str:
|
40
|
-
"""
|
41
|
-
Generates the composio action from the composio function name.
|
42
|
-
|
43
|
-
Args:
|
44
|
-
func_name: The composio function name
|
45
|
-
|
46
|
-
Returns:
|
47
|
-
composio action name
|
48
|
-
"""
|
49
|
-
return func_name.upper()
|
50
|
-
|
51
|
-
|
52
23
|
# TODO needed?
|
53
24
|
def generate_mcp_tool_wrapper(mcp_tool_name: str) -> tuple[str, str]:
|
54
25
|
|
@@ -58,73 +29,20 @@ def {mcp_tool_name}(**kwargs):
|
|
58
29
|
"""
|
59
30
|
|
60
31
|
# Compile safety check
|
61
|
-
|
32
|
+
_assert_code_gen_compilable(wrapper_function_str.strip())
|
62
33
|
|
63
34
|
return mcp_tool_name, wrapper_function_str.strip()
|
64
35
|
|
65
36
|
|
66
|
-
def generate_composio_tool_wrapper(action_name: str) -> tuple[str, str]:
|
67
|
-
# Generate func name
|
68
|
-
func_name = generate_func_name_from_composio_action(action_name)
|
69
|
-
|
70
|
-
wrapper_function_str = f"""\
|
71
|
-
def {func_name}(**kwargs):
|
72
|
-
raise RuntimeError("Something went wrong - we should never be using the persisted source code for Composio. Please reach out to Letta team")
|
73
|
-
"""
|
74
|
-
|
75
|
-
# Compile safety check
|
76
|
-
assert_code_gen_compilable(wrapper_function_str.strip())
|
77
|
-
|
78
|
-
return func_name, wrapper_function_str.strip()
|
79
|
-
|
80
|
-
|
81
|
-
def execute_composio_action(
|
82
|
-
action_name: str, args: dict, api_key: Optional[str] = None, entity_id: Optional[str] = None
|
83
|
-
) -> tuple[str, str]:
|
84
|
-
import os
|
85
|
-
|
86
|
-
from composio.exceptions import (
|
87
|
-
ApiKeyNotProvidedError,
|
88
|
-
ComposioSDKError,
|
89
|
-
ConnectedAccountNotFoundError,
|
90
|
-
EnumMetadataNotFound,
|
91
|
-
EnumStringNotFound,
|
92
|
-
)
|
93
|
-
from composio_langchain import ComposioToolSet
|
94
|
-
|
95
|
-
entity_id = entity_id or os.getenv(COMPOSIO_ENTITY_ENV_VAR_KEY, DEFAULT_ENTITY_ID)
|
96
|
-
try:
|
97
|
-
composio_toolset = ComposioToolSet(api_key=api_key, entity_id=entity_id, lock=False)
|
98
|
-
response = composio_toolset.execute_action(action=action_name, params=args)
|
99
|
-
except ApiKeyNotProvidedError:
|
100
|
-
raise RuntimeError(
|
101
|
-
f"Composio API key is missing for action '{action_name}'. "
|
102
|
-
"Please set the sandbox environment variables either through the ADE or the API."
|
103
|
-
)
|
104
|
-
except ConnectedAccountNotFoundError:
|
105
|
-
raise RuntimeError(f"No connected account was found for action '{action_name}'. " "Please link an account and try again.")
|
106
|
-
except EnumStringNotFound as e:
|
107
|
-
raise RuntimeError(f"Invalid value provided for action '{action_name}': " + str(e) + ". Please check the action parameters.")
|
108
|
-
except EnumMetadataNotFound as e:
|
109
|
-
raise RuntimeError(f"Invalid value provided for action '{action_name}': " + str(e) + ". Please check the action parameters.")
|
110
|
-
except ComposioSDKError as e:
|
111
|
-
raise RuntimeError(f"An unexpected error occurred in Composio SDK while executing action '{action_name}': " + str(e))
|
112
|
-
|
113
|
-
if response["error"]:
|
114
|
-
raise RuntimeError(f"Error while executing action '{action_name}': " + str(response["error"]))
|
115
|
-
|
116
|
-
return response["data"]
|
117
|
-
|
118
|
-
|
119
37
|
def generate_langchain_tool_wrapper(
|
120
38
|
tool: "LangChainBaseTool", additional_imports_module_attr_map: dict[str, str] = None
|
121
39
|
) -> tuple[str, str]:
|
122
40
|
tool_name = tool.__class__.__name__
|
123
41
|
import_statement = f"from langchain_community.tools import {tool_name}"
|
124
|
-
extra_module_imports =
|
42
|
+
extra_module_imports = _generate_import_code(additional_imports_module_attr_map)
|
125
43
|
|
126
44
|
# Safety check that user has passed in all required imports:
|
127
|
-
|
45
|
+
_assert_all_classes_are_imported(tool, additional_imports_module_attr_map)
|
128
46
|
|
129
47
|
tool_instantiation = f"tool = {generate_imported_tool_instantiation_call_str(tool)}"
|
130
48
|
run_call = f"return tool._run(**kwargs)"
|
@@ -141,25 +59,25 @@ def {func_name}(**kwargs):
|
|
141
59
|
"""
|
142
60
|
|
143
61
|
# Compile safety check
|
144
|
-
|
62
|
+
_assert_code_gen_compilable(wrapper_function_str)
|
145
63
|
|
146
64
|
return func_name, wrapper_function_str
|
147
65
|
|
148
66
|
|
149
|
-
def
|
67
|
+
def _assert_code_gen_compilable(code_str):
|
150
68
|
try:
|
151
69
|
compile(code_str, "<string>", "exec")
|
152
70
|
except SyntaxError as e:
|
153
71
|
print(f"Syntax error in code: {e}")
|
154
72
|
|
155
73
|
|
156
|
-
def
|
74
|
+
def _assert_all_classes_are_imported(tool: Union["LangChainBaseTool"], additional_imports_module_attr_map: dict[str, str]) -> None:
|
157
75
|
# Safety check that user has passed in all required imports:
|
158
76
|
tool_name = tool.__class__.__name__
|
159
77
|
current_class_imports = {tool_name}
|
160
78
|
if additional_imports_module_attr_map:
|
161
79
|
current_class_imports.update(set(additional_imports_module_attr_map.values()))
|
162
|
-
required_class_imports = set(
|
80
|
+
required_class_imports = set(_find_required_class_names_for_import(tool))
|
163
81
|
|
164
82
|
if not current_class_imports.issuperset(required_class_imports):
|
165
83
|
err_msg = f"[ERROR] You are missing module_attr pairs in `additional_imports_module_attr_map`. Currently, you have imports for {current_class_imports}, but the required classes for import are {required_class_imports}"
|
@@ -167,7 +85,7 @@ def assert_all_classes_are_imported(tool: Union["LangChainBaseTool"], additional
|
|
167
85
|
raise RuntimeError(err_msg)
|
168
86
|
|
169
87
|
|
170
|
-
def
|
88
|
+
def _find_required_class_names_for_import(obj: Union["LangChainBaseTool", BaseModel]) -> list[str]:
|
171
89
|
"""
|
172
90
|
Finds all the class names for required imports when instantiating the `obj`.
|
173
91
|
NOTE: This does not return the full import path, only the class name.
|
@@ -183,7 +101,7 @@ def find_required_class_names_for_import(obj: Union["LangChainBaseTool", BaseMod
|
|
183
101
|
|
184
102
|
# Collect all possible candidates for BaseModel objects
|
185
103
|
candidates = []
|
186
|
-
if
|
104
|
+
if _is_base_model(curr_obj):
|
187
105
|
# If it is a base model, we get all the values of the object parameters
|
188
106
|
# i.e., if obj('b' = <class A>), we would want to inspect <class A>
|
189
107
|
fields = dict(curr_obj)
|
@@ -200,7 +118,7 @@ def find_required_class_names_for_import(obj: Union["LangChainBaseTool", BaseMod
|
|
200
118
|
|
201
119
|
# Filter out all candidates that are not BaseModels
|
202
120
|
# In the list example above, ['a', 3, None, <class A>], we want to filter out 'a', 3, and None
|
203
|
-
candidates = filter(lambda x:
|
121
|
+
candidates = filter(lambda x: _is_base_model(x), candidates)
|
204
122
|
|
205
123
|
# Classic BFS here
|
206
124
|
for c in candidates:
|
@@ -218,7 +136,7 @@ def generate_imported_tool_instantiation_call_str(obj: Any) -> Optional[str]:
|
|
218
136
|
# If it is a basic Python type, we trivially return the string version of that value
|
219
137
|
# Handle basic types
|
220
138
|
return repr(obj)
|
221
|
-
elif
|
139
|
+
elif _is_base_model(obj):
|
222
140
|
# Otherwise, if it is a BaseModel
|
223
141
|
# We want to pull out all the parameters, and reformat them into strings
|
224
142
|
# e.g. {arg}={value}
|
@@ -271,11 +189,11 @@ def generate_imported_tool_instantiation_call_str(obj: Any) -> Optional[str]:
|
|
271
189
|
return None
|
272
190
|
|
273
191
|
|
274
|
-
def
|
192
|
+
def _is_base_model(obj: Any):
|
275
193
|
return isinstance(obj, BaseModel)
|
276
194
|
|
277
195
|
|
278
|
-
def
|
196
|
+
def _generate_import_code(module_attr_map: Optional[dict]):
|
279
197
|
if not module_attr_map:
|
280
198
|
return ""
|
281
199
|
|
@@ -288,7 +206,7 @@ def generate_import_code(module_attr_map: Optional[dict]):
|
|
288
206
|
return "\n".join(code_lines)
|
289
207
|
|
290
208
|
|
291
|
-
def
|
209
|
+
def _parse_letta_response_for_assistant_message(
|
292
210
|
target_agent_id: str,
|
293
211
|
letta_response: LettaResponse,
|
294
212
|
) -> Optional[str]:
|
@@ -348,7 +266,7 @@ def execute_send_message_to_agent(
|
|
348
266
|
return asyncio.run(async_execute_send_message_to_agent(sender_agent, messages, other_agent_id, log_prefix))
|
349
267
|
|
350
268
|
|
351
|
-
async def
|
269
|
+
async def _send_message_to_agent_no_stream(
|
352
270
|
server: "SyncServer",
|
353
271
|
agent_id: str,
|
354
272
|
actor: User,
|
@@ -377,7 +295,7 @@ async def send_message_to_agent_no_stream(
|
|
377
295
|
return LettaResponse(messages=final_messages, usage=usage_stats)
|
378
296
|
|
379
297
|
|
380
|
-
async def
|
298
|
+
async def _async_send_message_with_retries(
|
381
299
|
server: "SyncServer",
|
382
300
|
sender_agent: "Agent",
|
383
301
|
target_agent_id: str,
|
@@ -391,7 +309,7 @@ async def async_send_message_with_retries(
|
|
391
309
|
for attempt in range(1, max_retries + 1):
|
392
310
|
try:
|
393
311
|
response = await asyncio.wait_for(
|
394
|
-
|
312
|
+
_send_message_to_agent_no_stream(
|
395
313
|
server=server,
|
396
314
|
agent_id=target_agent_id,
|
397
315
|
actor=sender_agent.user,
|
@@ -401,7 +319,7 @@ async def async_send_message_with_retries(
|
|
401
319
|
)
|
402
320
|
|
403
321
|
# Then parse out the assistant message
|
404
|
-
assistant_message =
|
322
|
+
assistant_message = _parse_letta_response_for_assistant_message(target_agent_id, response)
|
405
323
|
if assistant_message:
|
406
324
|
sender_agent.logger.info(f"{logging_prefix} - {assistant_message}")
|
407
325
|
return assistant_message
|
letta/groups/helpers.py
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
import asyncio
|
2
2
|
import threading
|
3
3
|
from datetime import datetime, timezone
|
4
|
-
from typing import List, Optional
|
4
|
+
from typing import Dict, List, Optional
|
5
5
|
|
6
6
|
from letta.agent import Agent, AgentState
|
7
|
+
from letta.functions.mcp_client.base_client import BaseMCPClient
|
7
8
|
from letta.groups.helpers import stringify_message
|
8
9
|
from letta.interface import AgentInterface
|
9
10
|
from letta.orm import User
|
@@ -26,6 +27,7 @@ class SleeptimeMultiAgent(Agent):
|
|
26
27
|
interface: AgentInterface,
|
27
28
|
agent_state: AgentState,
|
28
29
|
user: User,
|
30
|
+
mcp_clients: Optional[Dict[str, BaseMCPClient]] = None,
|
29
31
|
# custom
|
30
32
|
group_id: str = "",
|
31
33
|
agent_ids: List[str] = [],
|
@@ -115,6 +117,7 @@ class SleeptimeMultiAgent(Agent):
|
|
115
117
|
agent_state=participant_agent_state,
|
116
118
|
interface=StreamingServerInterface(),
|
117
119
|
user=self.user,
|
120
|
+
mcp_clients=self.mcp_clients,
|
118
121
|
)
|
119
122
|
|
120
123
|
prior_messages = []
|
@@ -212,6 +215,7 @@ class SleeptimeMultiAgent(Agent):
|
|
212
215
|
agent_state=self.agent_state,
|
213
216
|
interface=self.interface,
|
214
217
|
user=self.user,
|
218
|
+
mcp_clients=self.mcp_clients,
|
215
219
|
)
|
216
220
|
# Perform main agent step
|
217
221
|
usage_stats = main_agent.step(
|
letta/helpers/message_helper.py
CHANGED
@@ -4,7 +4,24 @@ from letta.schemas.letta_message_content import TextContent
|
|
4
4
|
from letta.schemas.message import Message, MessageCreate
|
5
5
|
|
6
6
|
|
7
|
-
def
|
7
|
+
def convert_message_creates_to_messages(
|
8
|
+
messages: list[MessageCreate],
|
9
|
+
agent_id: str,
|
10
|
+
wrap_user_message: bool = True,
|
11
|
+
wrap_system_message: bool = True,
|
12
|
+
) -> list[Message]:
|
13
|
+
return [
|
14
|
+
_convert_message_create_to_message(
|
15
|
+
message=message,
|
16
|
+
agent_id=agent_id,
|
17
|
+
wrap_user_message=wrap_user_message,
|
18
|
+
wrap_system_message=wrap_system_message,
|
19
|
+
)
|
20
|
+
for message in messages
|
21
|
+
]
|
22
|
+
|
23
|
+
|
24
|
+
def _convert_message_create_to_message(
|
8
25
|
message: MessageCreate,
|
9
26
|
agent_id: str,
|
10
27
|
wrap_user_message: bool = True,
|
@@ -23,12 +40,12 @@ def prepare_input_message_create(
|
|
23
40
|
raise ValueError("Message content is empty or invalid")
|
24
41
|
|
25
42
|
# Apply wrapping if needed
|
26
|
-
if message.role
|
43
|
+
if message.role not in {MessageRole.user, MessageRole.system}:
|
44
|
+
raise ValueError(f"Invalid message role: {message.role}")
|
45
|
+
elif message.role == MessageRole.user and wrap_user_message:
|
27
46
|
message_content = system.package_user_message(user_message=message_content)
|
28
47
|
elif message.role == MessageRole.system and wrap_system_message:
|
29
48
|
message_content = system.package_system_message(system_message=message_content)
|
30
|
-
elif message.role not in {MessageRole.user, MessageRole.system}:
|
31
|
-
raise ValueError(f"Invalid message role: {message.role}")
|
32
49
|
|
33
50
|
return Message(
|
34
51
|
agent_id=agent_id,
|
@@ -3,7 +3,7 @@ from typing import Any, Dict, Optional
|
|
3
3
|
|
4
4
|
from letta.constants import COMPOSIO_ENTITY_ENV_VAR_KEY, PRE_EXECUTION_MESSAGE_ARG
|
5
5
|
from letta.functions.ast_parsers import coerce_dict_args_by_annotations, get_function_annotations_from_source
|
6
|
-
from letta.functions.
|
6
|
+
from letta.functions.composio_helpers import execute_composio_action, generate_composio_action_from_func_name
|
7
7
|
from letta.helpers.composio_helpers import get_composio_api_key
|
8
8
|
from letta.orm.enums import ToolType
|
9
9
|
from letta.schemas.agent import AgentState
|