letta-nightly 0.1.7.dev20240930104151__py3-none-any.whl → 0.1.7.dev20241002104051__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.

Potentially problematic release.


This version of letta-nightly might be problematic. Click here for more details.

letta/agent.py CHANGED
@@ -237,10 +237,8 @@ class Agent(BaseAgent):
237
237
  self.agent_state = agent_state
238
238
  assert isinstance(self.agent_state.memory, Memory), f"Memory object is not of type Memory: {type(self.agent_state.memory)}"
239
239
 
240
- try:
241
- self.link_tools(tools)
242
- except Exception as e:
243
- raise ValueError(f"Encountered an error while trying to link agent tools during initialization:\n{str(e)}")
240
+ # link tools
241
+ self.link_tools(tools)
244
242
 
245
243
  # gpt-4, gpt-3.5-turbo, ...
246
244
  self.model = self.agent_state.llm_config.model
@@ -345,13 +343,18 @@ class Agent(BaseAgent):
345
343
  env = {}
346
344
  env.update(globals())
347
345
  for tool in tools:
348
- # WARNING: name may not be consistent?
349
- if tool.module: # execute the whole module
350
- exec(tool.module, env)
351
- else:
352
- exec(tool.source_code, env)
353
- self.functions_python[tool.name] = env[tool.name]
354
- self.functions.append(tool.json_schema)
346
+ try:
347
+ # WARNING: name may not be consistent?
348
+ if tool.module: # execute the whole module
349
+ exec(tool.module, env)
350
+ else:
351
+ exec(tool.source_code, env)
352
+
353
+ self.functions_python[tool.json_schema["name"]] = env[tool.json_schema["name"]]
354
+ self.functions.append(tool.json_schema)
355
+ except Exception as e:
356
+ warnings.warn(f"WARNING: tool {tool.name} failed to link")
357
+ print(e)
355
358
  assert all([callable(f) for k, f in self.functions_python.items()]), self.functions_python
356
359
 
357
360
  def _load_messages_from_recall(self, message_ids: List[str]) -> List[Message]:
@@ -548,18 +551,19 @@ class Agent(BaseAgent):
548
551
  ) # extend conversation with assistant's reply
549
552
  printd(f"Function call message: {messages[-1]}")
550
553
 
551
- # The content if then internal monologue, not chat
552
- self.interface.internal_monologue(response_message.content, msg_obj=messages[-1])
554
+ if response_message.content:
555
+ # The content if then internal monologue, not chat
556
+ self.interface.internal_monologue(response_message.content, msg_obj=messages[-1])
553
557
 
554
558
  # Step 3: call the function
555
559
  # Note: the JSON response may not always be valid; be sure to handle errors
556
-
557
- # Failure case 1: function name is wrong
558
560
  function_call = (
559
561
  response_message.function_call if response_message.function_call is not None else response_message.tool_calls[0].function
560
562
  )
561
563
  function_name = function_call.name
562
564
  printd(f"Request to call function {function_name} with tool_call_id: {tool_call_id}")
565
+
566
+ # Failure case 1: function name is wrong
563
567
  try:
564
568
  function_to_call = self.functions_python[function_name]
565
569
  except KeyError:
@@ -1114,48 +1118,10 @@ class Agent(BaseAgent):
1114
1118
  def add_function(self, function_name: str) -> str:
1115
1119
  # TODO: refactor
1116
1120
  raise NotImplementedError
1117
- # if function_name in self.functions_python.keys():
1118
- # msg = f"Function {function_name} already loaded"
1119
- # printd(msg)
1120
- # return msg
1121
-
1122
- # available_functions = load_all_function_sets()
1123
- # if function_name not in available_functions.keys():
1124
- # raise ValueError(f"Function {function_name} not found in function library")
1125
-
1126
- # self.functions.append(available_functions[function_name]["json_schema"])
1127
- # self.functions_python[function_name] = available_functions[function_name]["python_function"]
1128
-
1129
- # msg = f"Added function {function_name}"
1130
- ## self.save()
1131
- # self.update_state()
1132
- # printd(msg)
1133
- # return msg
1134
1121
 
