letta-nightly 0.6.39.dev20250313162623__py3-none-any.whl → 0.6.40.dev20250314173529__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 +13 -3
- letta/agents/ephemeral_agent.py +2 -1
- letta/agents/low_latency_agent.py +8 -0
- letta/dynamic_multi_agent.py +274 -0
- letta/functions/function_sets/base.py +1 -0
- letta/functions/function_sets/extras.py +2 -1
- letta/functions/function_sets/multi_agent.py +17 -0
- letta/functions/helpers.py +41 -0
- letta/helpers/converters.py +67 -0
- letta/helpers/mcp_helpers.py +26 -5
- letta/llm_api/openai.py +1 -1
- letta/memory.py +2 -1
- letta/orm/__init__.py +2 -0
- letta/orm/agent.py +69 -20
- letta/orm/custom_columns.py +15 -0
- letta/orm/group.py +33 -0
- letta/orm/groups_agents.py +13 -0
- letta/orm/message.py +7 -4
- letta/orm/organization.py +1 -0
- letta/orm/sqlalchemy_base.py +3 -3
- letta/round_robin_multi_agent.py +152 -0
- letta/schemas/agent.py +3 -0
- letta/schemas/enums.py +0 -4
- letta/schemas/group.py +65 -0
- letta/schemas/letta_message.py +167 -106
- letta/schemas/letta_message_content.py +192 -0
- letta/schemas/message.py +28 -36
- letta/serialize_schemas/__init__.py +1 -1
- letta/serialize_schemas/marshmallow_agent.py +108 -0
- letta/serialize_schemas/{agent_environment_variable.py → marshmallow_agent_environment_variable.py} +1 -1
- letta/serialize_schemas/marshmallow_base.py +52 -0
- letta/serialize_schemas/{block.py → marshmallow_block.py} +1 -1
- letta/serialize_schemas/{custom_fields.py → marshmallow_custom_fields.py} +12 -0
- letta/serialize_schemas/marshmallow_message.py +42 -0
- letta/serialize_schemas/{tag.py → marshmallow_tag.py} +12 -2
- letta/serialize_schemas/{tool.py → marshmallow_tool.py} +1 -1
- letta/serialize_schemas/pydantic_agent_schema.py +111 -0
- letta/server/rest_api/app.py +15 -0
- letta/server/rest_api/routers/v1/__init__.py +2 -0
- letta/server/rest_api/routers/v1/agents.py +46 -40
- letta/server/rest_api/routers/v1/groups.py +233 -0
- letta/server/rest_api/routers/v1/tools.py +31 -3
- letta/server/rest_api/utils.py +1 -1
- letta/server/server.py +267 -12
- letta/services/agent_manager.py +65 -28
- letta/services/group_manager.py +147 -0
- letta/services/helpers/agent_manager_helper.py +151 -1
- letta/services/message_manager.py +11 -3
- letta/services/passage_manager.py +15 -0
- letta/settings.py +5 -0
- letta/supervisor_multi_agent.py +103 -0
- {letta_nightly-0.6.39.dev20250313162623.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/METADATA +1 -2
- {letta_nightly-0.6.39.dev20250313162623.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/RECORD +56 -46
- letta/serialize_schemas/agent.py +0 -80
- letta/serialize_schemas/base.py +0 -64
- letta/serialize_schemas/message.py +0 -29
- {letta_nightly-0.6.39.dev20250313162623.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.39.dev20250313162623.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.39.dev20250313162623.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/entry_points.txt +0 -0
letta/orm/agent.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import uuid
|
|
2
|
-
from typing import TYPE_CHECKING, List, Optional
|
|
2
|
+
from typing import TYPE_CHECKING, List, Optional, Set
|
|
3
3
|
|
|
4
4
|
from sqlalchemy import JSON, Boolean, Index, String
|
|
5
5
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
@@ -128,37 +128,86 @@ class Agent(SqlalchemyBase, OrganizationMixin):
|
|
|
128
128
|
back_populates="agents",
|
|
129
129
|
passive_deletes=True,
|
|
130
130
|
)
|
|
131
|
+
groups: Mapped[List["Group"]] = relationship(
|
|
132
|
+
"Group",
|
|
133
|
+
secondary="groups_agents",
|
|
134
|
+
lazy="selectin",
|
|
135
|
+
back_populates="agents",
|
|
136
|
+
passive_deletes=True,
|
|
137
|
+
)
|
|
138
|
+
multi_agent_group: Mapped["Group"] = relationship(
|
|
139
|
+
"Group",
|
|
140
|
+
lazy="joined",
|
|
141
|
+
viewonly=True,
|
|
142
|
+
back_populates="manager_agent",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def to_pydantic(self, include_relationships: Optional[Set[str]] = None) -> PydanticAgentState:
|
|
146
|
+
"""
|
|
147
|
+
Converts the SQLAlchemy Agent model into its Pydantic counterpart.
|
|
148
|
+
|
|
149
|
+
The following base fields are always included:
|
|
150
|
+
- id, agent_type, name, description, system, message_ids, metadata_,
|
|
151
|
+
llm_config, embedding_config, project_id, template_id, base_template_id,
|
|
152
|
+
tool_rules, message_buffer_autoclear, tags
|
|
131
153
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
154
|
+
Everything else (e.g., tools, sources, memory, etc.) is optional and only
|
|
155
|
+
included if specified in `include_fields`.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
include_relationships (Optional[Set[str]]):
|
|
159
|
+
A set of additional field names to include in the output. If None or empty,
|
|
160
|
+
no extra fields are loaded beyond the base fields.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
PydanticAgentState: The Pydantic representation of the agent.
|
|
164
|
+
"""
|
|
165
|
+
# Base fields: always included
|
|
136
166
|
state = {
|
|
137
167
|
"id": self.id,
|
|
138
|
-
"
|
|
168
|
+
"agent_type": self.agent_type,
|
|
139
169
|
"name": self.name,
|
|
140
170
|
"description": self.description,
|
|
141
|
-
"message_ids": self.message_ids,
|
|
142
|
-
"tools": self.tools,
|
|
143
|
-
"sources": [source.to_pydantic() for source in self.sources],
|
|
144
|
-
"tags": [t.tag for t in self.tags],
|
|
145
|
-
"tool_rules": tool_rules,
|
|
146
171
|
"system": self.system,
|
|
147
|
-
"
|
|
172
|
+
"message_ids": self.message_ids,
|
|
173
|
+
"metadata": self.metadata_, # Exposed as 'metadata' to Pydantic
|
|
148
174
|
"llm_config": self.llm_config,
|
|
149
175
|
"embedding_config": self.embedding_config,
|
|
150
|
-
"metadata": self.metadata_,
|
|
151
|
-
"memory": Memory(blocks=[b.to_pydantic() for b in self.core_memory]),
|
|
152
|
-
"created_by_id": self.created_by_id,
|
|
153
|
-
"last_updated_by_id": self.last_updated_by_id,
|
|
154
|
-
"created_at": self.created_at,
|
|
155
|
-
"updated_at": self.updated_at,
|
|
156
|
-
"tool_exec_environment_variables": self.tool_exec_environment_variables,
|
|
157
176
|
"project_id": self.project_id,
|
|
158
177
|
"template_id": self.template_id,
|
|
159
178
|
"base_template_id": self.base_template_id,
|
|
160
|
-
"
|
|
179
|
+
"tool_rules": self.tool_rules,
|
|
161
180
|
"message_buffer_autoclear": self.message_buffer_autoclear,
|
|
181
|
+
"created_by_id": self.created_by_id,
|
|
182
|
+
"last_updated_by_id": self.last_updated_by_id,
|
|
183
|
+
"created_at": self.created_at,
|
|
184
|
+
"updated_at": self.updated_at,
|
|
185
|
+
# optional field defaults
|
|
186
|
+
"tags": [],
|
|
187
|
+
"tools": [],
|
|
188
|
+
"sources": [],
|
|
189
|
+
"memory": Memory(blocks=[]),
|
|
190
|
+
"identity_ids": [],
|
|
191
|
+
"multi_agent_group": None,
|
|
192
|
+
"tool_exec_environment_variables": [],
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# Optional fields: only included if requested
|
|
196
|
+
optional_fields = {
|
|
197
|
+
"tags": lambda: [t.tag for t in self.tags],
|
|
198
|
+
"tools": lambda: self.tools,
|
|
199
|
+
"sources": lambda: [s.to_pydantic() for s in self.sources],
|
|
200
|
+
"memory": lambda: Memory(blocks=[b.to_pydantic() for b in self.core_memory]),
|
|
201
|
+
"identity_ids": lambda: [i.id for i in self.identities],
|
|
202
|
+
"multi_agent_group": lambda: self.multi_agent_group,
|
|
203
|
+
"tool_exec_environment_variables": lambda: self.tool_exec_environment_variables,
|
|
162
204
|
}
|
|
163
205
|
|
|
206
|
+
include_relationships = set(optional_fields.keys() if include_relationships is None else include_relationships)
|
|
207
|
+
|
|
208
|
+
for field_name in include_relationships:
|
|
209
|
+
resolver = optional_fields.get(field_name)
|
|
210
|
+
if resolver:
|
|
211
|
+
state[field_name] = resolver()
|
|
212
|
+
|
|
164
213
|
return self.__pydantic_model__(**state)
|
letta/orm/custom_columns.py
CHANGED
|
@@ -4,12 +4,14 @@ from sqlalchemy.types import BINARY, TypeDecorator
|
|
|
4
4
|
from letta.helpers.converters import (
|
|
5
5
|
deserialize_embedding_config,
|
|
6
6
|
deserialize_llm_config,
|
|
7
|
+
deserialize_message_content,
|
|
7
8
|
deserialize_tool_calls,
|
|
8
9
|
deserialize_tool_returns,
|
|
9
10
|
deserialize_tool_rules,
|
|
10
11
|
deserialize_vector,
|
|
11
12
|
serialize_embedding_config,
|
|
12
13
|
serialize_llm_config,
|
|
14
|
+
serialize_message_content,
|
|
13
15
|
serialize_tool_calls,
|
|
14
16
|
serialize_tool_returns,
|
|
15
17
|
serialize_tool_rules,
|
|
@@ -82,6 +84,19 @@ class ToolReturnColumn(TypeDecorator):
|
|
|
82
84
|
return deserialize_tool_returns(value)
|
|
83
85
|
|
|
84
86
|
|
|
87
|
+
class MessageContentColumn(TypeDecorator):
|
|
88
|
+
"""Custom SQLAlchemy column type for storing the content parts of a message as JSON."""
|
|
89
|
+
|
|
90
|
+
impl = JSON
|
|
91
|
+
cache_ok = True
|
|
92
|
+
|
|
93
|
+
def process_bind_param(self, value, dialect):
|
|
94
|
+
return serialize_message_content(value)
|
|
95
|
+
|
|
96
|
+
def process_result_value(self, value, dialect):
|
|
97
|
+
return deserialize_message_content(value)
|
|
98
|
+
|
|
99
|
+
|
|
85
100
|
class CommonVector(TypeDecorator):
|
|
86
101
|
"""Custom SQLAlchemy column type for storing vectors in SQLite."""
|
|
87
102
|
|
letta/orm/group.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import ForeignKey, String
|
|
5
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
6
|
+
|
|
7
|
+
from letta.orm.mixins import OrganizationMixin
|
|
8
|
+
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
|
9
|
+
from letta.schemas.group import Group as PydanticGroup
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Group(SqlalchemyBase, OrganizationMixin):
|
|
13
|
+
|
|
14
|
+
__tablename__ = "groups"
|
|
15
|
+
__pydantic_model__ = PydanticGroup
|
|
16
|
+
|
|
17
|
+
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"group-{uuid.uuid4()}")
|
|
18
|
+
description: Mapped[str] = mapped_column(nullable=False, doc="")
|
|
19
|
+
manager_type: Mapped[str] = mapped_column(nullable=False, doc="")
|
|
20
|
+
manager_agent_id: Mapped[Optional[str]] = mapped_column(String, ForeignKey("agents.id", ondelete="RESTRICT"), nullable=True, doc="")
|
|
21
|
+
termination_token: Mapped[Optional[str]] = mapped_column(nullable=True, doc="")
|
|
22
|
+
max_turns: Mapped[Optional[int]] = mapped_column(nullable=True, doc="")
|
|
23
|
+
|
|
24
|
+
# relationships
|
|
25
|
+
organization: Mapped["Organization"] = relationship("Organization", back_populates="groups")
|
|
26
|
+
agents: Mapped[List["Agent"]] = relationship(
|
|
27
|
+
"Agent", secondary="groups_agents", lazy="selectin", passive_deletes=True, back_populates="groups"
|
|
28
|
+
)
|
|
29
|
+
manager_agent: Mapped["Agent"] = relationship("Agent", lazy="joined", back_populates="multi_agent_group")
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def agent_ids(self) -> List[str]:
|
|
33
|
+
return [agent.id for agent in self.agents]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from sqlalchemy import ForeignKey, String
|
|
2
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
3
|
+
|
|
4
|
+
from letta.orm.base import Base
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GroupsAgents(Base):
|
|
8
|
+
"""Agents may have one or many groups associated with them."""
|
|
9
|
+
|
|
10
|
+
__tablename__ = "groups_agents"
|
|
11
|
+
|
|
12
|
+
group_id: Mapped[str] = mapped_column(String, ForeignKey("groups.id", ondelete="CASCADE"), primary_key=True)
|
|
13
|
+
agent_id: Mapped[str] = mapped_column(String, ForeignKey("agents.id", ondelete="CASCADE"), primary_key=True)
|
letta/orm/message.py
CHANGED
|
@@ -4,11 +4,12 @@ from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMe
|
|
|
4
4
|
from sqlalchemy import ForeignKey, Index
|
|
5
5
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
6
6
|
|
|
7
|
-
from letta.orm.custom_columns import ToolCallColumn, ToolReturnColumn
|
|
7
|
+
from letta.orm.custom_columns import MessageContentColumn, ToolCallColumn, ToolReturnColumn
|
|
8
8
|
from letta.orm.mixins import AgentMixin, OrganizationMixin
|
|
9
9
|
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
|
10
|
+
from letta.schemas.letta_message_content import MessageContent
|
|
11
|
+
from letta.schemas.letta_message_content import TextContent as PydanticTextContent
|
|
10
12
|
from letta.schemas.message import Message as PydanticMessage
|
|
11
|
-
from letta.schemas.message import TextContent as PydanticTextContent
|
|
12
13
|
from letta.schemas.message import ToolReturn
|
|
13
14
|
|
|
14
15
|
|
|
@@ -25,6 +26,7 @@ class Message(SqlalchemyBase, OrganizationMixin, AgentMixin):
|
|
|
25
26
|
id: Mapped[str] = mapped_column(primary_key=True, doc="Unique message identifier")
|
|
26
27
|
role: Mapped[str] = mapped_column(doc="Message role (user/assistant/system/tool)")
|
|
27
28
|
text: Mapped[Optional[str]] = mapped_column(nullable=True, doc="Message content")
|
|
29
|
+
content: Mapped[List[MessageContent]] = mapped_column(MessageContentColumn, nullable=True, doc="Message content parts")
|
|
28
30
|
model: Mapped[Optional[str]] = mapped_column(nullable=True, doc="LLM model used")
|
|
29
31
|
name: Mapped[Optional[str]] = mapped_column(nullable=True, doc="Name for multi-agent scenarios")
|
|
30
32
|
tool_calls: Mapped[List[OpenAIToolCall]] = mapped_column(ToolCallColumn, doc="Tool call information")
|
|
@@ -36,6 +38,7 @@ class Message(SqlalchemyBase, OrganizationMixin, AgentMixin):
|
|
|
36
38
|
tool_returns: Mapped[List[ToolReturn]] = mapped_column(
|
|
37
39
|
ToolReturnColumn, nullable=True, doc="Tool execution return information for prior tool calls"
|
|
38
40
|
)
|
|
41
|
+
group_id: Mapped[Optional[str]] = mapped_column(nullable=True, doc="The multi-agent group that the message was sent in")
|
|
39
42
|
|
|
40
43
|
# Relationships
|
|
41
44
|
agent: Mapped["Agent"] = relationship("Agent", back_populates="messages", lazy="selectin")
|
|
@@ -53,8 +56,8 @@ class Message(SqlalchemyBase, OrganizationMixin, AgentMixin):
|
|
|
53
56
|
return self.job_message.job if self.job_message else None
|
|
54
57
|
|
|
55
58
|
def to_pydantic(self) -> PydanticMessage:
|
|
56
|
-
"""
|
|
59
|
+
"""Custom pydantic conversion to handle data using legacy text field"""
|
|
57
60
|
model = self.__pydantic_model__.model_validate(self)
|
|
58
|
-
if self.text:
|
|
61
|
+
if self.text and not model.content:
|
|
59
62
|
model.content = [PydanticTextContent(text=self.text)]
|
|
60
63
|
return model
|
letta/orm/organization.py
CHANGED
|
@@ -49,6 +49,7 @@ class Organization(SqlalchemyBase):
|
|
|
49
49
|
agent_passages: Mapped[List["AgentPassage"]] = relationship("AgentPassage", back_populates="organization", cascade="all, delete-orphan")
|
|
50
50
|
providers: Mapped[List["Provider"]] = relationship("Provider", back_populates="organization", cascade="all, delete-orphan")
|
|
51
51
|
identities: Mapped[List["Identity"]] = relationship("Identity", back_populates="organization", cascade="all, delete-orphan")
|
|
52
|
+
groups: Mapped[List["Group"]] = relationship("Group", back_populates="organization", cascade="all, delete-orphan")
|
|
52
53
|
|
|
53
54
|
@property
|
|
54
55
|
def passages(self) -> List[Union["SourcePassage", "AgentPassage"]]:
|
letta/orm/sqlalchemy_base.py
CHANGED
|
@@ -139,11 +139,11 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
|
139
139
|
else:
|
|
140
140
|
# Match ANY tag - use join and filter
|
|
141
141
|
query = (
|
|
142
|
-
query.join(cls.tags).filter(cls.tags.property.mapper.class_.tag.in_(tags)).
|
|
142
|
+
query.join(cls.tags).filter(cls.tags.property.mapper.class_.tag.in_(tags)).distinct(cls.id).order_by(cls.id)
|
|
143
143
|
) # Deduplicate results
|
|
144
144
|
|
|
145
|
-
#
|
|
146
|
-
query = query.
|
|
145
|
+
# select distinct primary key
|
|
146
|
+
query = query.distinct(cls.id).order_by(cls.id)
|
|
147
147
|
|
|
148
148
|
if identifier_keys and hasattr(cls, "identities"):
|
|
149
149
|
query = query.join(cls.identities).filter(cls.identities.property.mapper.class_.identifier_key.in_(identifier_keys))
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from letta.agent import Agent, AgentState
|
|
4
|
+
from letta.interface import AgentInterface
|
|
5
|
+
from letta.orm import User
|
|
6
|
+
from letta.schemas.letta_message_content import TextContent
|
|
7
|
+
from letta.schemas.message import Message, MessageCreate
|
|
8
|
+
from letta.schemas.openai.chat_completion_response import UsageStatistics
|
|
9
|
+
from letta.schemas.usage import LettaUsageStatistics
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RoundRobinMultiAgent(Agent):
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
interface: AgentInterface,
|
|
16
|
+
agent_state: AgentState,
|
|
17
|
+
user: User = None,
|
|
18
|
+
# custom
|
|
19
|
+
group_id: str = "",
|
|
20
|
+
agent_ids: List[str] = [],
|
|
21
|
+
description: str = "",
|
|
22
|
+
max_turns: Optional[int] = None,
|
|
23
|
+
):
|
|
24
|
+
super().__init__(interface, agent_state, user)
|
|
25
|
+
self.group_id = group_id
|
|
26
|
+
self.agent_ids = agent_ids
|
|
27
|
+
self.description = description
|
|
28
|
+
self.max_turns = max_turns or len(agent_ids)
|
|
29
|
+
|
|
30
|
+
def step(
|
|
31
|
+
self,
|
|
32
|
+
messages: List[MessageCreate],
|
|
33
|
+
chaining: bool = True,
|
|
34
|
+
max_chaining_steps: Optional[int] = None,
|
|
35
|
+
put_inner_thoughts_first: bool = True,
|
|
36
|
+
**kwargs,
|
|
37
|
+
) -> LettaUsageStatistics:
|
|
38
|
+
total_usage = UsageStatistics()
|
|
39
|
+
step_count = 0
|
|
40
|
+
|
|
41
|
+
token_streaming = self.interface.streaming_mode if hasattr(self.interface, "streaming_mode") else False
|
|
42
|
+
metadata = self.interface.metadata if hasattr(self.interface, "metadata") else None
|
|
43
|
+
|
|
44
|
+
agents = {}
|
|
45
|
+
for agent_id in self.agent_ids:
|
|
46
|
+
agents[agent_id] = self.load_participant_agent(agent_id=agent_id)
|
|
47
|
+
|
|
48
|
+
message_index = {}
|
|
49
|
+
chat_history: List[Message] = []
|
|
50
|
+
new_messages = messages
|
|
51
|
+
speaker_id = None
|
|
52
|
+
try:
|
|
53
|
+
for i in range(self.max_turns):
|
|
54
|
+
speaker_id = self.agent_ids[i % len(self.agent_ids)]
|
|
55
|
+
# initialize input messages
|
|
56
|
+
start_index = message_index[speaker_id] if speaker_id in message_index else 0
|
|
57
|
+
for message in chat_history[start_index:]:
|
|
58
|
+
message.id = Message.generate_id()
|
|
59
|
+
message.agent_id = speaker_id
|
|
60
|
+
|
|
61
|
+
for message in new_messages:
|
|
62
|
+
chat_history.append(
|
|
63
|
+
Message(
|
|
64
|
+
agent_id=speaker_id,
|
|
65
|
+
role=message.role,
|
|
66
|
+
content=[TextContent(text=message.content)],
|
|
67
|
+
name=message.name,
|
|
68
|
+
model=None,
|
|
69
|
+
tool_calls=None,
|
|
70
|
+
tool_call_id=None,
|
|
71
|
+
group_id=self.group_id,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# load agent and perform step
|
|
76
|
+
participant_agent = agents[speaker_id]
|
|
77
|
+
usage_stats = participant_agent.step(
|
|
78
|
+
messages=chat_history[start_index:],
|
|
79
|
+
chaining=chaining,
|
|
80
|
+
max_chaining_steps=max_chaining_steps,
|
|
81
|
+
stream=token_streaming,
|
|
82
|
+
skip_verify=True,
|
|
83
|
+
metadata=metadata,
|
|
84
|
+
put_inner_thoughts_first=put_inner_thoughts_first,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# parse new messages for next step
|
|
88
|
+
responses = Message.to_letta_messages_from_list(participant_agent.last_response_messages)
|
|
89
|
+
assistant_messages = [response for response in responses if response.message_type == "assistant_message"]
|
|
90
|
+
new_messages = [
|
|
91
|
+
MessageCreate(
|
|
92
|
+
role="system",
|
|
93
|
+
content=message.content,
|
|
94
|
+
name=participant_agent.agent_state.name,
|
|
95
|
+
)
|
|
96
|
+
for message in assistant_messages
|
|
97
|
+
]
|
|
98
|
+
message_index[speaker_id] = len(chat_history) + len(new_messages)
|
|
99
|
+
|
|
100
|
+
# sum usage
|
|
101
|
+
total_usage.prompt_tokens += usage_stats.prompt_tokens
|
|
102
|
+
total_usage.completion_tokens += usage_stats.completion_tokens
|
|
103
|
+
total_usage.total_tokens += usage_stats.total_tokens
|
|
104
|
+
step_count += 1
|
|
105
|
+
|
|
106
|
+
# persist remaining chat history
|
|
107
|
+
for message in new_messages:
|
|
108
|
+
chat_history.append(
|
|
109
|
+
Message(
|
|
110
|
+
agent_id=agent_id,
|
|
111
|
+
role=message.role,
|
|
112
|
+
content=[TextContent(text=message.content)],
|
|
113
|
+
name=message.name,
|
|
114
|
+
model=None,
|
|
115
|
+
tool_calls=None,
|
|
116
|
+
tool_call_id=None,
|
|
117
|
+
group_id=self.group_id,
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
for agent_id, index in message_index.items():
|
|
121
|
+
if agent_id == speaker_id:
|
|
122
|
+
continue
|
|
123
|
+
for message in chat_history[index:]:
|
|
124
|
+
message.id = Message.generate_id()
|
|
125
|
+
message.agent_id = agent_id
|
|
126
|
+
self.message_manager.create_many_messages(chat_history[index:], actor=self.user)
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
raise e
|
|
130
|
+
finally:
|
|
131
|
+
self.interface.step_yield()
|
|
132
|
+
|
|
133
|
+
self.interface.step_complete()
|
|
134
|
+
|
|
135
|
+
return LettaUsageStatistics(**total_usage.model_dump(), step_count=step_count)
|
|
136
|
+
|
|
137
|
+
def load_participant_agent(self, agent_id: str) -> Agent:
|
|
138
|
+
agent_state = self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=self.user)
|
|
139
|
+
persona_block = agent_state.memory.get_block(label="persona")
|
|
140
|
+
group_chat_participant_persona = (
|
|
141
|
+
"\n\n====Group Chat Contex===="
|
|
142
|
+
f"\nYou are speaking in a group chat with {len(self.agent_ids) - 1} other "
|
|
143
|
+
"agents and one user. Respond to new messages in the group chat when prompted. "
|
|
144
|
+
f"Description of the group: {self.description}"
|
|
145
|
+
)
|
|
146
|
+
agent_state.memory.update_block_value(label="persona", value=persona_block.value + group_chat_participant_persona)
|
|
147
|
+
return Agent(
|
|
148
|
+
agent_state=agent_state,
|
|
149
|
+
interface=self.interface,
|
|
150
|
+
user=self.user,
|
|
151
|
+
save_last_response=True,
|
|
152
|
+
)
|
letta/schemas/agent.py
CHANGED
|
@@ -7,6 +7,7 @@ from letta.constants import DEFAULT_EMBEDDING_CHUNK_SIZE
|
|
|
7
7
|
from letta.schemas.block import CreateBlock
|
|
8
8
|
from letta.schemas.embedding_config import EmbeddingConfig
|
|
9
9
|
from letta.schemas.environment_variables import AgentEnvironmentVariable
|
|
10
|
+
from letta.schemas.group import Group
|
|
10
11
|
from letta.schemas.letta_base import OrmMetadataBase
|
|
11
12
|
from letta.schemas.llm_config import LLMConfig
|
|
12
13
|
from letta.schemas.memory import Memory
|
|
@@ -90,6 +91,8 @@ class AgentState(OrmMetadataBase, validate_assignment=True):
|
|
|
90
91
|
description="If set to True, the agent will not remember previous messages (though the agent will still retain state via core memory blocks and archival/recall memory). Not recommended unless you have an advanced use case.",
|
|
91
92
|
)
|
|
92
93
|
|
|
94
|
+
multi_agent_group: Optional[Group] = Field(None, description="The multi-agent group that this agent manages")
|
|
95
|
+
|
|
93
96
|
def get_agent_env_vars_as_dict(self) -> Dict[str, str]:
|
|
94
97
|
# Get environment variables for this agent specifically
|
|
95
98
|
per_agent_env_vars = {}
|
letta/schemas/enums.py
CHANGED
letta/schemas/group.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Annotated, List, Literal, Optional, Union
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
from letta.schemas.letta_base import LettaBase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ManagerType(str, Enum):
|
|
10
|
+
round_robin = "round_robin"
|
|
11
|
+
supervisor = "supervisor"
|
|
12
|
+
dynamic = "dynamic"
|
|
13
|
+
swarm = "swarm"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GroupBase(LettaBase):
|
|
17
|
+
__id_prefix__ = "group"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Group(GroupBase):
|
|
21
|
+
id: str = Field(..., description="The id of the group. Assigned by the database.")
|
|
22
|
+
manager_type: ManagerType = Field(..., description="")
|
|
23
|
+
agent_ids: List[str] = Field(..., description="")
|
|
24
|
+
description: str = Field(..., description="")
|
|
25
|
+
# Pattern fields
|
|
26
|
+
manager_agent_id: Optional[str] = Field(None, description="")
|
|
27
|
+
termination_token: Optional[str] = Field(None, description="")
|
|
28
|
+
max_turns: Optional[int] = Field(None, description="")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ManagerConfig(BaseModel):
|
|
32
|
+
manager_type: ManagerType = Field(..., description="")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RoundRobinManager(ManagerConfig):
|
|
36
|
+
manager_type: Literal[ManagerType.round_robin] = Field(ManagerType.round_robin, description="")
|
|
37
|
+
max_turns: Optional[int] = Field(None, description="")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SupervisorManager(ManagerConfig):
|
|
41
|
+
manager_type: Literal[ManagerType.supervisor] = Field(ManagerType.supervisor, description="")
|
|
42
|
+
manager_agent_id: str = Field(..., description="")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DynamicManager(ManagerConfig):
|
|
46
|
+
manager_type: Literal[ManagerType.dynamic] = Field(ManagerType.dynamic, description="")
|
|
47
|
+
manager_agent_id: str = Field(..., description="")
|
|
48
|
+
termination_token: Optional[str] = Field("DONE!", description="")
|
|
49
|
+
max_turns: Optional[int] = Field(None, description="")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# class SwarmGroup(ManagerConfig):
|
|
53
|
+
# manager_type: Literal[ManagerType.swarm] = Field(ManagerType.swarm, description="")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
ManagerConfigUnion = Annotated[
|
|
57
|
+
Union[RoundRobinManager, SupervisorManager, DynamicManager],
|
|
58
|
+
Field(discriminator="manager_type"),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class GroupCreate(BaseModel):
|
|
63
|
+
agent_ids: List[str] = Field(..., description="")
|
|
64
|
+
description: str = Field(..., description="")
|
|
65
|
+
manager_config: Optional[ManagerConfigUnion] = Field(None, description="")
|