letta-nightly 0.5.1.dev20241104104148__py3-none-any.whl → 0.5.1.dev20241106104104__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 +37 -5
- letta/agent_store/db.py +19 -19
- letta/cli/cli.py +3 -3
- letta/cli/cli_config.py +2 -2
- letta/client/client.py +36 -19
- letta/functions/schema_generator.py +48 -7
- letta/metadata.py +10 -10
- letta/orm/base.py +5 -2
- letta/orm/mixins.py +2 -53
- letta/orm/organization.py +3 -1
- letta/orm/sqlalchemy_base.py +6 -45
- letta/orm/tool.py +3 -2
- letta/orm/user.py +3 -1
- letta/schemas/agent.py +6 -2
- letta/schemas/block.py +1 -1
- letta/schemas/letta_base.py +2 -0
- letta/schemas/memory.py +4 -0
- letta/schemas/organization.py +4 -4
- letta/schemas/tool.py +14 -11
- letta/schemas/user.py +1 -1
- letta/server/rest_api/routers/v1/agents.py +6 -1
- letta/server/rest_api/routers/v1/organizations.py +2 -1
- letta/server/rest_api/routers/v1/tools.py +2 -1
- letta/server/rest_api/routers/v1/users.py +2 -2
- letta/server/server.py +20 -11
- letta/services/organization_manager.py +4 -4
- letta/services/tool_manager.py +17 -16
- letta/services/user_manager.py +3 -3
- {letta_nightly-0.5.1.dev20241104104148.dist-info → letta_nightly-0.5.1.dev20241106104104.dist-info}/METADATA +3 -3
- {letta_nightly-0.5.1.dev20241104104148.dist-info → letta_nightly-0.5.1.dev20241106104104.dist-info}/RECORD +33 -33
- {letta_nightly-0.5.1.dev20241104104148.dist-info → letta_nightly-0.5.1.dev20241106104104.dist-info}/LICENSE +0 -0
- {letta_nightly-0.5.1.dev20241104104148.dist-info → letta_nightly-0.5.1.dev20241106104104.dist-info}/WHEEL +0 -0
- {letta_nightly-0.5.1.dev20241104104148.dist-info → letta_nightly-0.5.1.dev20241106104104.dist-info}/entry_points.txt +0 -0
letta/orm/sqlalchemy_base.py
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING, List, Literal, Optional, Type
|
|
2
|
-
from uuid import uuid4
|
|
3
2
|
|
|
4
|
-
from
|
|
5
|
-
from sqlalchemy import Boolean, String, select
|
|
3
|
+
from sqlalchemy import String, select
|
|
6
4
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
5
|
|
|
8
6
|
from letta.log import get_logger
|
|
9
7
|
from letta.orm.base import Base, CommonSqlalchemyMetaMixins
|
|
10
8
|
from letta.orm.errors import NoResultFound
|
|
11
|
-
from letta.orm.mixins import is_valid_uuid4
|
|
12
9
|
|
|
13
10
|
if TYPE_CHECKING:
|
|
14
11
|
from pydantic import BaseModel
|
|
@@ -24,27 +21,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
|
24
21
|
|
|
25
22
|
__order_by_default__ = "created_at"
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
deleted: Mapped[bool] = mapped_column(Boolean, default=False, doc="Is this record deleted? Used for universal soft deletes.")
|
|
30
|
-
|
|
31
|
-
@classmethod
|
|
32
|
-
def __prefix__(cls) -> str:
|
|
33
|
-
return depascalize(cls.__name__)
|
|
34
|
-
|
|
35
|
-
@property
|
|
36
|
-
def id(self) -> Optional[str]:
|
|
37
|
-
if self._id:
|
|
38
|
-
return f"{self.__prefix__()}-{self._id}"
|
|
39
|
-
|
|
40
|
-
@id.setter
|
|
41
|
-
def id(self, value: str) -> None:
|
|
42
|
-
if not value:
|
|
43
|
-
return
|
|
44
|
-
prefix, id_ = value.split("-", 1)
|
|
45
|
-
assert prefix == self.__prefix__(), f"{prefix} is not a valid id prefix for {self.__class__.__name__}"
|
|
46
|
-
assert is_valid_uuid4(id_), f"{id_} is not a valid uuid4"
|
|
47
|
-
self._id = id_
|
|
24
|
+
id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
48
25
|
|
|
49
26
|
@classmethod
|
|
50
27
|
def list(
|
|
@@ -57,11 +34,10 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
|
57
34
|
|
|
58
35
|
# Add a cursor condition if provided
|
|
59
36
|
if cursor:
|
|
60
|
-
|
|
61
|
-
query = query.where(cls._id > cursor_uuid)
|
|
37
|
+
query = query.where(cls.id > cursor)
|
|
62
38
|
|
|
63
39
|
# Add a limit to the query if provided
|
|
64
|
-
query = query.order_by(cls.
|
|
40
|
+
query = query.order_by(cls.id).limit(limit)
|
|
65
41
|
|
|
66
42
|
# Handle soft deletes if the class has the 'is_deleted' attribute
|
|
67
43
|
if hasattr(cls, "is_deleted"):
|
|
@@ -70,20 +46,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
|
70
46
|
# Execute the query and return the results as a list of model instances
|
|
71
47
|
return list(session.execute(query).scalars())
|
|
72
48
|
|
|
73
|
-
@classmethod
|
|
74
|
-
def get_uid_from_identifier(cls, identifier: str, indifferent: Optional[bool] = False) -> str:
|
|
75
|
-
"""converts the id into a uuid object
|
|
76
|
-
Args:
|
|
77
|
-
identifier: the string identifier, such as `organization-xxxx-xx...`
|
|
78
|
-
indifferent: if True, will not enforce the prefix check
|
|
79
|
-
"""
|
|
80
|
-
try:
|
|
81
|
-
uuid_string = identifier.split("-", 1)[1] if indifferent else identifier.replace(f"{cls.__prefix__()}-", "")
|
|
82
|
-
assert is_valid_uuid4(uuid_string)
|
|
83
|
-
return uuid_string
|
|
84
|
-
except ValueError as e:
|
|
85
|
-
raise ValueError(f"{identifier} is not a valid identifier for class {cls.__name__}") from e
|
|
86
|
-
|
|
87
49
|
@classmethod
|
|
88
50
|
def read(
|
|
89
51
|
cls,
|
|
@@ -112,8 +74,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
|
112
74
|
|
|
113
75
|
# If an identifier is provided, add it to the query conditions
|
|
114
76
|
if identifier is not None:
|
|
115
|
-
|
|
116
|
-
query = query.where(cls._id == identifier)
|
|
77
|
+
query = query.where(cls.id == identifier)
|
|
117
78
|
query_conditions.append(f"id='{identifier}'")
|
|
118
79
|
|
|
119
80
|
if kwargs:
|
|
@@ -183,7 +144,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
|
183
144
|
org_id = getattr(actor, "organization_id", None)
|
|
184
145
|
if not org_id:
|
|
185
146
|
raise ValueError(f"object {actor} has no organization accessor")
|
|
186
|
-
return query.where(cls.
|
|
147
|
+
return query.where(cls.organization_id == org_id, cls.is_deleted == False)
|
|
187
148
|
|
|
188
149
|
@property
|
|
189
150
|
def __pydantic_model__(self) -> Type["BaseModel"]:
|
letta/orm/tool.py
CHANGED
|
@@ -21,13 +21,14 @@ class Tool(SqlalchemyBase, OrganizationMixin):
|
|
|
21
21
|
more granular permissions.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
__tablename__ = "
|
|
24
|
+
__tablename__ = "tools"
|
|
25
25
|
__pydantic_model__ = PydanticTool
|
|
26
26
|
|
|
27
27
|
# Add unique constraint on (name, _organization_id)
|
|
28
28
|
# An organization should not have multiple tools with the same name
|
|
29
|
-
__table_args__ = (UniqueConstraint("name", "
|
|
29
|
+
__table_args__ = (UniqueConstraint("name", "organization_id", name="uix_name_organization"),)
|
|
30
30
|
|
|
31
|
+
id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
31
32
|
name: Mapped[str] = mapped_column(doc="The display name of the tool.")
|
|
32
33
|
description: Mapped[Optional[str]] = mapped_column(nullable=True, doc="The description of the tool.")
|
|
33
34
|
tags: Mapped[List] = mapped_column(JSON, doc="Metadata tags used to filter tools.")
|
letta/orm/user.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING
|
|
2
2
|
|
|
3
|
+
from sqlalchemy import String
|
|
3
4
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
4
5
|
|
|
5
6
|
from letta.orm.mixins import OrganizationMixin
|
|
@@ -13,9 +14,10 @@ if TYPE_CHECKING:
|
|
|
13
14
|
class User(SqlalchemyBase, OrganizationMixin):
|
|
14
15
|
"""User ORM class"""
|
|
15
16
|
|
|
16
|
-
__tablename__ = "
|
|
17
|
+
__tablename__ = "users"
|
|
17
18
|
__pydantic_model__ = PydanticUser
|
|
18
19
|
|
|
20
|
+
id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
19
21
|
name: Mapped[str] = mapped_column(nullable=False, doc="The display name of the user.")
|
|
20
22
|
|
|
21
23
|
# relationships
|
letta/schemas/agent.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import uuid
|
|
2
1
|
from datetime import datetime
|
|
3
2
|
from enum import Enum
|
|
4
3
|
from typing import Dict, List, Optional
|
|
@@ -105,7 +104,7 @@ class AgentState(BaseAgent, validate_assignment=True):
|
|
|
105
104
|
class CreateAgent(BaseAgent):
|
|
106
105
|
# all optional as server can generate defaults
|
|
107
106
|
name: Optional[str] = Field(None, description="The name of the agent.")
|
|
108
|
-
message_ids: Optional[List[
|
|
107
|
+
message_ids: Optional[List[str]] = Field(None, description="The ids of the messages in the agent's in-context memory.")
|
|
109
108
|
memory: Optional[Memory] = Field(None, description="The in-context memory of the agent.")
|
|
110
109
|
tools: Optional[List[str]] = Field(None, description="The tools used by the agent.")
|
|
111
110
|
tool_rules: Optional[List[BaseToolRule]] = Field(None, description="The tool rules governing the agent.")
|
|
@@ -113,6 +112,11 @@ class CreateAgent(BaseAgent):
|
|
|
113
112
|
agent_type: Optional[AgentType] = Field(None, description="The type of agent.")
|
|
114
113
|
llm_config: Optional[LLMConfig] = Field(None, description="The LLM configuration used by the agent.")
|
|
115
114
|
embedding_config: Optional[EmbeddingConfig] = Field(None, description="The embedding configuration used by the agent.")
|
|
115
|
+
# Note: if this is None, then we'll populate with the standard "more human than human" initial message sequence
|
|
116
|
+
# If the client wants to make this empty, then the client can set the arg to an empty list
|
|
117
|
+
initial_message_sequence: Optional[List[Message]] = Field(
|
|
118
|
+
None, description="The initial set of messages to put in the agent's in-context memory."
|
|
119
|
+
)
|
|
116
120
|
|
|
117
121
|
@field_validator("name")
|
|
118
122
|
@classmethod
|
letta/schemas/block.py
CHANGED
|
@@ -18,7 +18,7 @@ class BaseBlock(LettaBase, validate_assignment=True):
|
|
|
18
18
|
limit: int = Field(2000, description="Character limit of the block.")
|
|
19
19
|
|
|
20
20
|
# template data (optional)
|
|
21
|
-
|
|
21
|
+
template_name: Optional[str] = Field(None, description="Name of the block if it is a template.")
|
|
22
22
|
template: bool = Field(False, description="Whether the block is a template (e.g. saved human/persona options).")
|
|
23
23
|
|
|
24
24
|
# context window label
|
letta/schemas/letta_base.py
CHANGED
|
@@ -21,6 +21,8 @@ class LettaBase(BaseModel):
|
|
|
21
21
|
from_attributes=True,
|
|
22
22
|
# throw errors if attributes are given that don't belong
|
|
23
23
|
extra="forbid",
|
|
24
|
+
# handle datetime serialization consistently across all models
|
|
25
|
+
# json_encoders={datetime: lambda dt: (dt.replace(tzinfo=timezone.utc) if dt.tzinfo is None else dt).isoformat()},
|
|
24
26
|
)
|
|
25
27
|
|
|
26
28
|
# def __id_prefix__(self):
|
letta/schemas/memory.py
CHANGED
|
@@ -106,6 +106,10 @@ class Memory(BaseModel, validate_assignment=True):
|
|
|
106
106
|
# New format
|
|
107
107
|
obj.prompt_template = state["prompt_template"]
|
|
108
108
|
for key, value in state["memory"].items():
|
|
109
|
+
# TODO: This is migration code, please take a look at a later time to get rid of this
|
|
110
|
+
if "name" in value:
|
|
111
|
+
value["template_name"] = value["name"]
|
|
112
|
+
value.pop("name")
|
|
109
113
|
obj.memory[key] = Block(**value)
|
|
110
114
|
else:
|
|
111
115
|
# Old format (pre-template)
|
letta/schemas/organization.py
CHANGED
|
@@ -4,16 +4,16 @@ from typing import Optional
|
|
|
4
4
|
from pydantic import Field
|
|
5
5
|
|
|
6
6
|
from letta.schemas.letta_base import LettaBase
|
|
7
|
-
from letta.utils import get_utc_time
|
|
7
|
+
from letta.utils import create_random_username, get_utc_time
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class OrganizationBase(LettaBase):
|
|
11
|
-
__id_prefix__ = "
|
|
11
|
+
__id_prefix__ = "org"
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Organization(OrganizationBase):
|
|
15
|
-
id: str =
|
|
16
|
-
name: str = Field(
|
|
15
|
+
id: str = OrganizationBase.generate_id_field()
|
|
16
|
+
name: str = Field(create_random_username(), description="The name of the organization.")
|
|
17
17
|
created_at: Optional[datetime] = Field(default_factory=get_utc_time, description="The creation date of the organization.")
|
|
18
18
|
|
|
19
19
|
|
letta/schemas/tool.py
CHANGED
|
@@ -8,7 +8,10 @@ from letta.functions.helpers import (
|
|
|
8
8
|
generate_crewai_tool_wrapper,
|
|
9
9
|
generate_langchain_tool_wrapper,
|
|
10
10
|
)
|
|
11
|
-
from letta.functions.schema_generator import
|
|
11
|
+
from letta.functions.schema_generator import (
|
|
12
|
+
generate_schema_from_args_schema_v1,
|
|
13
|
+
generate_schema_from_args_schema_v2,
|
|
14
|
+
)
|
|
12
15
|
from letta.schemas.letta_base import LettaBase
|
|
13
16
|
from letta.schemas.openai.chat_completions import ToolCall
|
|
14
17
|
|
|
@@ -30,21 +33,21 @@ class Tool(BaseTool):
|
|
|
30
33
|
|
|
31
34
|
"""
|
|
32
35
|
|
|
33
|
-
id: str =
|
|
36
|
+
id: str = BaseTool.generate_id_field()
|
|
34
37
|
description: Optional[str] = Field(None, description="The description of the tool.")
|
|
35
38
|
source_type: Optional[str] = Field(None, description="The type of the source code.")
|
|
36
39
|
module: Optional[str] = Field(None, description="The module of the function.")
|
|
37
|
-
organization_id: str = Field(
|
|
38
|
-
name: str = Field(
|
|
39
|
-
tags: List[str] = Field(
|
|
40
|
+
organization_id: Optional[str] = Field(None, description="The unique identifier of the organization associated with the tool.")
|
|
41
|
+
name: Optional[str] = Field(None, description="The name of the function.")
|
|
42
|
+
tags: List[str] = Field([], description="Metadata tags.")
|
|
40
43
|
|
|
41
44
|
# code
|
|
42
45
|
source_code: str = Field(..., description="The source code of the function.")
|
|
43
|
-
json_schema: Dict = Field(
|
|
46
|
+
json_schema: Optional[Dict] = Field(None, description="The JSON schema of the function.")
|
|
44
47
|
|
|
45
48
|
# metadata fields
|
|
46
|
-
created_by_id: str = Field(
|
|
47
|
-
last_updated_by_id: str = Field(
|
|
49
|
+
created_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
|
|
50
|
+
last_updated_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
|
|
48
51
|
|
|
49
52
|
def to_dict(self):
|
|
50
53
|
"""
|
|
@@ -97,7 +100,7 @@ class ToolCreate(LettaBase):
|
|
|
97
100
|
source_type = "python"
|
|
98
101
|
tags = ["composio"]
|
|
99
102
|
wrapper_func_name, wrapper_function_str = generate_composio_tool_wrapper(action)
|
|
100
|
-
json_schema =
|
|
103
|
+
json_schema = generate_schema_from_args_schema_v2(composio_tool.args_schema, name=wrapper_func_name, description=description)
|
|
101
104
|
|
|
102
105
|
return cls(
|
|
103
106
|
name=wrapper_func_name,
|
|
@@ -129,7 +132,7 @@ class ToolCreate(LettaBase):
|
|
|
129
132
|
tags = ["langchain"]
|
|
130
133
|
# NOTE: langchain tools may come from different packages
|
|
131
134
|
wrapper_func_name, wrapper_function_str = generate_langchain_tool_wrapper(langchain_tool, additional_imports_module_attr_map)
|
|
132
|
-
json_schema =
|
|
135
|
+
json_schema = generate_schema_from_args_schema_v1(langchain_tool.args_schema, name=wrapper_func_name, description=description)
|
|
133
136
|
|
|
134
137
|
return cls(
|
|
135
138
|
name=wrapper_func_name,
|
|
@@ -159,7 +162,7 @@ class ToolCreate(LettaBase):
|
|
|
159
162
|
source_type = "python"
|
|
160
163
|
tags = ["crew-ai"]
|
|
161
164
|
wrapper_func_name, wrapper_function_str = generate_crewai_tool_wrapper(crewai_tool, additional_imports_module_attr_map)
|
|
162
|
-
json_schema =
|
|
165
|
+
json_schema = generate_schema_from_args_schema_v1(crewai_tool.args_schema, name=wrapper_func_name, description=description)
|
|
163
166
|
|
|
164
167
|
return cls(
|
|
165
168
|
name=wrapper_func_name,
|
letta/schemas/user.py
CHANGED
|
@@ -21,7 +21,7 @@ class User(UserBase):
|
|
|
21
21
|
created_at (datetime): The creation date of the user.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
id: str =
|
|
24
|
+
id: str = UserBase.generate_id_field()
|
|
25
25
|
organization_id: Optional[str] = Field(OrganizationManager.DEFAULT_ORG_ID, description="The organization id of the user")
|
|
26
26
|
name: str = Field(..., description="The name of the user.")
|
|
27
27
|
created_at: Optional[datetime] = Field(default_factory=datetime.utcnow, description="The creation date of the user.")
|
|
@@ -40,6 +40,7 @@ router = APIRouter(prefix="/agents", tags=["agents"])
|
|
|
40
40
|
|
|
41
41
|
@router.get("/", response_model=List[AgentState], operation_id="list_agents")
|
|
42
42
|
def list_agents(
|
|
43
|
+
name: Optional[str] = Query(None, description="Name of the agent"),
|
|
43
44
|
server: "SyncServer" = Depends(get_letta_server),
|
|
44
45
|
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
|
45
46
|
):
|
|
@@ -49,7 +50,11 @@ def list_agents(
|
|
|
49
50
|
"""
|
|
50
51
|
actor = server.get_user_or_default(user_id=user_id)
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
agents = server.list_agents(user_id=actor.id)
|
|
54
|
+
# TODO: move this logic to the ORM
|
|
55
|
+
if name:
|
|
56
|
+
agents = [a for a in agents if a.name == name]
|
|
57
|
+
return agents
|
|
53
58
|
|
|
54
59
|
|
|
55
60
|
@router.get("/{agent_id}/context", response_model=ContextWindowOverview, operation_id="get_agent_context_window")
|
|
@@ -38,7 +38,8 @@ def create_org(
|
|
|
38
38
|
"""
|
|
39
39
|
Create a new org in the database
|
|
40
40
|
"""
|
|
41
|
-
org =
|
|
41
|
+
org = Organization(**request.model_dump())
|
|
42
|
+
org = server.organization_manager.create_organization(pydantic_org=org)
|
|
42
43
|
return org
|
|
43
44
|
|
|
44
45
|
|
|
@@ -89,7 +89,8 @@ def create_tool(
|
|
|
89
89
|
actor = server.get_user_or_default(user_id=user_id)
|
|
90
90
|
|
|
91
91
|
# Send request to create the tool
|
|
92
|
-
|
|
92
|
+
tool = Tool(**request.model_dump())
|
|
93
|
+
return server.tool_manager.create_or_update_tool(pydantic_tool=tool, actor=actor)
|
|
93
94
|
|
|
94
95
|
|
|
95
96
|
@router.patch("/{tool_id}", response_model=Tool, operation_id="update_tool")
|
letta/server/server.py
CHANGED
|
@@ -824,7 +824,7 @@ class SyncServer(Server):
|
|
|
824
824
|
source_type = "python"
|
|
825
825
|
tags = ["memory", "memgpt-base"]
|
|
826
826
|
tool = self.tool_manager.create_or_update_tool(
|
|
827
|
-
|
|
827
|
+
Tool(
|
|
828
828
|
source_code=source_code,
|
|
829
829
|
source_type=source_type,
|
|
830
830
|
tags=tags,
|
|
@@ -857,7 +857,10 @@ class SyncServer(Server):
|
|
|
857
857
|
agent_state=agent_state,
|
|
858
858
|
tools=tool_objs,
|
|
859
859
|
# gpt-3.5-turbo tends to omit inner monologue, relax this requirement for now
|
|
860
|
-
first_message_verify_mono=
|
|
860
|
+
first_message_verify_mono=(
|
|
861
|
+
True if (llm_config and llm_config.model is not None and "gpt-4" in llm_config.model) else False
|
|
862
|
+
),
|
|
863
|
+
initial_message_sequence=request.initial_message_sequence,
|
|
861
864
|
)
|
|
862
865
|
elif request.agent_type == AgentType.o1_agent:
|
|
863
866
|
agent = O1Agent(
|
|
@@ -865,7 +868,9 @@ class SyncServer(Server):
|
|
|
865
868
|
agent_state=agent_state,
|
|
866
869
|
tools=tool_objs,
|
|
867
870
|
# gpt-3.5-turbo tends to omit inner monologue, relax this requirement for now
|
|
868
|
-
first_message_verify_mono=
|
|
871
|
+
first_message_verify_mono=(
|
|
872
|
+
True if (llm_config and llm_config.model is not None and "gpt-4" in llm_config.model) else False
|
|
873
|
+
),
|
|
869
874
|
)
|
|
870
875
|
# rebuilding agent memory on agent create in case shared memory blocks
|
|
871
876
|
# were specified in the new agent's memory config. we're doing this for two reasons:
|
|
@@ -1084,7 +1089,7 @@ class SyncServer(Server):
|
|
|
1084
1089
|
id: Optional[str] = None,
|
|
1085
1090
|
) -> Optional[List[Block]]:
|
|
1086
1091
|
|
|
1087
|
-
return self.ms.get_blocks(user_id=user_id, label=label, template=template,
|
|
1092
|
+
return self.ms.get_blocks(user_id=user_id, label=label, template=template, template_name=name, id=id)
|
|
1088
1093
|
|
|
1089
1094
|
def get_block(self, block_id: str):
|
|
1090
1095
|
|
|
@@ -1096,14 +1101,18 @@ class SyncServer(Server):
|
|
|
1096
1101
|
return blocks[0]
|
|
1097
1102
|
|
|
1098
1103
|
def create_block(self, request: CreateBlock, user_id: str, update: bool = False) -> Block:
|
|
1099
|
-
existing_blocks = self.ms.get_blocks(
|
|
1100
|
-
|
|
1104
|
+
existing_blocks = self.ms.get_blocks(
|
|
1105
|
+
template_name=request.template_name, user_id=user_id, template=request.template, label=request.label
|
|
1106
|
+
)
|
|
1107
|
+
|
|
1108
|
+
# for templates, update existing block template if exists
|
|
1109
|
+
if existing_blocks is not None and request.template:
|
|
1101
1110
|
existing_block = existing_blocks[0]
|
|
1102
1111
|
assert len(existing_blocks) == 1
|
|
1103
1112
|
if update:
|
|
1104
1113
|
return self.update_block(UpdateBlock(id=existing_block.id, **vars(request)))
|
|
1105
1114
|
else:
|
|
1106
|
-
raise ValueError(f"Block with name {request.
|
|
1115
|
+
raise ValueError(f"Block with name {request.template_name} already exists")
|
|
1107
1116
|
block = Block(**vars(request))
|
|
1108
1117
|
self.ms.create_block(block)
|
|
1109
1118
|
return block
|
|
@@ -1112,7 +1121,7 @@ class SyncServer(Server):
|
|
|
1112
1121
|
block = self.get_block(request.id)
|
|
1113
1122
|
block.limit = request.limit if request.limit is not None else block.limit
|
|
1114
1123
|
block.value = request.value if request.value is not None else block.value
|
|
1115
|
-
block.
|
|
1124
|
+
block.template_name = request.template_name if request.template_name is not None else block.template_name
|
|
1116
1125
|
self.ms.update_block(block=block)
|
|
1117
1126
|
return self.ms.get_block(block_id=request.id)
|
|
1118
1127
|
|
|
@@ -1757,7 +1766,7 @@ class SyncServer(Server):
|
|
|
1757
1766
|
tool_creates += ToolCreate.load_default_composio_tools()
|
|
1758
1767
|
for tool_create in tool_creates:
|
|
1759
1768
|
try:
|
|
1760
|
-
self.tool_manager.create_or_update_tool(tool_create, actor=actor)
|
|
1769
|
+
self.tool_manager.create_or_update_tool(Tool(**tool_create.model_dump()), actor=actor)
|
|
1761
1770
|
except Exception as e:
|
|
1762
1771
|
warnings.warn(f"An error occurred while creating tool {tool_create}: {e}")
|
|
1763
1772
|
warnings.warn(traceback.format_exc())
|
|
@@ -1773,12 +1782,12 @@ class SyncServer(Server):
|
|
|
1773
1782
|
for persona_file in list_persona_files():
|
|
1774
1783
|
text = open(persona_file, "r", encoding="utf-8").read()
|
|
1775
1784
|
name = os.path.basename(persona_file).replace(".txt", "")
|
|
1776
|
-
self.create_block(CreatePersona(user_id=user_id,
|
|
1785
|
+
self.create_block(CreatePersona(user_id=user_id, template_name=name, value=text, template=True), user_id=user_id, update=True)
|
|
1777
1786
|
|
|
1778
1787
|
for human_file in list_human_files():
|
|
1779
1788
|
text = open(human_file, "r", encoding="utf-8").read()
|
|
1780
1789
|
name = os.path.basename(human_file).replace(".txt", "")
|
|
1781
|
-
self.create_block(CreateHuman(user_id=user_id,
|
|
1790
|
+
self.create_block(CreateHuman(user_id=user_id, template_name=name, value=text, template=True), user_id=user_id, update=True)
|
|
1782
1791
|
|
|
1783
1792
|
def get_agent_message(self, agent_id: str, message_id: str) -> Optional[Message]:
|
|
1784
1793
|
"""Get a single message from the agent's memory"""
|
|
@@ -3,13 +3,13 @@ from typing import List, Optional
|
|
|
3
3
|
from letta.orm.errors import NoResultFound
|
|
4
4
|
from letta.orm.organization import Organization as OrganizationModel
|
|
5
5
|
from letta.schemas.organization import Organization as PydanticOrganization
|
|
6
|
-
from letta.utils import
|
|
6
|
+
from letta.utils import enforce_types
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class OrganizationManager:
|
|
10
10
|
"""Manager class to handle business logic related to Organizations."""
|
|
11
11
|
|
|
12
|
-
DEFAULT_ORG_ID = "
|
|
12
|
+
DEFAULT_ORG_ID = "org-00000000-0000-4000-8000-000000000000"
|
|
13
13
|
DEFAULT_ORG_NAME = "default_org"
|
|
14
14
|
|
|
15
15
|
def __init__(self):
|
|
@@ -37,10 +37,10 @@ class OrganizationManager:
|
|
|
37
37
|
raise ValueError(f"Organization with id {org_id} not found.")
|
|
38
38
|
|
|
39
39
|
@enforce_types
|
|
40
|
-
def create_organization(self,
|
|
40
|
+
def create_organization(self, pydantic_org: PydanticOrganization) -> PydanticOrganization:
|
|
41
41
|
"""Create a new organization. If a name is provided, it is used, otherwise, a random one is generated."""
|
|
42
42
|
with self.session_maker() as session:
|
|
43
|
-
org = OrganizationModel(
|
|
43
|
+
org = OrganizationModel(**pydantic_org.model_dump())
|
|
44
44
|
org.create(session)
|
|
45
45
|
return org.to_pydantic()
|
|
46
46
|
|
letta/services/tool_manager.py
CHANGED
|
@@ -7,10 +7,9 @@ from letta.functions.functions import derive_openai_json_schema, load_function_s
|
|
|
7
7
|
|
|
8
8
|
# TODO: Remove this once we translate all of these to the ORM
|
|
9
9
|
from letta.orm.errors import NoResultFound
|
|
10
|
-
from letta.orm.organization import Organization as OrganizationModel
|
|
11
10
|
from letta.orm.tool import Tool as ToolModel
|
|
12
11
|
from letta.schemas.tool import Tool as PydanticTool
|
|
13
|
-
from letta.schemas.tool import
|
|
12
|
+
from letta.schemas.tool import ToolUpdate
|
|
14
13
|
from letta.schemas.user import User as PydanticUser
|
|
15
14
|
from letta.utils import enforce_types, printd
|
|
16
15
|
|
|
@@ -33,20 +32,20 @@ class ToolManager:
|
|
|
33
32
|
self.session_maker = db_context
|
|
34
33
|
|
|
35
34
|
@enforce_types
|
|
36
|
-
def create_or_update_tool(self,
|
|
35
|
+
def create_or_update_tool(self, pydantic_tool: PydanticTool, actor: PydanticUser) -> PydanticTool:
|
|
37
36
|
"""Create a new tool based on the ToolCreate schema."""
|
|
38
37
|
# Derive json_schema
|
|
39
|
-
derived_json_schema =
|
|
40
|
-
source_code=
|
|
38
|
+
derived_json_schema = pydantic_tool.json_schema or derive_openai_json_schema(
|
|
39
|
+
source_code=pydantic_tool.source_code, name=pydantic_tool.name
|
|
41
40
|
)
|
|
42
|
-
derived_name =
|
|
41
|
+
derived_name = pydantic_tool.name or derived_json_schema["name"]
|
|
43
42
|
|
|
44
43
|
try:
|
|
45
44
|
# NOTE: We use the organization id here
|
|
46
45
|
# This is important, because even if it's a different user, adding the same tool to the org should not happen
|
|
47
46
|
tool = self.get_tool_by_name(tool_name=derived_name, actor=actor)
|
|
48
47
|
# Put to dict and remove fields that should not be reset
|
|
49
|
-
update_data =
|
|
48
|
+
update_data = pydantic_tool.model_dump(exclude={"module"}, exclude_unset=True, exclude_none=True)
|
|
50
49
|
# Remove redundant update fields
|
|
51
50
|
update_data = {key: value for key, value in update_data.items() if getattr(tool, key) != value}
|
|
52
51
|
|
|
@@ -55,22 +54,24 @@ class ToolManager:
|
|
|
55
54
|
self.update_tool_by_id(tool.id, ToolUpdate(**update_data), actor)
|
|
56
55
|
else:
|
|
57
56
|
printd(
|
|
58
|
-
f"`create_or_update_tool` was called with user_id={actor.id}, organization_id={actor.organization_id}, name={
|
|
57
|
+
f"`create_or_update_tool` was called with user_id={actor.id}, organization_id={actor.organization_id}, name={pydantic_tool.name}, but found existing tool with nothing to update."
|
|
59
58
|
)
|
|
60
59
|
except NoResultFound:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
tool = self.create_tool(
|
|
60
|
+
pydantic_tool.json_schema = derived_json_schema
|
|
61
|
+
pydantic_tool.name = derived_name
|
|
62
|
+
tool = self.create_tool(pydantic_tool, actor=actor)
|
|
64
63
|
|
|
65
64
|
return tool
|
|
66
65
|
|
|
67
66
|
@enforce_types
|
|
68
|
-
def create_tool(self,
|
|
67
|
+
def create_tool(self, pydantic_tool: PydanticTool, actor: PydanticUser) -> PydanticTool:
|
|
69
68
|
"""Create a new tool based on the ToolCreate schema."""
|
|
70
69
|
# Create the tool
|
|
71
70
|
with self.session_maker() as session:
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
# Set the organization id at the ORM layer
|
|
72
|
+
pydantic_tool.organization_id = actor.organization_id
|
|
73
|
+
tool_data = pydantic_tool.model_dump()
|
|
74
|
+
tool = ToolModel(**tool_data)
|
|
74
75
|
tool.create(session, actor=actor)
|
|
75
76
|
|
|
76
77
|
return tool.to_pydantic()
|
|
@@ -99,7 +100,7 @@ class ToolManager:
|
|
|
99
100
|
db_session=session,
|
|
100
101
|
cursor=cursor,
|
|
101
102
|
limit=limit,
|
|
102
|
-
|
|
103
|
+
organization_id=actor.organization_id,
|
|
103
104
|
)
|
|
104
105
|
return [tool.to_pydantic() for tool in tools]
|
|
105
106
|
|
|
@@ -176,7 +177,7 @@ class ToolManager:
|
|
|
176
177
|
# create to tool
|
|
177
178
|
tools.append(
|
|
178
179
|
self.create_or_update_tool(
|
|
179
|
-
|
|
180
|
+
PydanticTool(
|
|
180
181
|
name=name,
|
|
181
182
|
tags=tags,
|
|
182
183
|
source_type="python",
|
letta/services/user_manager.py
CHANGED
|
@@ -4,7 +4,7 @@ from letta.orm.errors import NoResultFound
|
|
|
4
4
|
from letta.orm.organization import Organization as OrganizationModel
|
|
5
5
|
from letta.orm.user import User as UserModel
|
|
6
6
|
from letta.schemas.user import User as PydanticUser
|
|
7
|
-
from letta.schemas.user import
|
|
7
|
+
from letta.schemas.user import UserUpdate
|
|
8
8
|
from letta.services.organization_manager import OrganizationManager
|
|
9
9
|
from letta.utils import enforce_types
|
|
10
10
|
|
|
@@ -42,10 +42,10 @@ class UserManager:
|
|
|
42
42
|
return user.to_pydantic()
|
|
43
43
|
|
|
44
44
|
@enforce_types
|
|
45
|
-
def create_user(self,
|
|
45
|
+
def create_user(self, pydantic_user: PydanticUser) -> PydanticUser:
|
|
46
46
|
"""Create a new user if it doesn't already exist."""
|
|
47
47
|
with self.session_maker() as session:
|
|
48
|
-
new_user = UserModel(**
|
|
48
|
+
new_user = UserModel(**pydantic_user.model_dump())
|
|
49
49
|
new_user.create(session)
|
|
50
50
|
return new_user.to_pydantic()
|
|
51
51
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: letta-nightly
|
|
3
|
-
Version: 0.5.1.
|
|
3
|
+
Version: 0.5.1.dev20241106104104
|
|
4
4
|
Summary: Create LLM agents with long-term memory and custom tools
|
|
5
5
|
License: Apache License
|
|
6
6
|
Author: Letta Team
|
|
@@ -52,7 +52,7 @@ Requires-Dist: pg8000 (>=1.30.3,<2.0.0) ; extra == "postgres"
|
|
|
52
52
|
Requires-Dist: pgvector (>=0.2.3,<0.3.0) ; extra == "postgres"
|
|
53
53
|
Requires-Dist: pre-commit (>=3.5.0,<4.0.0) ; extra == "dev"
|
|
54
54
|
Requires-Dist: prettytable (>=3.9.0,<4.0.0)
|
|
55
|
-
Requires-Dist: psycopg2 (>=2.9.10,<3.0.0)
|
|
55
|
+
Requires-Dist: psycopg2 (>=2.9.10,<3.0.0) ; extra == "postgres"
|
|
56
56
|
Requires-Dist: psycopg2-binary (>=2.9.10,<3.0.0) ; extra == "postgres"
|
|
57
57
|
Requires-Dist: pyautogen (==0.2.22) ; extra == "autogen"
|
|
58
58
|
Requires-Dist: pydantic (>=2.7.4,<3.0.0)
|
|
@@ -99,7 +99,7 @@ Description-Content-Type: text/markdown
|
|
|
99
99
|
</h3>
|
|
100
100
|
|
|
101
101
|
**👾 Letta** is an open source framework for building stateful LLM applications. You can use Letta to build **stateful agents** with advanced reasoning capabilities and transparent long-term memory. The Letta framework is white box and model-agnostic.
|
|
102
|
-
|
|
102
|
+
|
|
103
103
|
[](https://discord.gg/letta)
|
|
104
104
|
[](https://twitter.com/Letta_AI)
|
|
105
105
|
[](https://arxiv.org/abs/2310.08560)
|