1135
1122
  def remove_function(self, function_name: str) -> str:
1136
1123
  # TODO: refactor
1137
1124
  raise NotImplementedError
1138
- # if function_name not in self.functions_python.keys():
1139
- # msg = f"Function {function_name} not loaded, ignoring"
1140
- # printd(msg)
1141
- # return msg
1142
-
1143
- ## only allow removal of user defined functions
1144
- # user_func_path = Path(USER_FUNCTIONS_DIR)
1145
- # func_path = Path(inspect.getfile(self.functions_python[function_name]))
1146
- # is_subpath = func_path.resolve().parts[: len(user_func_path.resolve().parts)] == user_func_path.resolve().parts
1147
-
1148
- # if not is_subpath:
1149
- # raise ValueError(f"Function {function_name} is not user defined and cannot be removed")
1150
-
1151
- # self.functions = [f_schema for f_schema in self.functions if f_schema["name"] != function_name]
1152
- # self.functions_python.pop(function_name)
1153
-
1154
- # msg = f"Removed function {function_name}"
1155
- ## self.save()
1156
- # self.update_state()
1157
- # printd(msg)
1158
- # return msg
1159
1125
 
1160
1126
  def update_state(self) -> AgentState:
1161
1127
  message_ids = [msg.id for msg in self._messages]
