letta-nightly 0.7.7.dev20250430205840__py3-none-any.whl → 0.7.8.dev20250501104226__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +8 -12
  3. letta/agents/exceptions.py +6 -0
  4. letta/agents/letta_agent.py +48 -35
  5. letta/agents/letta_agent_batch.py +6 -2
  6. letta/agents/voice_agent.py +10 -7
  7. letta/constants.py +5 -1
  8. letta/functions/composio_helpers.py +100 -0
  9. letta/functions/functions.py +4 -2
  10. letta/functions/helpers.py +19 -99
  11. letta/groups/helpers.py +1 -0
  12. letta/groups/sleeptime_multi_agent.py +5 -1
  13. letta/helpers/message_helper.py +21 -4
  14. letta/helpers/tool_execution_helper.py +1 -1
  15. letta/interfaces/anthropic_streaming_interface.py +165 -158
  16. letta/interfaces/openai_chat_completions_streaming_interface.py +1 -1
  17. letta/llm_api/anthropic.py +15 -10
  18. letta/llm_api/anthropic_client.py +5 -1
  19. letta/llm_api/google_vertex_client.py +1 -1
  20. letta/llm_api/llm_api_tools.py +7 -0
  21. letta/llm_api/llm_client.py +12 -2
  22. letta/llm_api/llm_client_base.py +4 -0
  23. letta/llm_api/openai.py +9 -3
  24. letta/llm_api/openai_client.py +18 -4
  25. letta/memory.py +3 -1
  26. letta/orm/group.py +2 -0
  27. letta/orm/provider.py +10 -0
  28. letta/schemas/agent.py +0 -1
  29. letta/schemas/enums.py +11 -0
  30. letta/schemas/group.py +24 -0
  31. letta/schemas/llm_config.py +1 -0
  32. letta/schemas/llm_config_overrides.py +2 -2
  33. letta/schemas/providers.py +75 -20
  34. letta/schemas/tool.py +3 -8
  35. letta/server/rest_api/app.py +12 -0
  36. letta/server/rest_api/chat_completions_interface.py +1 -1
  37. letta/server/rest_api/interface.py +8 -10
  38. letta/server/rest_api/{optimistic_json_parser.py → json_parser.py} +62 -26
  39. letta/server/rest_api/routers/v1/agents.py +1 -1
  40. letta/server/rest_api/routers/v1/llms.py +4 -3
  41. letta/server/rest_api/routers/v1/providers.py +4 -1
  42. letta/server/rest_api/routers/v1/voice.py +0 -2
  43. letta/server/rest_api/utils.py +8 -19
  44. letta/server/server.py +25 -11
  45. letta/services/group_manager.py +58 -0
  46. letta/services/provider_manager.py +25 -14
  47. letta/services/summarizer/summarizer.py +15 -7
  48. letta/services/tool_executor/tool_execution_manager.py +1 -1
  49. letta/services/tool_executor/tool_executor.py +3 -3
  50. {letta_nightly-0.7.7.dev20250430205840.dist-info → letta_nightly-0.7.8.dev20250501104226.dist-info}/METADATA +4 -5
  51. {letta_nightly-0.7.7.dev20250430205840.dist-info → letta_nightly-0.7.8.dev20250501104226.dist-info}/RECORD +54 -52
  52. {letta_nightly-0.7.7.dev20250430205840.dist-info → letta_nightly-0.7.8.dev20250501104226.dist-info}/LICENSE +0 -0
  53. {letta_nightly-0.7.7.dev20250430205840.dist-info → letta_nightly-0.7.8.dev20250501104226.dist-info}/WHEEL +0 -0
  54. {letta_nightly-0.7.7.dev20250430205840.dist-info → letta_nightly-0.7.8.dev20250501104226.dist-info}/entry_points.txt +0 -0
@@ -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 COMPOSIO_ENTITY_ENV_VAR_KEY, DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
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
- assert_code_gen_compilable(wrapper_function_str.strip())
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 = generate_import_code(additional_imports_module_attr_map)
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
- assert_all_classes_are_imported(tool, additional_imports_module_attr_map)
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
- assert_code_gen_compilable(wrapper_function_str)
62
+ _assert_code_gen_compilable(wrapper_function_str)
143
63
 
144
64
  return func_name, wrapper_function_str
145
65
 
146
66
 
147
- def assert_code_gen_compilable(code_str):
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 assert_all_classes_are_imported(tool: Union["LangChainBaseTool"], additional_imports_module_attr_map: dict[str, str]) -> None:
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(find_required_class_names_for_import(tool))
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 find_required_class_names_for_import(obj: Union["LangChainBaseTool", BaseModel]) -> list[str]:
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 is_base_model(curr_obj):
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: is_base_model(x), candidates)
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 is_base_model(obj):
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 is_base_model(obj: Any):
192
+ def _is_base_model(obj: Any):
273
193
  return isinstance(obj, BaseModel)
274
194
 
275
195
 
276
- def generate_import_code(module_attr_map: Optional[dict]):
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 parse_letta_response_for_assistant_message(
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 send_message_to_agent_no_stream(
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 async_send_message_with_retries(
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
- send_message_to_agent_no_stream(
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 = parse_letta_response_for_assistant_message(target_agent_id, response)
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
@@ -76,6 +76,7 @@ def load_multi_agent(
76
76
  agent_state=agent_state,
77
77
  interface=interface,
78
78
  user=actor,
79
+ mcp_clients=mcp_clients,
79
80
  group_id=group.id,
80
81
  agent_ids=group.agent_ids,
81
82
  description=group.description,
@@ -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(
@@ -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 prepare_input_message_create(
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 == MessageRole.user and wrap_user_message:
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.helpers import execute_composio_action, generate_composio_action_from_func_name
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