letta-nightly 0.5.4.dev20241126104249__py3-none-any.whl → 0.5.4.dev20241128000451__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/__init__.py +1 -1
- letta/agent.py +102 -140
- letta/agent_store/chroma.py +2 -0
- letta/cli/cli.py +3 -5
- letta/client/client.py +360 -117
- letta/config.py +2 -2
- letta/constants.py +5 -0
- letta/errors.py +12 -0
- letta/functions/function_sets/base.py +38 -1
- letta/functions/functions.py +4 -6
- letta/functions/schema_generator.py +6 -5
- letta/helpers/tool_rule_solver.py +6 -5
- letta/main.py +1 -1
- letta/metadata.py +45 -42
- letta/o1_agent.py +1 -4
- letta/orm/block.py +2 -1
- letta/orm/blocks_agents.py +4 -1
- letta/orm/sqlalchemy_base.py +13 -0
- letta/persistence_manager.py +1 -0
- letta/schemas/agent.py +57 -52
- letta/schemas/block.py +70 -26
- letta/schemas/enums.py +14 -0
- letta/schemas/letta_base.py +1 -1
- letta/schemas/letta_request.py +11 -23
- letta/schemas/letta_response.py +1 -2
- letta/schemas/memory.py +31 -100
- letta/schemas/message.py +3 -3
- letta/schemas/tool_rule.py +13 -5
- letta/server/rest_api/interface.py +12 -19
- letta/server/rest_api/routers/openai/assistants/threads.py +2 -3
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +0 -2
- letta/server/rest_api/routers/v1/agents.py +100 -94
- letta/server/rest_api/routers/v1/blocks.py +50 -5
- letta/server/rest_api/routers/v1/tools.py +14 -3
- letta/server/server.py +246 -460
- letta/server/static_files/assets/index-9fa459a2.js +1 -1
- letta/services/block_manager.py +23 -4
- letta/services/blocks_agents_manager.py +23 -1
- letta/services/per_agent_lock_manager.py +18 -0
- letta/services/tool_execution_sandbox.py +1 -1
- letta/services/tool_manager.py +2 -1
- {letta_nightly-0.5.4.dev20241126104249.dist-info → letta_nightly-0.5.4.dev20241128000451.dist-info}/METADATA +1 -1
- {letta_nightly-0.5.4.dev20241126104249.dist-info → letta_nightly-0.5.4.dev20241128000451.dist-info}/RECORD +46 -45
- {letta_nightly-0.5.4.dev20241126104249.dist-info → letta_nightly-0.5.4.dev20241128000451.dist-info}/LICENSE +0 -0
- {letta_nightly-0.5.4.dev20241126104249.dist-info → letta_nightly-0.5.4.dev20241128000451.dist-info}/WHEEL +0 -0
- {letta_nightly-0.5.4.dev20241126104249.dist-info → letta_nightly-0.5.4.dev20241128000451.dist-info}/entry_points.txt +0 -0
letta/schemas/agent.py
CHANGED
|
@@ -2,15 +2,18 @@ from datetime import datetime
|
|
|
2
2
|
from enum import Enum
|
|
3
3
|
from typing import Dict, List, Optional
|
|
4
4
|
|
|
5
|
-
from pydantic import BaseModel, Field, field_validator
|
|
5
|
+
from pydantic import BaseModel, Field, field_validator
|
|
6
6
|
|
|
7
|
+
from letta.schemas.block import CreateBlock
|
|
7
8
|
from letta.schemas.embedding_config import EmbeddingConfig
|
|
8
9
|
from letta.schemas.letta_base import LettaBase
|
|
9
10
|
from letta.schemas.llm_config import LLMConfig
|
|
10
11
|
from letta.schemas.memory import Memory
|
|
11
12
|
from letta.schemas.message import Message
|
|
12
13
|
from letta.schemas.openai.chat_completion_response import UsageStatistics
|
|
13
|
-
from letta.schemas.
|
|
14
|
+
from letta.schemas.source import Source
|
|
15
|
+
from letta.schemas.tool import Tool
|
|
16
|
+
from letta.schemas.tool_rule import ToolRule
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
class BaseAgent(LettaBase, validate_assignment=True):
|
|
@@ -32,23 +35,8 @@ class AgentType(str, Enum):
|
|
|
32
35
|
o1_agent = "o1_agent"
|
|
33
36
|
|
|
34
37
|
|
|
35
|
-
class
|
|
36
|
-
|
|
37
|
-
Representation of an agent's state. This is the state of the agent at a given time, and is persisted in the DB backend. The state has all the information needed to recreate a persisted agent.
|
|
38
|
-
|
|
39
|
-
Parameters:
|
|
40
|
-
id (str): The unique identifier of the agent.
|
|
41
|
-
name (str): The name of the agent (must be unique to the user).
|
|
42
|
-
created_at (datetime): The datetime the agent was created.
|
|
43
|
-
message_ids (List[str]): The ids of the messages in the agent's in-context memory.
|
|
44
|
-
memory (Memory): The in-context memory of the agent.
|
|
45
|
-
tools (List[str]): The tools used by the agent. This includes any memory editing functions specified in `memory`.
|
|
46
|
-
system (str): The system prompt used by the agent.
|
|
47
|
-
llm_config (LLMConfig): The LLM configuration used by the agent.
|
|
48
|
-
embedding_config (EmbeddingConfig): The embedding configuration used by the agent.
|
|
49
|
-
|
|
50
|
-
"""
|
|
51
|
-
|
|
38
|
+
class PersistedAgentState(BaseAgent, validate_assignment=True):
|
|
39
|
+
# NOTE: this has been changed to represent the data stored in the ORM, NOT what is passed around internally or returned to the user
|
|
52
40
|
id: str = BaseAgent.generate_id_field()
|
|
53
41
|
name: str = Field(..., description="The name of the agent.")
|
|
54
42
|
created_at: datetime = Field(..., description="The datetime the agent was created.", default_factory=datetime.now)
|
|
@@ -56,16 +44,12 @@ class AgentState(BaseAgent, validate_assignment=True):
|
|
|
56
44
|
# in-context memory
|
|
57
45
|
message_ids: Optional[List[str]] = Field(default=None, description="The ids of the messages in the agent's in-context memory.")
|
|
58
46
|
|
|
59
|
-
memory: Memory = Field(default_factory=Memory, description="The in-context memory of the agent.")
|
|
60
|
-
|
|
61
47
|
# tools
|
|
62
|
-
|
|
48
|
+
# TODO: move to ORM mapping
|
|
49
|
+
tool_names: List[str] = Field(..., description="The tools used by the agent.")
|
|
63
50
|
|
|
64
51
|
# tool rules
|
|
65
|
-
tool_rules: Optional[List[
|
|
66
|
-
|
|
67
|
-
# tags
|
|
68
|
-
tags: Optional[List[str]] = Field(None, description="The tags associated with the agent.")
|
|
52
|
+
tool_rules: Optional[List[ToolRule]] = Field(default=None, description="The list of tool rules.")
|
|
69
53
|
|
|
70
54
|
# system prompt
|
|
71
55
|
system: str = Field(..., description="The system prompt used by the agent.")
|
|
@@ -77,40 +61,62 @@ class AgentState(BaseAgent, validate_assignment=True):
|
|
|
77
61
|
llm_config: LLMConfig = Field(..., description="The LLM configuration used by the agent.")
|
|
78
62
|
embedding_config: EmbeddingConfig = Field(..., description="The embedding configuration used by the agent.")
|
|
79
63
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
64
|
+
class Config:
|
|
65
|
+
arbitrary_types_allowed = True
|
|
66
|
+
validate_assignment = True
|
|
83
67
|
|
|
84
|
-
@model_validator(mode="after")
|
|
85
|
-
def verify_memory_type(self):
|
|
86
|
-
try:
|
|
87
|
-
assert isinstance(self.memory, Memory)
|
|
88
|
-
except Exception as e:
|
|
89
|
-
raise e
|
|
90
|
-
return self
|
|
91
68
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
69
|
+
class AgentState(PersistedAgentState):
|
|
70
|
+
"""
|
|
71
|
+
Representation of an agent's state. This is the state of the agent at a given time, and is persisted in the DB backend. The state has all the information needed to recreate a persisted agent.
|
|
72
|
+
|
|
73
|
+
Parameters:
|
|
74
|
+
id (str): The unique identifier of the agent.
|
|
75
|
+
name (str): The name of the agent (must be unique to the user).
|
|
76
|
+
created_at (datetime): The datetime the agent was created.
|
|
77
|
+
message_ids (List[str]): The ids of the messages in the agent's in-context memory.
|
|
78
|
+
memory (Memory): The in-context memory of the agent.
|
|
79
|
+
tools (List[str]): The tools used by the agent. This includes any memory editing functions specified in `memory`.
|
|
80
|
+
system (str): The system prompt used by the agent.
|
|
81
|
+
llm_config (LLMConfig): The LLM configuration used by the agent.
|
|
82
|
+
embedding_config (EmbeddingConfig): The embedding configuration used by the agent.
|
|
95
83
|
|
|
96
|
-
|
|
97
|
-
def memory(self, value):
|
|
98
|
-
if not isinstance(value, Memory):
|
|
99
|
-
raise TypeError(f"Expected Memory, got {type(value).__name__}")
|
|
100
|
-
self._internal_memory = value
|
|
84
|
+
"""
|
|
101
85
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
86
|
+
# NOTE: this is what is returned to the client and also what is used to initialize `Agent`
|
|
87
|
+
|
|
88
|
+
# This is an object representing the in-process state of a running `Agent`
|
|
89
|
+
# Field in this object can be theoretically edited by tools, and will be persisted by the ORM
|
|
90
|
+
memory: Memory = Field(..., description="The in-context memory of the agent.")
|
|
91
|
+
tools: List[Tool] = Field(..., description="The tools used by the agent.")
|
|
92
|
+
sources: List[Source] = Field(..., description="The sources used by the agent.")
|
|
93
|
+
tags: List[str] = Field(..., description="The tags associated with the agent.")
|
|
94
|
+
# TODO: add in context message objects
|
|
105
95
|
|
|
96
|
+
def to_persisted_agent_state(self) -> PersistedAgentState:
|
|
97
|
+
# turn back into persisted agent
|
|
98
|
+
data = self.model_dump()
|
|
99
|
+
del data["memory"]
|
|
100
|
+
del data["tools"]
|
|
101
|
+
del data["sources"]
|
|
102
|
+
del data["tags"]
|
|
103
|
+
return PersistedAgentState(**data)
|
|
106
104
|
|
|
107
|
-
|
|
105
|
+
|
|
106
|
+
class CreateAgent(BaseAgent): #
|
|
108
107
|
# all optional as server can generate defaults
|
|
109
108
|
name: Optional[str] = Field(None, description="The name of the agent.")
|
|
110
109
|
message_ids: Optional[List[str]] = Field(None, description="The ids of the messages in the agent's in-context memory.")
|
|
111
|
-
|
|
110
|
+
|
|
111
|
+
# memory creation
|
|
112
|
+
memory_blocks: List[CreateBlock] = Field(
|
|
113
|
+
# [CreateHuman(), CreatePersona()], description="The blocks to create in the agent's in-context memory."
|
|
114
|
+
...,
|
|
115
|
+
description="The blocks to create in the agent's in-context memory.",
|
|
116
|
+
)
|
|
117
|
+
|
|
112
118
|
tools: Optional[List[str]] = Field(None, description="The tools used by the agent.")
|
|
113
|
-
tool_rules: Optional[List[
|
|
119
|
+
tool_rules: Optional[List[ToolRule]] = Field(None, description="The tool rules governing the agent.")
|
|
114
120
|
tags: Optional[List[str]] = Field(None, description="The tags associated with the agent.")
|
|
115
121
|
system: Optional[str] = Field(None, description="The system prompt used by the agent.")
|
|
116
122
|
agent_type: Optional[AgentType] = Field(None, description="The type of agent.")
|
|
@@ -151,7 +157,7 @@ class CreateAgent(BaseAgent):
|
|
|
151
157
|
class UpdateAgentState(BaseAgent):
|
|
152
158
|
id: str = Field(..., description="The id of the agent.")
|
|
153
159
|
name: Optional[str] = Field(None, description="The name of the agent.")
|
|
154
|
-
|
|
160
|
+
tool_names: Optional[List[str]] = Field(None, description="The tools used by the agent.")
|
|
155
161
|
tags: Optional[List[str]] = Field(None, description="The tags associated with the agent.")
|
|
156
162
|
system: Optional[str] = Field(None, description="The system prompt used by the agent.")
|
|
157
163
|
llm_config: Optional[LLMConfig] = Field(None, description="The LLM configuration used by the agent.")
|
|
@@ -159,7 +165,6 @@ class UpdateAgentState(BaseAgent):
|
|
|
159
165
|
|
|
160
166
|
# TODO: determine if these should be editable via this schema?
|
|
161
167
|
message_ids: Optional[List[str]] = Field(None, description="The ids of the messages in the agent's in-context memory.")
|
|
162
|
-
memory: Optional[Memory] = Field(None, description="The in-context memory of the agent.")
|
|
163
168
|
|
|
164
169
|
|
|
165
170
|
class AgentStepResponse(BaseModel):
|
letta/schemas/block.py
CHANGED
|
@@ -28,17 +28,20 @@ class BaseBlock(LettaBase, validate_assignment=True):
|
|
|
28
28
|
description: Optional[str] = Field(None, description="Description of the block.")
|
|
29
29
|
metadata_: Optional[dict] = Field({}, description="Metadata of the block.")
|
|
30
30
|
|
|
31
|
+
# def __len__(self):
|
|
32
|
+
# return len(self.value)
|
|
33
|
+
|
|
34
|
+
class Config:
|
|
35
|
+
extra = "ignore" # Ignores extra fields
|
|
36
|
+
|
|
31
37
|
@model_validator(mode="after")
|
|
32
38
|
def verify_char_limit(self) -> Self:
|
|
33
|
-
if len(self.value) > self.limit:
|
|
39
|
+
if self.value and len(self.value) > self.limit:
|
|
34
40
|
error_msg = f"Edit failed: Exceeds {self.limit} character limit (requested {len(self.value)}) - {str(self)}."
|
|
35
41
|
raise ValueError(error_msg)
|
|
36
42
|
|
|
37
43
|
return self
|
|
38
44
|
|
|
39
|
-
# def __len__(self):
|
|
40
|
-
# return len(self.value)
|
|
41
|
-
|
|
42
45
|
def __setattr__(self, name, value):
|
|
43
46
|
"""Run validation if self.value is updated"""
|
|
44
47
|
super().__setattr__(name, value)
|
|
@@ -46,9 +49,6 @@ class BaseBlock(LettaBase, validate_assignment=True):
|
|
|
46
49
|
# run validation
|
|
47
50
|
self.__class__.model_validate(self.model_dump(exclude_unset=True))
|
|
48
51
|
|
|
49
|
-
class Config:
|
|
50
|
-
extra = "ignore" # Ignores extra fields
|
|
51
|
-
|
|
52
52
|
|
|
53
53
|
class Block(BaseBlock):
|
|
54
54
|
"""
|
|
@@ -88,11 +88,11 @@ class Persona(Block):
|
|
|
88
88
|
label: str = "persona"
|
|
89
89
|
|
|
90
90
|
|
|
91
|
-
class
|
|
92
|
-
"""Create a block"""
|
|
93
|
-
|
|
94
|
-
is_template: bool = True
|
|
95
|
-
label: str = Field(..., description="Label of the block.")
|
|
91
|
+
# class CreateBlock(BaseBlock):
|
|
92
|
+
# """Create a block"""
|
|
93
|
+
#
|
|
94
|
+
# is_template: bool = True
|
|
95
|
+
# label: str = Field(..., description="Label of the block.")
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
class BlockLabelUpdate(BaseModel):
|
|
@@ -102,16 +102,16 @@ class BlockLabelUpdate(BaseModel):
|
|
|
102
102
|
new_label: str = Field(..., description="New label of the block.")
|
|
103
103
|
|
|
104
104
|
|
|
105
|
-
class CreatePersona(
|
|
106
|
-
"""Create a persona block"""
|
|
107
|
-
|
|
108
|
-
label: str = "persona"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
class CreateHuman(
|
|
112
|
-
"""Create a human block"""
|
|
113
|
-
|
|
114
|
-
label: str = "human"
|
|
105
|
+
# class CreatePersona(CreateBlock):
|
|
106
|
+
# """Create a persona block"""
|
|
107
|
+
#
|
|
108
|
+
# label: str = "persona"
|
|
109
|
+
#
|
|
110
|
+
#
|
|
111
|
+
# class CreateHuman(CreateBlock):
|
|
112
|
+
# """Create a human block"""
|
|
113
|
+
#
|
|
114
|
+
# label: str = "human"
|
|
115
115
|
|
|
116
116
|
|
|
117
117
|
class BlockUpdate(BaseBlock):
|
|
@@ -131,13 +131,57 @@ class BlockLimitUpdate(BaseModel):
|
|
|
131
131
|
limit: int = Field(..., description="New limit of the block.")
|
|
132
132
|
|
|
133
133
|
|
|
134
|
-
class UpdatePersona(BlockUpdate):
|
|
135
|
-
"""Update a persona block"""
|
|
134
|
+
# class UpdatePersona(BlockUpdate):
|
|
135
|
+
# """Update a persona block"""
|
|
136
|
+
#
|
|
137
|
+
# label: str = "persona"
|
|
138
|
+
#
|
|
139
|
+
#
|
|
140
|
+
# class UpdateHuman(BlockUpdate):
|
|
141
|
+
# """Update a human block"""
|
|
142
|
+
#
|
|
143
|
+
# label: str = "human"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class CreateBlock(BaseBlock):
|
|
147
|
+
"""Create a block"""
|
|
148
|
+
|
|
149
|
+
label: str = Field(..., description="Label of the block.")
|
|
150
|
+
limit: int = Field(2000, description="Character limit of the block.")
|
|
151
|
+
value: str = Field(..., description="Value of the block.")
|
|
152
|
+
|
|
153
|
+
# block templates
|
|
154
|
+
is_template: bool = False
|
|
155
|
+
template_name: Optional[str] = Field(None, description="Name of the block if it is a template.", alias="name")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class CreateHuman(CreateBlock):
|
|
159
|
+
"""Create a human block"""
|
|
160
|
+
|
|
161
|
+
label: str = "human"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class CreatePersona(CreateBlock):
|
|
165
|
+
"""Create a persona block"""
|
|
136
166
|
|
|
137
167
|
label: str = "persona"
|
|
138
168
|
|
|
139
169
|
|
|
140
|
-
class
|
|
141
|
-
"""
|
|
170
|
+
class CreateBlockTemplate(CreateBlock):
|
|
171
|
+
"""Create a block template"""
|
|
172
|
+
|
|
173
|
+
is_template: bool = True
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class CreateHumanBlockTemplate(CreateHuman):
|
|
177
|
+
"""Create a human block template"""
|
|
142
178
|
|
|
179
|
+
is_template: bool = True
|
|
143
180
|
label: str = "human"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class CreatePersonaBlockTemplate(CreatePersona):
|
|
184
|
+
"""Create a persona block template"""
|
|
185
|
+
|
|
186
|
+
is_template: bool = True
|
|
187
|
+
label: str = "persona"
|
letta/schemas/enums.py
CHANGED
|
@@ -33,3 +33,17 @@ class MessageStreamStatus(str, Enum):
|
|
|
33
33
|
done_generation = "[DONE_GEN]"
|
|
34
34
|
done_step = "[DONE_STEP]"
|
|
35
35
|
done = "[DONE]"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ToolRuleType(str, Enum):
|
|
39
|
+
"""
|
|
40
|
+
Type of tool rule.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# note: some of these should be renamed when we do the data migration
|
|
44
|
+
|
|
45
|
+
run_first = "InitToolRule"
|
|
46
|
+
exit_loop = "TerminalToolRule" # reasoning loop should exit
|
|
47
|
+
continue_loop = "continue_loop" # reasoning loop should continue
|
|
48
|
+
constrain_child_tools = "ToolRule"
|
|
49
|
+
require_parent_tools = "require_parent_tools"
|
letta/schemas/letta_base.py
CHANGED
|
@@ -63,7 +63,7 @@ class LettaBase(BaseModel):
|
|
|
63
63
|
@classmethod
|
|
64
64
|
def _id_example(cls, prefix: str):
|
|
65
65
|
"""generates an example id for a given prefix"""
|
|
66
|
-
return
|
|
66
|
+
return f"{prefix}-123e4567-e89b-12d3-a456-426614174000"
|
|
67
67
|
|
|
68
68
|
@classmethod
|
|
69
69
|
def _id_description(cls, prefix: str):
|
letta/schemas/letta_request.py
CHANGED
|
@@ -8,33 +8,21 @@ from letta.schemas.message import Message, MessageCreate
|
|
|
8
8
|
|
|
9
9
|
class LettaRequest(BaseModel):
|
|
10
10
|
messages: Union[List[MessageCreate], List[Message]] = Field(..., description="The messages to be sent to the agent.")
|
|
11
|
-
run_async: bool = Field(default=False, description="Whether to asynchronously send the messages to the agent.") # TODO: implement
|
|
12
|
-
|
|
13
|
-
stream_steps: bool = Field(
|
|
14
|
-
default=False, description="Flag to determine if the response should be streamed. Set to True for streaming agent steps."
|
|
15
|
-
)
|
|
16
|
-
stream_tokens: bool = Field(
|
|
17
|
-
default=False,
|
|
18
|
-
description="Flag to determine if individual tokens should be streamed. Set to True for token streaming (requires stream_steps = True).",
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
return_message_object: bool = Field(
|
|
22
|
-
default=False,
|
|
23
|
-
description="Set True to return the raw Message object. Set False to return the Message in the format of the Letta API.",
|
|
24
|
-
)
|
|
25
11
|
|
|
26
12
|
# Flags to support the use of AssistantMessage message types
|
|
27
13
|
|
|
28
|
-
|
|
29
|
-
default=False,
|
|
30
|
-
description="[Only applicable if return_message_object is False] If true, returns AssistantMessage objects when the agent calls a designated message tool. If false, return FunctionCallMessage objects for all tool calls.",
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
assistant_message_function_name: str = Field(
|
|
14
|
+
assistant_message_tool_name: str = Field(
|
|
34
15
|
default=DEFAULT_MESSAGE_TOOL,
|
|
35
|
-
description="
|
|
16
|
+
description="The name of the designated message tool.",
|
|
36
17
|
)
|
|
37
|
-
|
|
18
|
+
assistant_message_tool_kwarg: str = Field(
|
|
38
19
|
default=DEFAULT_MESSAGE_TOOL_KWARG,
|
|
39
|
-
description="
|
|
20
|
+
description="The name of the message argument in the designated message tool.",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LettaStreamingRequest(LettaRequest):
|
|
25
|
+
stream_tokens: bool = Field(
|
|
26
|
+
default=False,
|
|
27
|
+
description="Flag to determine if individual tokens should be streamed. Set to True for token streaming (requires stream_steps = True).",
|
|
40
28
|
)
|
letta/schemas/letta_response.py
CHANGED
|
@@ -7,7 +7,6 @@ from pydantic import BaseModel, Field
|
|
|
7
7
|
|
|
8
8
|
from letta.schemas.enums import MessageStreamStatus
|
|
9
9
|
from letta.schemas.letta_message import LettaMessage, LettaMessageUnion
|
|
10
|
-
from letta.schemas.message import Message
|
|
11
10
|
from letta.schemas.usage import LettaUsageStatistics
|
|
12
11
|
from letta.utils import json_dumps
|
|
13
12
|
|
|
@@ -24,7 +23,7 @@ class LettaResponse(BaseModel):
|
|
|
24
23
|
usage (LettaUsageStatistics): The usage statistics
|
|
25
24
|
"""
|
|
26
25
|
|
|
27
|
-
messages:
|
|
26
|
+
messages: List[LettaMessageUnion] = Field(..., description="The messages returned by the agent.")
|
|
28
27
|
usage: LettaUsageStatistics = Field(..., description="The usage statistics of the agent.")
|
|
29
28
|
|
|
30
29
|
def __str__(self):
|
letta/schemas/memory.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING,
|
|
1
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
2
2
|
|
|
3
3
|
from jinja2 import Template, TemplateSyntaxError
|
|
4
4
|
from pydantic import BaseModel, Field
|
|
@@ -55,19 +55,16 @@ class ContextWindowOverview(BaseModel):
|
|
|
55
55
|
class Memory(BaseModel, validate_assignment=True):
|
|
56
56
|
"""
|
|
57
57
|
|
|
58
|
-
Represents the in-context memory of the agent. This includes both the `Block` objects (labelled by sections), as well as tools to edit the blocks.
|
|
59
|
-
|
|
60
|
-
Attributes:
|
|
61
|
-
memory (Dict[str, Block]): Mapping from memory block section to memory block.
|
|
58
|
+
Represents the in-context memory (i.e. Core memory) of the agent. This includes both the `Block` objects (labelled by sections), as well as tools to edit the blocks.
|
|
62
59
|
|
|
63
60
|
"""
|
|
64
61
|
|
|
65
|
-
# Memory.
|
|
66
|
-
|
|
62
|
+
# Memory.block contains the list of memory blocks in the core memory
|
|
63
|
+
blocks: List[Block] = Field(..., description="Memory blocks contained in the agent's in-context memory")
|
|
67
64
|
|
|
68
65
|
# Memory.template is a Jinja2 template for compiling memory module into a prompt string.
|
|
69
66
|
prompt_template: str = Field(
|
|
70
|
-
default="{% for block in
|
|
67
|
+
default="{% for block in blocks %}"
|
|
71
68
|
'<{{ block.label }} characters="{{ block.value|length }}/{{ block.limit }}">\n'
|
|
72
69
|
"{{ block.value }}\n"
|
|
73
70
|
"</{{ block.label }}>"
|
|
@@ -90,7 +87,7 @@ class Memory(BaseModel, validate_assignment=True):
|
|
|
90
87
|
Template(prompt_template)
|
|
91
88
|
|
|
92
89
|
# Validate compatibility with current memory structure
|
|
93
|
-
test_render = Template(prompt_template).render(
|
|
90
|
+
test_render = Template(prompt_template).render(blocks=self.blocks)
|
|
94
91
|
|
|
95
92
|
# If we get here, the template is valid and compatible
|
|
96
93
|
self.prompt_template = prompt_template
|
|
@@ -99,107 +96,49 @@ class Memory(BaseModel, validate_assignment=True):
|
|
|
99
96
|
except Exception as e:
|
|
100
97
|
raise ValueError(f"Prompt template is not compatible with current memory structure: {str(e)}")
|
|
101
98
|
|
|
102
|
-
@classmethod
|
|
103
|
-
def load(cls, state: dict):
|
|
104
|
-
"""Load memory from dictionary object"""
|
|
105
|
-
obj = cls()
|
|
106
|
-
if len(state.keys()) == 2 and "memory" in state and "prompt_template" in state:
|
|
107
|
-
# New format
|
|
108
|
-
obj.prompt_template = state["prompt_template"]
|
|
109
|
-
for key, value in state["memory"].items():
|
|
110
|
-
# TODO: This is migration code, please take a look at a later time to get rid of this
|
|
111
|
-
if "name" in value:
|
|
112
|
-
value["template_name"] = value["name"]
|
|
113
|
-
value.pop("name")
|
|
114
|
-
obj.memory[key] = Block(**value)
|
|
115
|
-
else:
|
|
116
|
-
# Old format (pre-template)
|
|
117
|
-
for key, value in state.items():
|
|
118
|
-
obj.memory[key] = Block(**value)
|
|
119
|
-
return obj
|
|
120
|
-
|
|
121
99
|
def compile(self) -> str:
|
|
122
100
|
"""Generate a string representation of the memory in-context using the Jinja2 template"""
|
|
123
101
|
template = Template(self.prompt_template)
|
|
124
|
-
return template.render(
|
|
125
|
-
|
|
126
|
-
def to_dict(self):
|
|
127
|
-
"""Convert to dictionary representation"""
|
|
128
|
-
return {
|
|
129
|
-
"memory": {key: value.model_dump() for key, value in self.memory.items()},
|
|
130
|
-
"prompt_template": self.prompt_template,
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
def to_flat_dict(self):
|
|
134
|
-
"""Convert to a dictionary that maps directly from block label to values"""
|
|
135
|
-
return {k: v.value for k, v in self.memory.items() if v is not None}
|
|
102
|
+
return template.render(blocks=self.blocks)
|
|
136
103
|
|
|
137
104
|
def list_block_labels(self) -> List[str]:
|
|
138
105
|
"""Return a list of the block names held inside the memory object"""
|
|
139
|
-
return list(self.memory.keys())
|
|
106
|
+
# return list(self.memory.keys())
|
|
107
|
+
return [block.label for block in self.blocks]
|
|
140
108
|
|
|
141
109
|
# TODO: these should actually be label, not name
|
|
142
110
|
def get_block(self, label: str) -> Block:
|
|
143
111
|
"""Correct way to index into the memory.memory field, returns a Block"""
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
112
|
+
keys = []
|
|
113
|
+
for block in self.blocks:
|
|
114
|
+
if block.label == label:
|
|
115
|
+
return block
|
|
116
|
+
keys.append(block.label)
|
|
117
|
+
raise KeyError(f"Block field {label} does not exist (available sections = {', '.join(keys)})")
|
|
148
118
|
|
|
149
119
|
def get_blocks(self) -> List[Block]:
|
|
150
120
|
"""Return a list of the blocks held inside the memory object"""
|
|
151
|
-
return list(self.memory.values())
|
|
152
|
-
|
|
153
|
-
def link_block(self, block: Block, override: Optional[bool] = False):
|
|
154
|
-
"""Link a new block to the memory object"""
|
|
155
|
-
if not isinstance(block, Block):
|
|
156
|
-
raise ValueError(f"Param block must be type Block (not {type(block)})")
|
|
157
|
-
if not override and block.label in self.memory:
|
|
158
|
-
raise ValueError(f"Block with label {block.label} already exists")
|
|
159
|
-
|
|
160
|
-
self.memory[block.label] = block
|
|
121
|
+
# return list(self.memory.values())
|
|
122
|
+
return self.blocks
|
|
161
123
|
|
|
162
|
-
def
|
|
163
|
-
"""
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
124
|
+
def set_block(self, block: Block):
|
|
125
|
+
"""Set a block in the memory object"""
|
|
126
|
+
for i, b in enumerate(self.blocks):
|
|
127
|
+
if b.label == block.label:
|
|
128
|
+
self.blocks[i] = block
|
|
129
|
+
return
|
|
130
|
+
self.blocks.append(block)
|
|
168
131
|
|
|
169
132
|
def update_block_value(self, label: str, value: str):
|
|
170
133
|
"""Update the value of a block"""
|
|
171
|
-
if label not in self.memory:
|
|
172
|
-
raise ValueError(f"Block with label {label} does not exist")
|
|
173
134
|
if not isinstance(value, str):
|
|
174
135
|
raise ValueError(f"Provided value must be a string")
|
|
175
136
|
|
|
176
|
-
self.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
raise ValueError(f"Block with label {current_label} does not exist")
|
|
182
|
-
if not isinstance(new_label, str):
|
|
183
|
-
raise ValueError(f"Provided new label must be a string")
|
|
184
|
-
|
|
185
|
-
# First change the label of the block
|
|
186
|
-
self.memory[current_label].label = new_label
|
|
187
|
-
|
|
188
|
-
# Then swap the block to the new label
|
|
189
|
-
self.memory[new_label] = self.memory.pop(current_label)
|
|
190
|
-
|
|
191
|
-
def update_block_limit(self, label: str, limit: int):
|
|
192
|
-
"""Update the limit of a block"""
|
|
193
|
-
if label not in self.memory:
|
|
194
|
-
raise ValueError(f"Block with label {label} does not exist")
|
|
195
|
-
if not isinstance(limit, int):
|
|
196
|
-
raise ValueError(f"Provided limit must be an integer")
|
|
197
|
-
|
|
198
|
-
# Check to make sure the new limit is greater than the current length of the block
|
|
199
|
-
if len(self.memory[label].value) > limit:
|
|
200
|
-
raise ValueError(f"New limit {limit} is less than the current length of the block {len(self.memory[label].value)}")
|
|
201
|
-
|
|
202
|
-
self.memory[label].limit = limit
|
|
137
|
+
for block in self.blocks:
|
|
138
|
+
if block.label == label:
|
|
139
|
+
block.value = value
|
|
140
|
+
return
|
|
141
|
+
raise ValueError(f"Block with label {label} does not exist")
|
|
203
142
|
|
|
204
143
|
|
|
205
144
|
# TODO: ideally this is refactored into ChatMemory and the subclasses are given more specific names.
|
|
@@ -222,13 +161,7 @@ class BasicBlockMemory(Memory):
|
|
|
222
161
|
Args:
|
|
223
162
|
blocks (List[Block]): List of blocks to be linked to the memory object.
|
|
224
163
|
"""
|
|
225
|
-
super().__init__()
|
|
226
|
-
for block in blocks:
|
|
227
|
-
# TODO: centralize these internal schema validations
|
|
228
|
-
# assert block.name is not None and block.name != "", "each existing chat block must have a name"
|
|
229
|
-
# self.link_block(name=block.name, block=block)
|
|
230
|
-
assert block.label is not None and block.label != "", "each existing chat block must have a name"
|
|
231
|
-
self.link_block(block=block)
|
|
164
|
+
super().__init__(blocks=blocks)
|
|
232
165
|
|
|
233
166
|
def core_memory_append(agent_state: "AgentState", label: str, content: str) -> Optional[str]: # type: ignore
|
|
234
167
|
"""
|
|
@@ -280,9 +213,7 @@ class ChatMemory(BasicBlockMemory):
|
|
|
280
213
|
human (str): The starter value for the human block.
|
|
281
214
|
limit (int): The character limit for each block.
|
|
282
215
|
"""
|
|
283
|
-
super().__init__()
|
|
284
|
-
self.link_block(block=Block(value=persona, limit=limit, label="persona"))
|
|
285
|
-
self.link_block(block=Block(value=human, limit=limit, label="human"))
|
|
216
|
+
super().__init__(blocks=[Block(value=persona, limit=limit, label="persona"), Block(value=human, limit=limit, label="human")])
|
|
286
217
|
|
|
287
218
|
|
|
288
219
|
class UpdateMemory(BaseModel):
|
letta/schemas/message.py
CHANGED
|
@@ -134,8 +134,8 @@ class Message(BaseMessage):
|
|
|
134
134
|
def to_letta_message(
|
|
135
135
|
self,
|
|
136
136
|
assistant_message: bool = False,
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
assistant_message_tool_name: str = DEFAULT_MESSAGE_TOOL,
|
|
138
|
+
assistant_message_tool_kwarg: str = DEFAULT_MESSAGE_TOOL_KWARG,
|
|
139
139
|
) -> List[LettaMessage]:
|
|
140
140
|
"""Convert message object (in DB format) to the style used by the original Letta API"""
|
|
141
141
|
|
|
@@ -156,7 +156,7 @@ class Message(BaseMessage):
|
|
|
156
156
|
for tool_call in self.tool_calls:
|
|
157
157
|
# If we're supporting using assistant message,
|
|
158
158
|
# then we want to treat certain function calls as a special case
|
|
159
|
-
if assistant_message and tool_call.function.name ==
|
|
159
|
+
if assistant_message and tool_call.function.name == assistant_message_tool_name:
|
|
160
160
|
# We need to unpack the actual message contents from the function call
|
|
161
161
|
try:
|
|
162
162
|
func_args = json.loads(tool_call.function.arguments)
|
letta/schemas/tool_rule.py
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
from typing import List
|
|
1
|
+
from typing import List, Union
|
|
2
2
|
|
|
3
3
|
from pydantic import Field
|
|
4
4
|
|
|
5
|
+
from letta.schemas.enums import ToolRuleType
|
|
5
6
|
from letta.schemas.letta_base import LettaBase
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class BaseToolRule(LettaBase):
|
|
9
10
|
__id_prefix__ = "tool_rule"
|
|
10
11
|
tool_name: str = Field(..., description="The name of the tool. Must exist in the database for the user's organization.")
|
|
12
|
+
type: ToolRuleType
|
|
11
13
|
|
|
12
14
|
|
|
13
|
-
class
|
|
15
|
+
class ChildToolRule(BaseToolRule):
|
|
14
16
|
"""
|
|
15
17
|
A ToolRule represents a tool that can be invoked by the agent.
|
|
16
18
|
"""
|
|
17
19
|
|
|
18
|
-
type: str = Field("ToolRule")
|
|
20
|
+
# type: str = Field("ToolRule")
|
|
21
|
+
type: ToolRuleType = ToolRuleType.constrain_child_tools
|
|
19
22
|
children: List[str] = Field(..., description="The children tools that can be invoked.")
|
|
20
23
|
|
|
21
24
|
|
|
@@ -24,7 +27,8 @@ class InitToolRule(BaseToolRule):
|
|
|
24
27
|
Represents the initial tool rule configuration.
|
|
25
28
|
"""
|
|
26
29
|
|
|
27
|
-
type: str = Field("InitToolRule")
|
|
30
|
+
# type: str = Field("InitToolRule")
|
|
31
|
+
type: ToolRuleType = ToolRuleType.run_first
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
class TerminalToolRule(BaseToolRule):
|
|
@@ -32,4 +36,8 @@ class TerminalToolRule(BaseToolRule):
|
|
|
32
36
|
Represents a terminal tool rule configuration where if this tool gets called, it must end the agent loop.
|
|
33
37
|
"""
|
|
34
38
|
|
|
35
|
-
type: str = Field("TerminalToolRule")
|
|
39
|
+
# type: str = Field("TerminalToolRule")
|
|
40
|
+
type: ToolRuleType = ToolRuleType.exit_loop
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
ToolRule = Union[ChildToolRule, InitToolRule, TerminalToolRule]
|