letta/cli/cli.py CHANGED
@@ -464,7 +464,6 @@ def run(
464
464
  # read user id from config
465
465
  ms = MetadataStore(config)
466
466
  client = create_client()
467
- client.user_id
468
467
 
469
468
  # determine agent to use, if not provided
470
469
  if not yes and not agent:
letta/cli/cli_load.py CHANGED
@@ -18,56 +18,6 @@ from letta.data_sources.connectors import DirectoryConnector
18
18
 
19
19
  app = typer.Typer()
20
20
 
21
- # NOTE: not supported due to llama-index breaking things (please reach out if you still need it)
22
- # @app.command("index")
23
- # def load_index(
24
- # name: Annotated[str, typer.Option(help="Name of dataset to load.")],
25
- # dir: Annotated[Optional[str], typer.Option(help="Path to directory containing index.")] = None,
26
- # user_id: Annotated[Optional[uuid.UUID], typer.Option(help="User ID to associate with dataset.")] = None,
27
- # ):
28
- # """Load a LlamaIndex saved VectorIndex into Letta"""
29
- # if user_id is None:
30
- # config = LettaConfig.load()
31
- # user_id = uuid.UUID(config.anon_clientid)
32
- #
33
- # try:
34
- # # load index data
35
- # storage_context = StorageContext.from_defaults(persist_dir=dir)
36
- # loaded_index = load_index_from_storage(storage_context)
37
- #
38
- # # hacky code to extract out passages/embeddings (thanks a lot, llama index)
39
- # embed_dict = loaded_index._vector_store._data.embedding_dict
40
- # node_dict = loaded_index._docstore.docs
41
- #
42
- # # create storage connector
43
- # config = LettaConfig.load()
44
- # if user_id is None:
45
- # user_id = uuid.UUID(config.anon_clientid)
46
- #
47
- # passages = []
48
- # for node_id, node in node_dict.items():
49
- # vector = embed_dict[node_id]
50
- # node.embedding = vector
51
- # # assume embedding are the same as config
52
- # passages.append(
53
- # Passage(
54
- # text=node.text,
55
- # embedding=np.array(vector),
56
- # embedding_dim=config.default_embedding_config.embedding_dim,
57
- # embedding_model=config.default_embedding_config.embedding_model,
58
- # )
59
- # )
60
- # assert config.default_embedding_config.embedding_dim == len(
61
- # vector
62
- # ), f"Expected embedding dimension {config.default_embedding_config.embedding_dim}, got {len(vector)}"
63
- #
64
- # if len(passages) == 0:
65
- # raise ValueError(f"No passages found in index {dir}")
66
- #
67
- # insert_passages_into_source(passages, name, user_id, config)
68
- # except ValueError as e:
69
- # typer.secho(f"Failed to load index from provided information.\n{e}", fg=typer.colors.RED)
70
-
71
21
 
72
22
  default_extensions = ".txt,.md,.pdf"
73
23
 
letta/client/client.py CHANGED
@@ -1562,7 +1562,7 @@ class LocalClient(AbstractClient):
1562
1562
 
1563
1563
  def get_agent(self, agent_id: str) -> AgentState:
1564
1564
  """
1565
- Get an agent's state by it's ID.
1565
+ Get an agent's state by its ID.
1566
1566
 
1567
1567
  Args:
1568
1568
  agent_id (str): ID of the agent
letta/config.py CHANGED
@@ -2,7 +2,6 @@ import configparser
2
2
  import inspect
3
3
  import json
4
4
  import os
5
- import uuid
6
5
  from dataclasses import dataclass
7
6
  from typing import Optional
8
7
 
@@ -14,6 +13,7 @@ from letta.constants import (
14
13
  DEFAULT_HUMAN,
15
14
  DEFAULT_PERSONA,
16
15
  DEFAULT_PRESET,
16
+ DEFAULT_USER_ID,
17
17
  LETTA_DIR,
18
18
  )
19
19
  from letta.log import get_logger
@@ -45,7 +45,7 @@ def set_field(config, section, field, value):
45
45
  @dataclass
46
46
  class LettaConfig:
47
47
  config_path: str = os.getenv("MEMGPT_CONFIG_PATH") or os.path.join(LETTA_DIR, "config")
48
- anon_clientid: str = str(uuid.UUID(int=0))
48
+ anon_clientid: str = DEFAULT_USER_ID
49
49
 
50
50
  # preset
51
51
  preset: str = DEFAULT_PRESET # TODO: rename to system prompt
@@ -100,10 +100,6 @@ class LettaConfig:
100
100
  # self.context_window = int(self.context_window)
101
101
  pass
102
102
 
103
- @staticmethod
104
- def generate_uuid() -> str:
105
- return uuid.UUID(int=uuid.getnode()).hex
106
-
107
103
  @classmethod
108
104
  def load(cls, llm_config: Optional[LLMConfig] = None, embedding_config: Optional[EmbeddingConfig] = None) -> "LettaConfig":
109
105
  # avoid circular import
@@ -199,8 +195,7 @@ class LettaConfig:
199
195
  # assert llm_config is not None, "LLM config must be provided if config does not exist"
200
196
 
201
197
  # create new config
202
- anon_clientid = LettaConfig.generate_uuid()
203
- config = cls(anon_clientid=anon_clientid, config_path=config_path)
198
+ config = cls(config_path=config_path)
204
199
 
205
200
  config.create_config_dir() # create dirs
206
201
 
@@ -284,8 +279,6 @@ class LettaConfig:
284
279
  set_field(config, "version", "letta_version", letta.__version__)
285
280
 
286
281
  # client
287
- if not self.anon_clientid:
288
- self.anon_clientid = self.generate_uuid()
289
282
  set_field(config, "client", "anon_clientid", self.anon_clientid)
290
283
 
291
284
  # always make sure all directories are present
@@ -1,17 +1,12 @@
1
1
  import importlib
2
2
  import inspect
3
3
  import os
4
- import sys
5
4
  from textwrap import dedent # remove indentation
6
5
  from types import ModuleType
7
6
 
8
- from letta.constants import CLI_WARNING_PREFIX, LETTA_DIR
7
+ from letta.constants import CLI_WARNING_PREFIX
9
8
  from letta.functions.schema_generator import generate_schema
10
9
 
11
- USER_FUNCTIONS_DIR = os.path.join(LETTA_DIR, "functions")
12
-
13
- sys.path.append(USER_FUNCTIONS_DIR)
14
-
15
10
 
16
11
  def parse_source_code(func) -> str:
17
12
  """Parse the source code of a function and remove indendation"""
@@ -68,24 +63,6 @@ def validate_function(module_name, module_full_path):
68
63
  return True, None
69
64
 
70
65
 
71
- def write_function(module_name: str, function_name: str, function_code: str):
72
- """Write a function to a file in the user functions directory"""
73
- # Create the user functions directory if it doesn't exist
74
- if not os.path.exists(USER_FUNCTIONS_DIR):
75
- os.makedirs(USER_FUNCTIONS_DIR)
76
-
77
- # Write the function to a file
78
- file_path = os.path.join(USER_FUNCTIONS_DIR, f"{module_name}.py")
79
- with open(file_path, "w", encoding="utf-8") as f:
80
- f.write(function_code)
81
- succ, error = validate_function(module_name, file_path)
82
-
83
- # raise error if function cannot be loaded
84
- if not succ:
85
- raise ValueError(error)
86
- return file_path
87
-
88
-
89
66
  def load_function_file(filepath: str) -> dict:
90
67
  file = os.path.basename(filepath)
91
68
  module_name = file[:-3] # Remove '.py' from filename
@@ -0,0 +1,191 @@
1
+ from typing import Any, Optional, Union
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ def generate_langchain_tool_wrapper(
7
+ tool: "LangChainBaseTool", additional_imports_module_attr_map: dict[str, str] = None
8
+ ) -> tuple[str, str]:
9
+ tool_name = tool.__class__.__name__
10
+ import_statement = f"from langchain_community.tools import {tool_name}"
11
+ extra_module_imports = generate_import_code(additional_imports_module_attr_map)
12
+
13
+ # Safety check that user has passed in all required imports:
14
+ assert_all_classes_are_imported(tool, additional_imports_module_attr_map)
15
+
16
+ tool_instantiation = f"tool = {generate_imported_tool_instantiation_call_str(tool)}"
17
+ run_call = f"return tool._run(**kwargs)"
18
+ func_name = f"run_{tool_name.lower()}"
19
+
20
+ # Combine all parts into the wrapper function
21
+ wrapper_function_str = f"""
22
+ def {func_name}(**kwargs):
23
+ if 'self' in kwargs:
24
+ del kwargs['self']
25
+ import importlib
26
+ {import_statement}
27
+ {extra_module_imports}
28
+ {tool_instantiation}
29
+ {run_call}
30
+ """
31
+ return func_name, wrapper_function_str
32
+
33
+
34
+ def generate_crewai_tool_wrapper(tool: "CrewAIBaseTool", additional_imports_module_attr_map: dict[str, str] = None) -> tuple[str, str]:
35
+ tool_name = tool.__class__.__name__
36
+ import_statement = f"from crewai_tools import {tool_name}"
37
+ extra_module_imports = generate_import_code(additional_imports_module_attr_map)
38
+
39
+ # Safety check that user has passed in all required imports:
40
+ assert_all_classes_are_imported(tool, additional_imports_module_attr_map)
41
+
42
+ tool_instantiation = f"tool = {generate_imported_tool_instantiation_call_str(tool)}"
43
+ run_call = f"return tool._run(**kwargs)"
44
+ func_name = f"run_{tool_name.lower()}"
45
+
46
+ # Combine all parts into the wrapper function
47
+ wrapper_function_str = f"""
48
+ def {func_name}(**kwargs):
49
+ if 'self' in kwargs:
50
+ del kwargs['self']
51
+ {import_statement}
52
+ {extra_module_imports}
53
+ {tool_instantiation}
54
+ {run_call}
55
+ """
56
+ return func_name, wrapper_function_str
57
+
58
+
59
+ def assert_all_classes_are_imported(
60
+ tool: Union["LangChainBaseTool", "CrewAIBaseTool"], additional_imports_module_attr_map: dict[str, str]
61
+ ) -> None:
62
+ # Safety check that user has passed in all required imports:
63
+ tool_name = tool.__class__.__name__
64
+ current_class_imports = {tool_name}
65
+ if additional_imports_module_attr_map:
66
+ current_class_imports.update(set(additional_imports_module_attr_map.values()))
67
+ required_class_imports = set(find_required_class_names_for_import(tool))
68
+
69
+ if not current_class_imports.issuperset(required_class_imports):
70
+ 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}"
71
+ print(err_msg)
72
+ raise RuntimeError(err_msg)
73
+
74
+
75
+ def find_required_class_names_for_import(obj: Union["LangChainBaseTool", "CrewAIBaseTool", BaseModel]) -> list[str]:
76
+ """
77
+ Finds all the class names for required imports when instantiating the `obj`.
78
+ NOTE: This does not return the full import path, only the class name.
79
+
80
+ We accomplish this by running BFS and deep searching all the BaseModel objects in the obj parameters.
81
+ """
82
+ class_names = {obj.__class__.__name__}
83
+ queue = [obj]
84
+
85
+ while queue:
86
+ # Get the current object we are inspecting
87
+ curr_obj = queue.pop()
88
+
89
+ # Collect all possible candidates for BaseModel objects
90
+ candidates = []
91
+ if is_base_model(curr_obj):
92
+ # If it is a base model, we get all the values of the object parameters
93
+ # i.e., if obj('b' = <class A>), we would want to inspect <class A>
94
+ fields = dict(curr_obj)
95
+ # Generate code for each field, skipping empty or None values
96
+ candidates = list(fields.values())
97
+ elif isinstance(curr_obj, dict):
98
+ # If it is a dictionary, we get all the values
99
+ # i.e., if obj = {'a': 3, 'b': <class A>}, we would want to inspect <class A>
100
+ candidates = list(curr_obj.values())
101
+ elif isinstance(curr_obj, list):
102
+ # If it is a list, we inspect all the items in the list
103
+ # i.e., if obj = ['a', 3, None, <class A>], we would want to inspect <class A>
104
+ candidates = curr_obj
105
+
106
+ # Filter out all candidates that are not BaseModels
107
+ # In the list example above, ['a', 3, None, <class A>], we want to filter out 'a', 3, and None
108
+ candidates = filter(lambda x: is_base_model(x), candidates)
109
+
110
+ # Classic BFS here
111
+ for c in candidates:
112
+ c_name = c.__class__.__name__
113
+ if c_name not in class_names:
114
+ class_names.add(c_name)
115
+ queue.append(c)
116
+
117
+ return list(class_names)
118
+
119
+
120
+ def generate_imported_tool_instantiation_call_str(obj: Any) -> Optional[str]:
121
+ if isinstance(obj, (int, float, str, bool, type(None))):
122
+ # This is the base case
123
+ # If it is a basic Python type, we trivially return the string version of that value
124
+ # Handle basic types
125
+ return repr(obj)
126
+ elif is_base_model(obj):
127
+ # Otherwise, if it is a BaseModel
128
+ # We want to pull out all the parameters, and reformat them into strings
129
+ # e.g. {arg}={value}
130
+ # The reason why this is recursive, is because the value can be another BaseModel that we need to stringify
131
+ model_name = obj.__class__.__name__
132
+ fields = dict(obj)
133
+ # Generate code for each field, skipping empty or None values
134
+ field_assignments = []
135
+ for arg, value in fields.items():
136
+ python_string = generate_imported_tool_instantiation_call_str(value)
137
+ if python_string:
138
+ field_assignments.append(f"{arg}={python_string}")
139
+
140
+ assignments = ", ".join(field_assignments)
141
+ return f"{model_name}({assignments})"
142
+ elif isinstance(obj, dict):
143
+ # Inspect each of the items in the dict and stringify them
144
+ # This is important because the dictionary may contain other BaseModels
145
+ dict_items = []
146
+ for k, v in obj.items():
147
+ python_string = generate_imported_tool_instantiation_call_str(v)
148
+ if python_string:
149
+ dict_items.append(f"{repr(k)}: {python_string}")
150
+
151
+ joined_items = ", ".join(dict_items)
152
+ return f"{{{joined_items}}}"
153
+ elif isinstance(obj, list):
154
+ # Inspect each of the items in the list and stringify them
155
+ # This is important because the list may contain other BaseModels
156
+ list_items = [generate_imported_tool_instantiation_call_str(v) for v in obj]
157
+ filtered_list_items = list(filter(None, list_items))
158
+ list_items = ", ".join(filtered_list_items)
159
+ return f"[{list_items}]"
160
+ else:
161
+ # Otherwise, if it is none of the above, that usually means it is a custom Python class that is NOT a BaseModel
162
+ # Thus, we cannot get enough information about it to stringify it
163
+ # This may cause issues, but we are making the assumption that any of these custom Python types are handled correctly by the parent library, such as LangChain or CrewAI
164
+ # An example would be that WikipediaAPIWrapper has an argument that is a wikipedia (pip install wikipedia) object
165
+ # We cannot stringify this easily, but WikipediaAPIWrapper handles the setting of this parameter internally
166
+ # This assumption seems fair to me, since usually they are external imports, and LangChain and CrewAI should be bundling those as module-level imports within the tool
167
+ # We throw a warning here anyway and provide the class name
168
+ print(
169
+ f"[WARNING] Skipping parsing unknown class {obj.__class__.__name__} (does not inherit from the Pydantic BaseModel and is not a basic Python type)"
170
+ )
171
+ return None
172
+
173
+
174
+ def is_base_model(obj: Any):
175
+ from crewai_tools.tools.base_tool import BaseModel as CrewAiBaseModel
176
+ from langchain_core.pydantic_v1 import BaseModel as LangChainBaseModel
177
+
178
+ return isinstance(obj, BaseModel) or isinstance(obj, LangChainBaseModel) or isinstance(obj, CrewAiBaseModel)
179
+
180
+
181
+ def generate_import_code(module_attr_map: Optional[dict]):
182
+ if not module_attr_map:
183
+ return ""
184
+
185
+ code_lines = []
186
+ for module, attr in module_attr_map.items():
187
+ module_name = module.split(".")[-1]
188
+ code_lines.append(f"# Load the module\n {module_name} = importlib.import_module('{module}')")
189
+ code_lines.append(f" # Access the {attr} from the module")
190
+ code_lines.append(f" {attr} = getattr({module_name}, '{attr}')")
191
+ return "\n".join(code_lines)
@@ -1,6 +1,5 @@
1
1
  import inspect
2
- import typing
3
- from typing import Any, Dict, Optional, Type, get_args, get_origin
2
+ from typing import Any, Dict, Optional, Type, Union, get_args, get_origin
4
3
 
5
4
  from docstring_parser import parse
6
5
  from pydantic import BaseModel
@@ -8,7 +7,7 @@ from pydantic import BaseModel
8
7
 
9
8
  def is_optional(annotation):
10
9
  # Check if the annotation is a Union
11
- if getattr(annotation, "__origin__", None) is typing.Union:
10
+ if getattr(annotation, "__origin__", None) is Union:
12
11
  # Check if None is one of the options in the Union
13
12
  return type(None) in annotation.__args__
14
13
  return False
@@ -164,42 +163,3 @@ def generate_schema_from_args_schema(
164
163
  }
165
164
 
166
165
  return function_call_json
167
-
168
-
169
- def generate_langchain_tool_wrapper(tool_name: str) -> str:
170
- import_statement = f"from langchain_community.tools import {tool_name}"
171
-
172
- # NOTE: this will fail for tools like 'wikipedia = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper())' since it needs to pass an argument to the tool instantiation
173
- # https://python.langchain.com/v0.1/docs/integrations/tools/wikipedia/
174
- tool_instantiation = f"tool = {tool_name}()"
175
- run_call = f"return tool._run(**kwargs)"
176
- func_name = f"run_{tool_name.lower()}"
177
-
178
- # Combine all parts into the wrapper function
179
- wrapper_function_str = f"""
180
- def {func_name}(**kwargs):
181
- if 'self' in kwargs:
182
- del kwargs['self']
183
- {import_statement}
184
- {tool_instantiation}
185
- {run_call}
186
- """
187
- return func_name, wrapper_function_str
188
-
189
-
190
- def generate_crewai_tool_wrapper(tool_name: str) -> str:
191
- import_statement = f"from crewai_tools import {tool_name}"
192
- tool_instantiation = f"tool = {tool_name}()"
193
- run_call = f"return tool._run(**kwargs)"
194
- func_name = f"run_{tool_name.lower()}"
195
-
196
- # Combine all parts into the wrapper function
197
- wrapper_function_str = f"""
198
- def {func_name}(**kwargs):
199
- if 'self' in kwargs:
200
- del kwargs['self']
201
- {import_statement}
202
- {tool_instantiation}
203
- {run_call}
204
- """
205
- return func_name, wrapper_function_str
letta/schemas/tool.py CHANGED
@@ -2,11 +2,11 @@ from typing import Dict, List, Optional
2
2
 
3
3
  from pydantic import Field
4
4
 
5
- from letta.functions.schema_generator import (
5
+ from letta.functions.helpers import (
6
6
  generate_crewai_tool_wrapper,
7
7
  generate_langchain_tool_wrapper,
8
- generate_schema_from_args_schema,
9
8
  )
9
+ from letta.functions.schema_generator import generate_schema_from_args_schema
10
10
  from letta.schemas.letta_base import LettaBase
11
11
  from letta.schemas.openai.chat_completions import ToolCall
12
12
 
@@ -58,12 +58,13 @@ class Tool(BaseTool):
58
58
  )
59
59
 
60
60
  @classmethod
61
- def from_langchain(cls, langchain_tool) -> "Tool":
61
+ def from_langchain(cls, langchain_tool: "LangChainBaseTool", additional_imports_module_attr_map: dict[str, str] = None) -> "Tool":
62
62
  """
63
63
  Class method to create an instance of Tool from a Langchain tool (must be from langchain_community.tools).
64
64
 
65
65
  Args:
66
- langchain_tool (LangchainTool): An instance of a crewAI BaseTool (BaseTool from crewai)
66
+ langchain_tool (LangChainBaseTool): An instance of a crewAI BaseTool (BaseTool from crewai)
67
+ additional_imports_module_attr_map (dict[str, str]): A mapping of module names to attribute name. This is used internally to import all the required classes for the langchain tool. For example, you would pass in `{"langchain_community.utilities": "WikipediaAPIWrapper"}` for `from langchain_community.tools import WikipediaQueryRun`. NOTE: You do NOT need to specify the tool import here, that is done automatically for you.
67
68
 
68
69
  Returns:
69
70
  Tool: A Letta Tool initialized with attributes derived from the provided crewAI BaseTool object.
@@ -72,7 +73,7 @@ class Tool(BaseTool):
72
73
  source_type = "python"
73
74
  tags = ["langchain"]
74
75
  # NOTE: langchain tools may come from different packages
75
- wrapper_func_name, wrapper_function_str = generate_langchain_tool_wrapper(langchain_tool.__class__.__name__)
76
+ wrapper_func_name, wrapper_function_str = generate_langchain_tool_wrapper(langchain_tool, additional_imports_module_attr_map)
76
77
  json_schema = generate_schema_from_args_schema(langchain_tool.args_schema, name=wrapper_func_name, description=description)
77
78
 
78
79
  # append heartbeat (necessary for triggering another reasoning step after this tool call)
@@ -92,7 +93,7 @@ class Tool(BaseTool):
92
93
  )
