letta-nightly 0.7.7.dev20250430205840__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/letta_agent.py +48 -35
- letta/agents/letta_agent_batch.py +6 -2
- letta/agents/voice_agent.py +10 -7
- letta/constants.py +5 -1
- letta/functions/composio_helpers.py +100 -0
- letta/functions/functions.py +4 -2
- letta/functions/helpers.py +19 -99
- 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/group.py +2 -0
- letta/orm/provider.py +10 -0
- letta/schemas/agent.py +0 -1
- letta/schemas/enums.py +11 -0
- letta/schemas/group.py +24 -0
- letta/schemas/llm_config.py +1 -0
- letta/schemas/llm_config_overrides.py +2 -2
- letta/schemas/providers.py +75 -20
- letta/schemas/tool.py +3 -8
- 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/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 +8 -19
- letta/server/server.py +25 -11
- letta/services/group_manager.py +58 -0
- letta/services/provider_manager.py +25 -14
- letta/services/summarizer/summarizer.py +15 -7
- letta/services/tool_executor/tool_execution_manager.py +1 -1
- letta/services/tool_executor/tool_executor.py +3 -3
- {letta_nightly-0.7.7.dev20250430205840.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/METADATA +4 -5
- {letta_nightly-0.7.7.dev20250430205840.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/RECORD +54 -52
- {letta_nightly-0.7.7.dev20250430205840.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.7.dev20250430205840.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.7.dev20250430205840.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,71 +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(action_name: str, args: dict, api_key: Optional[str] = None, entity_id: Optional[str] = None) -> Any:
|
82
|
-
import os
|
83
|
-
|
84
|
-
from composio.exceptions import (
|
85
|
-
ApiKeyNotProvidedError,
|
86
|
-
ComposioSDKError,
|
87
|
-
ConnectedAccountNotFoundError,
|
88
|
-
EnumMetadataNotFound,
|
89
|
-
EnumStringNotFound,
|
90
|
-
)
|
91
|
-
from composio_langchain import ComposioToolSet
|
92
|
-
|
93
|
-
entity_id = entity_id or os.getenv(COMPOSIO_ENTITY_ENV_VAR_KEY, DEFAULT_ENTITY_ID)
|
94
|
-
try:
|
95
|
-
composio_toolset = ComposioToolSet(api_key=api_key, entity_id=entity_id, lock=False)
|
96
|
-
response = composio_toolset.execute_action(action=action_name, params=args)
|
97
|
-
except ApiKeyNotProvidedError:
|
98
|
-
raise RuntimeError(
|
99
|
-
f"Composio API key is missing for action '{action_name}'. "
|
100
|
-
"Please set the sandbox environment variables either through the ADE or the API."
|
101
|
-
)
|
102
|
-
except ConnectedAccountNotFoundError:
|
103
|
-
raise RuntimeError(f"No connected account was found for action '{action_name}'. " "Please link an account and try again.")
|
104
|
-
except EnumStringNotFound as e:
|
105
|
-
raise RuntimeError(f"Invalid value provided for action '{action_name}': " + str(e) + ". Please check the action parameters.")
|
106
|
-
except EnumMetadataNotFound as e:
|
107
|
-
raise RuntimeError(f"Invalid value provided for action '{action_name}': " + str(e) + ". Please check the action parameters.")
|
108
|
-
except ComposioSDKError as e:
|
109
|
-
raise RuntimeError(f"An unexpected error occurred in Composio SDK while executing action '{action_name}': " + str(e))
|
110
|
-
|
111
|
-
if "error" in response:
|
112
|
-
raise RuntimeError(f"Error while executing action '{action_name}': " + str(response["error"]))
|
113
|
-
|
114
|
-
return response.get("data")
|
115
|
-
|
116
|
-
|
117
37
|
def generate_langchain_tool_wrapper(
|
118
38
|
tool: "LangChainBaseTool", additional_imports_module_attr_map: dict[str, str] = None
|
119
39
|
) -> tuple[str, str]:
|
120
40
|
tool_name = tool.__class__.__name__
|
121
41
|
import_statement = f"from langchain_community.tools import {tool_name}"
|
122
|
-
extra_module_imports =
|
42
|
+
extra_module_imports = _generate_import_code(additional_imports_module_attr_map)
|
123
43
|
|
124
44
|
# Safety check that user has passed in all required imports:
|
125
|
-
|
45
|
+
_assert_all_classes_are_imported(tool, additional_imports_module_attr_map)
|
126
46
|
|
127
47
|
tool_instantiation = f"tool = {generate_imported_tool_instantiation_call_str(tool)}"
|
128
48
|
run_call = f"return tool._run(**kwargs)"
|
@@ -139,25 +59,25 @@ def {func_name}(**kwargs):
|
|
139
59
|
"""
|
140
60
|
|
141
61
|
# Compile safety check
|
142
|
-
|
62
|
+
_assert_code_gen_compilable(wrapper_function_str)
|
143
63
|
|
144
64
|
return func_name, wrapper_function_str
|
145
65
|
|
146
66
|
|
147
|
-
def
|
67
|
+
def _assert_code_gen_compilable(code_str):
|
148
68
|
try:
|
149
69
|
compile(code_str, "<string>", "exec")
|
150
70
|
except SyntaxError as e:
|
151
71
|
print(f"Syntax error in code: {e}")
|
152
72
|
|
153
73
|
|
154
|
-
def
|
74
|
+
def _assert_all_classes_are_imported(tool: Union["LangChainBaseTool"], additional_imports_module_attr_map: dict[str, str]) -> None:
|
155
75
|
# Safety check that user has passed in all required imports:
|
156
76
|
tool_name = tool.__class__.__name__
|
157
77
|
current_class_imports = {tool_name}
|
158
78
|
if additional_imports_module_attr_map:
|
159
79
|
current_class_imports.update(set(additional_imports_module_attr_map.values()))
|
160
|
-
required_class_imports = set(
|
80
|
+
required_class_imports = set(_find_required_class_names_for_import(tool))
|
161
81
|
|
162
82
|
if not current_class_imports.issuperset(required_class_imports):
|
163
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}"
|
@@ -165,7 +85,7 @@ def assert_all_classes_are_imported(tool: Union["LangChainBaseTool"], additional
|
|
165
85
|
raise RuntimeError(err_msg)
|
166
86
|
|
167
87
|
|
168
|
-
def
|
88
|
+
def _find_required_class_names_for_import(obj: Union["LangChainBaseTool", BaseModel]) -> list[str]:
|
169
89
|
"""
|
170
90
|
Finds all the class names for required imports when instantiating the `obj`.
|
171
91
|
NOTE: This does not return the full import path, only the class name.
|
@@ -181,7 +101,7 @@ def find_required_class_names_for_import(obj: Union["LangChainBaseTool", BaseMod
|
|
181
101
|
|
182
102
|
# Collect all possible candidates for BaseModel objects
|
183
103
|
candidates = []
|
184
|
-
if
|
104
|
+
if _is_base_model(curr_obj):
|
185
105
|
# If it is a base model, we get all the values of the object parameters
|
186
106
|
# i.e., if obj('b' = <class A>), we would want to inspect <class A>
|
187
107
|
fields = dict(curr_obj)
|
@@ -198,7 +118,7 @@ def find_required_class_names_for_import(obj: Union["LangChainBaseTool", BaseMod
|
|
198
118
|
|
199
119
|
# Filter out all candidates that are not BaseModels
|
200
120
|
# In the list example above, ['a', 3, None, <class A>], we want to filter out 'a', 3, and None
|
201
|
-
candidates = filter(lambda x:
|
121
|
+
candidates = filter(lambda x: _is_base_model(x), candidates)
|
202
122
|
|
203
123
|
# Classic BFS here
|
204
124
|
for c in candidates:
|
@@ -216,7 +136,7 @@ def generate_imported_tool_instantiation_call_str(obj: Any) -> Optional[str]:
|
|
216
136
|
# If it is a basic Python type, we trivially return the string version of that value
|
217
137
|
# Handle basic types
|
218
138
|
return repr(obj)
|
219
|
-
elif
|
139
|
+
elif _is_base_model(obj):
|
220
140
|
# Otherwise, if it is a BaseModel
|
221
141
|
# We want to pull out all the parameters, and reformat them into strings
|
222
142
|
# e.g. {arg}={value}
|
@@ -269,11 +189,11 @@ def generate_imported_tool_instantiation_call_str(obj: Any) -> Optional[str]:
|
|
269
189
|
return None
|
270
190
|
|
271
191
|
|
272
|
-
def
|
192
|
+
def _is_base_model(obj: Any):
|
273
193
|
return isinstance(obj, BaseModel)
|
274
194
|
|
275
195
|
|
276
|
-
def
|
196
|
+
def _generate_import_code(module_attr_map: Optional[dict]):
|
277
197
|
if not module_attr_map:
|
278
198
|
return ""
|
279
199
|
|
@@ -286,7 +206,7 @@ def generate_import_code(module_attr_map: Optional[dict]):
|
|
286
206
|
return "\n".join(code_lines)
|
287
207
|
|
288
208
|
|
289
|
-
def
|
209
|
+
def _parse_letta_response_for_assistant_message(
|
290
210
|
target_agent_id: str,
|
291
211
|
letta_response: LettaResponse,
|
292
212
|
) -> Optional[str]:
|
@@ -346,7 +266,7 @@ def execute_send_message_to_agent(
|
|
346
266
|
return asyncio.run(async_execute_send_message_to_agent(sender_agent, messages, other_agent_id, log_prefix))
|
347
267
|
|
348
268
|
|
349
|
-
async def
|
269
|
+
async def _send_message_to_agent_no_stream(
|
350
270
|
server: "SyncServer",
|
351
271
|
agent_id: str,
|
352
272
|
actor: User,
|
@@ -375,7 +295,7 @@ async def send_message_to_agent_no_stream(
|
|
375
295
|
return LettaResponse(messages=final_messages, usage=usage_stats)
|
376
296
|
|
377
297
|
|
378
|
-
async def
|
298
|
+
async def _async_send_message_with_retries(
|
379
299
|
server: "SyncServer",
|
380
300
|
sender_agent: "Agent",
|
381
301
|
target_agent_id: str,
|
@@ -389,7 +309,7 @@ async def async_send_message_with_retries(
|
|
389
309
|
for attempt in range(1, max_retries + 1):
|
390
310
|
try:
|
391
311
|
response = await asyncio.wait_for(
|
392
|
-
|
312
|
+
_send_message_to_agent_no_stream(
|
393
313
|
server=server,
|
394
314
|
agent_id=target_agent_id,
|
395
315
|
actor=sender_agent.user,
|
@@ -399,7 +319,7 @@ async def async_send_message_with_retries(
|
|
399
319
|
)
|
400
320
|
|
401
321
|
# Then parse out the assistant message
|
402
|
-
assistant_message =
|
322
|
+
assistant_message = _parse_letta_response_for_assistant_message(target_agent_id, response)
|
403
323
|
if assistant_message:
|
404
324
|
sender_agent.logger.info(f"{logging_prefix} - {assistant_message}")
|
405
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
|