letta-nightly 0.6.39.dev20250314104053__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.

Files changed (59) hide show
  1. letta/agent.py +13 -3
  2. letta/agents/ephemeral_agent.py +2 -1
  3. letta/agents/low_latency_agent.py +8 -0
  4. letta/dynamic_multi_agent.py +274 -0
  5. letta/functions/function_sets/base.py +1 -0
  6. letta/functions/function_sets/extras.py +2 -1
  7. letta/functions/function_sets/multi_agent.py +17 -0
  8. letta/functions/helpers.py +41 -0
  9. letta/helpers/converters.py +67 -0
  10. letta/helpers/mcp_helpers.py +26 -5
  11. letta/llm_api/openai.py +1 -1
  12. letta/memory.py +2 -1
  13. letta/orm/__init__.py +2 -0
  14. letta/orm/agent.py +69 -20
  15. letta/orm/custom_columns.py +15 -0
  16. letta/orm/group.py +33 -0
  17. letta/orm/groups_agents.py +13 -0
  18. letta/orm/message.py +7 -4
  19. letta/orm/organization.py +1 -0
  20. letta/orm/sqlalchemy_base.py +3 -3
  21. letta/round_robin_multi_agent.py +152 -0
  22. letta/schemas/agent.py +3 -0
  23. letta/schemas/enums.py +0 -4
  24. letta/schemas/group.py +65 -0
  25. letta/schemas/letta_message.py +167 -106
  26. letta/schemas/letta_message_content.py +192 -0
  27. letta/schemas/message.py +28 -36
  28. letta/serialize_schemas/__init__.py +1 -1
  29. letta/serialize_schemas/marshmallow_agent.py +108 -0
  30. letta/serialize_schemas/{agent_environment_variable.py → marshmallow_agent_environment_variable.py} +1 -1
  31. letta/serialize_schemas/marshmallow_base.py +52 -0
  32. letta/serialize_schemas/{block.py → marshmallow_block.py} +1 -1
  33. letta/serialize_schemas/{custom_fields.py → marshmallow_custom_fields.py} +12 -0
  34. letta/serialize_schemas/marshmallow_message.py +42 -0
  35. letta/serialize_schemas/{tag.py → marshmallow_tag.py} +12 -2
  36. letta/serialize_schemas/{tool.py → marshmallow_tool.py} +1 -1
  37. letta/serialize_schemas/pydantic_agent_schema.py +111 -0
  38. letta/server/rest_api/app.py +15 -0
  39. letta/server/rest_api/routers/v1/__init__.py +2 -0
  40. letta/server/rest_api/routers/v1/agents.py +46 -40
  41. letta/server/rest_api/routers/v1/groups.py +233 -0
  42. letta/server/rest_api/routers/v1/tools.py +31 -3
  43. letta/server/rest_api/utils.py +1 -1
  44. letta/server/server.py +267 -12
  45. letta/services/agent_manager.py +65 -28
  46. letta/services/group_manager.py +147 -0
  47. letta/services/helpers/agent_manager_helper.py +151 -1
  48. letta/services/message_manager.py +11 -3
  49. letta/services/passage_manager.py +15 -0
  50. letta/settings.py +5 -0
  51. letta/supervisor_multi_agent.py +103 -0
  52. {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/METADATA +1 -2
  53. {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/RECORD +56 -46
  54. letta/serialize_schemas/agent.py +0 -80
  55. letta/serialize_schemas/base.py +0 -64
  56. letta/serialize_schemas/message.py +0 -29
  57. {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/LICENSE +0 -0
  58. {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/WHEEL +0 -0
  59. {letta_nightly-0.6.39.dev20250314104053.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
- def to_pydantic(self) -> PydanticAgentState:
133
- """converts to the basic pydantic model counterpart"""
134
- # add default rule for having send_message be a terminal tool
135
- tool_rules = self.tool_rules
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
- "organization_id": self.organization_id,
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
- "agent_type": self.agent_type,
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
- "identity_ids": [identity.id for identity in self.identities],
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)
@@ -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
- """custom pydantic conversion for message content mapping"""
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"]]:
@@ -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)).group_by(cls.id)
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
- # Group by primary key and all necessary columns to avoid JSON comparison
146
- query = query.group_by(cls.id)
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
@@ -9,10 +9,6 @@ class MessageRole(str, Enum):
9
9
  system = "system"
10
10
 
11
11
 
12
- class MessageContentType(str, Enum):
13
- text = "text"
14
-
15
-
16
12
  class OptionState(str, Enum):
17
13
  """Useful for kwargs that are bool + default option"""
18
14
 
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="")