93
94
 
94
95
  @classmethod
95
- def from_crewai(cls, crewai_tool) -> "Tool":
96
+ def from_crewai(cls, crewai_tool: "CrewAIBaseTool", additional_imports_module_attr_map: dict[str, str] = None) -> "Tool":
96
97
  """
97
98
  Class method to create an instance of Tool from a crewAI BaseTool object.
98
99
 
@@ -102,11 +103,10 @@ class Tool(BaseTool):
102
103
  Returns:
103
104
  Tool: A Letta Tool initialized with attributes derived from the provided crewAI BaseTool object.
104
105
  """
105
- crewai_tool.name
106
106
  description = crewai_tool.description
107
107
  source_type = "python"
108
108
  tags = ["crew-ai"]
109
- wrapper_func_name, wrapper_function_str = generate_crewai_tool_wrapper(crewai_tool.__class__.__name__)
109
+ wrapper_func_name, wrapper_function_str = generate_crewai_tool_wrapper(crewai_tool, additional_imports_module_attr_map)
110
110
  json_schema = generate_schema_from_args_schema(crewai_tool.args_schema, name=wrapper_func_name, description=description)
111
111
 
112
112
  # append heartbeat (necessary for triggering another reasoning step after this tool call)
@@ -128,7 +128,7 @@ class Tool(BaseTool):
128
128
 
129
129
  class ToolCreate(BaseTool):
130
130
  name: Optional[str] = Field(None, description="The name of the function (auto-generated from source_code if not provided).")
131
- tags: List[str] = Field(..., description="Metadata tags.")
131
+ tags: List[str] = Field([], description="Metadata tags.")
132
132
  source_code: str = Field(..., description="The source code of the function.")
133
133
  json_schema: Optional[Dict] = Field(
134
134
  None, description="The JSON schema of the function (auto-generated from source_code if not provided)"
letta/server/server.py CHANGED
@@ -1441,10 +1441,6 @@ class SyncServer(Server):
1441
1441
  logger.exception(f"Failed to delete agent {agent_id} via ID with:\n{str(e)}")
1442
1442
  raise ValueError(f"Failed to delete agent {agent_id} in database")
1443
1443
 
1444
- def authenticate_user(self) -> str:
1445
- # TODO: Implement actual authentication to enable multi user setup
1446
- return str(LettaConfig.load().anon_clientid)
1447
-
1448
1444
  def api_key_to_user(self, api_key: str) -> str:
1449
1445
  """Decode an API key to a user"""
1450
1446
  user = self.ms.get_user_from_api_key(api_key=api_key)
@@ -1752,7 +1748,7 @@ class SyncServer(Server):
1752
1748
 
1753
1749
  # TODO: not sure if this always works
1754
1750
  func = env[functions[-1]]
1755
- json_schema = generate_schema(func, request.name)
1751
+ json_schema = generate_schema(func)
1756
1752
  else:
1757
1753
  # provided by client
1758
1754
  json_schema = request.json_schema
@@ -1973,19 +1969,6 @@ class SyncServer(Server):
1973
1969
  return current_user
1974
1970
 
1975
1971
  return self.get_default_user()
1976
- ## NOTE: same code as local client to get the default user
1977
- # config = LettaConfig.load()
1978
- # user_id = config.anon_clientid
1979
- # user = self.get_user(user_id)
1980
-
1981
- # if not user:
1982
- # user = self.create_user(UserCreate())
1983
-
1984
- # # # update config
1985
- # config.anon_clientid = str(user.id)
1986
- # config.save()
1987
-
1988
- # return user
1989
1972
 
1990
1973
  def list_models(self) -> List[LLMConfig]:
1991
1974
  """List available models"""