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.
- 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.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/METADATA +1 -2
- {letta_nightly-0.6.39.dev20250314104053.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.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from sqlalchemy.orm import Session
|
|
4
|
+
|
|
5
|
+
from letta.orm.agent import Agent as AgentModel
|
|
6
|
+
from letta.orm.errors import NoResultFound
|
|
7
|
+
from letta.orm.group import Group as GroupModel
|
|
8
|
+
from letta.orm.message import Message as MessageModel
|
|
9
|
+
from letta.schemas.group import Group as PydanticGroup
|
|
10
|
+
from letta.schemas.group import GroupCreate, ManagerType
|
|
11
|
+
from letta.schemas.user import User as PydanticUser
|
|
12
|
+
from letta.utils import enforce_types
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GroupManager:
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
from letta.server.db import db_context
|
|
19
|
+
|
|
20
|
+
self.session_maker = db_context
|
|
21
|
+
|
|
22
|
+
@enforce_types
|
|
23
|
+
def list_groups(
|
|
24
|
+
self,
|
|
25
|
+
project_id: Optional[str] = None,
|
|
26
|
+
manager_type: Optional[ManagerType] = None,
|
|
27
|
+
before: Optional[str] = None,
|
|
28
|
+
after: Optional[str] = None,
|
|
29
|
+
limit: Optional[int] = 50,
|
|
30
|
+
actor: PydanticUser = None,
|
|
31
|
+
) -> list[PydanticGroup]:
|
|
32
|
+
with self.session_maker() as session:
|
|
33
|
+
filters = {"organization_id": actor.organization_id}
|
|
34
|
+
if project_id:
|
|
35
|
+
filters["project_id"] = project_id
|
|
36
|
+
if manager_type:
|
|
37
|
+
filters["manager_type"] = manager_type
|
|
38
|
+
groups = GroupModel.list(
|
|
39
|
+
db_session=session,
|
|
40
|
+
before=before,
|
|
41
|
+
after=after,
|
|
42
|
+
limit=limit,
|
|
43
|
+
**filters,
|
|
44
|
+
)
|
|
45
|
+
return [group.to_pydantic() for group in groups]
|
|
46
|
+
|
|
47
|
+
@enforce_types
|
|
48
|
+
def retrieve_group(self, group_id: str, actor: PydanticUser) -> PydanticGroup:
|
|
49
|
+
with self.session_maker() as session:
|
|
50
|
+
group = GroupModel.read(db_session=session, identifier=group_id, actor=actor)
|
|
51
|
+
return group.to_pydantic()
|
|
52
|
+
|
|
53
|
+
@enforce_types
|
|
54
|
+
def create_group(self, group: GroupCreate, actor: PydanticUser) -> PydanticGroup:
|
|
55
|
+
with self.session_maker() as session:
|
|
56
|
+
new_group = GroupModel()
|
|
57
|
+
new_group.organization_id = actor.organization_id
|
|
58
|
+
new_group.description = group.description
|
|
59
|
+
self._process_agent_relationship(session=session, group=new_group, agent_ids=group.agent_ids, allow_partial=False)
|
|
60
|
+
if group.manager_config is None:
|
|
61
|
+
new_group.manager_type = ManagerType.round_robin
|
|
62
|
+
else:
|
|
63
|
+
match group.manager_config.manager_type:
|
|
64
|
+
case ManagerType.round_robin:
|
|
65
|
+
new_group.manager_type = ManagerType.round_robin
|
|
66
|
+
new_group.max_turns = group.manager_config.max_turns
|
|
67
|
+
case ManagerType.dynamic:
|
|
68
|
+
new_group.manager_type = ManagerType.dynamic
|
|
69
|
+
new_group.manager_agent_id = group.manager_config.manager_agent_id
|
|
70
|
+
new_group.max_turns = group.manager_config.max_turns
|
|
71
|
+
new_group.termination_token = group.manager_config.termination_token
|
|
72
|
+
case ManagerType.supervisor:
|
|
73
|
+
new_group.manager_type = ManagerType.supervisor
|
|
74
|
+
new_group.manager_agent_id = group.manager_config.manager_agent_id
|
|
75
|
+
case _:
|
|
76
|
+
raise ValueError(f"Unsupported manager type: {group.manager_config.manager_type}")
|
|
77
|
+
new_group.create(session, actor=actor)
|
|
78
|
+
return new_group.to_pydantic()
|
|
79
|
+
|
|
80
|
+
@enforce_types
|
|
81
|
+
def delete_group(self, group_id: str, actor: PydanticUser) -> None:
|
|
82
|
+
with self.session_maker() as session:
|
|
83
|
+
# Retrieve the agent
|
|
84
|
+
group = GroupModel.read(db_session=session, identifier=group_id, actor=actor)
|
|
85
|
+
group.hard_delete(session)
|
|
86
|
+
|
|
87
|
+
@enforce_types
|
|
88
|
+
def list_group_messages(
|
|
89
|
+
self,
|
|
90
|
+
group_id: Optional[str] = None,
|
|
91
|
+
before: Optional[str] = None,
|
|
92
|
+
after: Optional[str] = None,
|
|
93
|
+
limit: Optional[int] = 50,
|
|
94
|
+
actor: PydanticUser = None,
|
|
95
|
+
use_assistant_message: bool = True,
|
|
96
|
+
assistant_message_tool_name: str = "send_message",
|
|
97
|
+
assistant_message_tool_kwarg: str = "message",
|
|
98
|
+
) -> list[PydanticGroup]:
|
|
99
|
+
with self.session_maker() as session:
|
|
100
|
+
group = GroupModel.read(db_session=session, identifier=group_id, actor=actor)
|
|
101
|
+
agent_id = group.manager_agent_id if group.manager_agent_id else group.agent_ids[0]
|
|
102
|
+
|
|
103
|
+
filters = {
|
|
104
|
+
"organization_id": actor.organization_id,
|
|
105
|
+
"group_id": group_id,
|
|
106
|
+
"agent_id": agent_id,
|
|
107
|
+
}
|
|
108
|
+
messages = MessageModel.list(
|
|
109
|
+
db_session=session,
|
|
110
|
+
before=before,
|
|
111
|
+
after=after,
|
|
112
|
+
limit=limit,
|
|
113
|
+
**filters,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
messages = PydanticMessage.to_letta_messages_from_list(
|
|
117
|
+
messages=messages,
|
|
118
|
+
use_assistant_message=use_assistant_message,
|
|
119
|
+
assistant_message_tool_name=assistant_message_tool_name,
|
|
120
|
+
assistant_message_tool_kwarg=assistant_message_tool_kwarg,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return messages
|
|
124
|
+
|
|
125
|
+
def _process_agent_relationship(self, session: Session, group: GroupModel, agent_ids: List[str], allow_partial=False, replace=True):
|
|
126
|
+
current_relationship = getattr(group, "agents", [])
|
|
127
|
+
if not agent_ids:
|
|
128
|
+
if replace:
|
|
129
|
+
setattr(group, "agents", [])
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# Retrieve models for the provided IDs
|
|
133
|
+
found_items = session.query(AgentModel).filter(AgentModel.id.in_(agent_ids)).all()
|
|
134
|
+
|
|
135
|
+
# Validate all items are found if allow_partial is False
|
|
136
|
+
if not allow_partial and len(found_items) != len(agent_ids):
|
|
137
|
+
missing = set(agent_ids) - {item.id for item in found_items}
|
|
138
|
+
raise NoResultFound(f"Items not found in agents: {missing}")
|
|
139
|
+
|
|
140
|
+
if replace:
|
|
141
|
+
# Replace the relationship
|
|
142
|
+
setattr(group, "agents", found_items)
|
|
143
|
+
else:
|
|
144
|
+
# Extend the relationship (only add new items)
|
|
145
|
+
current_ids = {item.id for item in current_relationship}
|
|
146
|
+
new_items = [item for item in found_items if item.id not in current_ids]
|
|
147
|
+
current_relationship.extend(new_items)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
from typing import List, Literal, Optional
|
|
3
3
|
|
|
4
|
+
from sqlalchemy import and_, func, literal, or_, select
|
|
5
|
+
|
|
4
6
|
from letta import system
|
|
5
7
|
from letta.constants import IN_CONTEXT_MEMORY_KEYWORD, STRUCTURED_OUTPUT_MODELS
|
|
6
8
|
from letta.helpers import ToolRulesSolver
|
|
@@ -8,11 +10,13 @@ from letta.helpers.datetime_helpers import get_local_time
|
|
|
8
10
|
from letta.orm.agent import Agent as AgentModel
|
|
9
11
|
from letta.orm.agents_tags import AgentsTags
|
|
10
12
|
from letta.orm.errors import NoResultFound
|
|
13
|
+
from letta.orm.identity import Identity
|
|
11
14
|
from letta.prompts import gpt_system
|
|
12
15
|
from letta.schemas.agent import AgentState, AgentType
|
|
13
16
|
from letta.schemas.enums import MessageRole
|
|
17
|
+
from letta.schemas.letta_message_content import TextContent
|
|
14
18
|
from letta.schemas.memory import Memory
|
|
15
|
-
from letta.schemas.message import Message, MessageCreate
|
|
19
|
+
from letta.schemas.message import Message, MessageCreate
|
|
16
20
|
from letta.schemas.passage import Passage as PydanticPassage
|
|
17
21
|
from letta.schemas.tool_rule import ToolRule
|
|
18
22
|
from letta.schemas.user import User
|
|
@@ -293,3 +297,149 @@ def check_supports_structured_output(model: str, tool_rules: List[ToolRule]) ->
|
|
|
293
297
|
return False
|
|
294
298
|
else:
|
|
295
299
|
return True
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _apply_pagination(query, before: Optional[str], after: Optional[str], session) -> any:
|
|
303
|
+
"""
|
|
304
|
+
Apply cursor-based pagination filters using the agent's created_at timestamp with id as a tie-breaker.
|
|
305
|
+
|
|
306
|
+
Instead of relying on the UUID ordering, this function uses the agent's creation time
|
|
307
|
+
(and id for tie-breaking) to paginate the results. It performs a minimal lookup to fetch
|
|
308
|
+
only the created_at and id for the agent corresponding to the provided cursor.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
query: The SQLAlchemy query object to modify.
|
|
312
|
+
before (Optional[str]): Cursor (agent id) to return agents created before this agent.
|
|
313
|
+
after (Optional[str]): Cursor (agent id) to return agents created after this agent.
|
|
314
|
+
session: The active database session used to execute the minimal lookup.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
The modified query with pagination filters applied and ordered by created_at and id.
|
|
318
|
+
"""
|
|
319
|
+
if after:
|
|
320
|
+
# Retrieve only the created_at and id for the agent corresponding to the 'after' cursor.
|
|
321
|
+
result = session.execute(select(AgentModel.created_at, AgentModel.id).where(AgentModel.id == after)).first()
|
|
322
|
+
if result:
|
|
323
|
+
after_created_at, after_id = result
|
|
324
|
+
# Filter: include agents created after the reference, or at the same time but with a greater id.
|
|
325
|
+
query = query.where(
|
|
326
|
+
or_(
|
|
327
|
+
AgentModel.created_at > after_created_at,
|
|
328
|
+
and_(
|
|
329
|
+
AgentModel.created_at == after_created_at,
|
|
330
|
+
AgentModel.id > after_id,
|
|
331
|
+
),
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
if before:
|
|
335
|
+
# Retrieve only the created_at and id for the agent corresponding to the 'before' cursor.
|
|
336
|
+
result = session.execute(select(AgentModel.created_at, AgentModel.id).where(AgentModel.id == before)).first()
|
|
337
|
+
if result:
|
|
338
|
+
before_created_at, before_id = result
|
|
339
|
+
# Filter: include agents created before the reference, or at the same time but with a smaller id.
|
|
340
|
+
query = query.where(
|
|
341
|
+
or_(
|
|
342
|
+
AgentModel.created_at < before_created_at,
|
|
343
|
+
and_(
|
|
344
|
+
AgentModel.created_at == before_created_at,
|
|
345
|
+
AgentModel.id < before_id,
|
|
346
|
+
),
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
# Enforce a deterministic ordering: first by created_at, then by id.
|
|
350
|
+
query = query.order_by(AgentModel.created_at.asc(), AgentModel.id.asc())
|
|
351
|
+
return query
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _apply_tag_filter(query, tags: Optional[List[str]], match_all_tags: bool):
|
|
355
|
+
"""
|
|
356
|
+
Apply tag-based filtering to the agent query.
|
|
357
|
+
|
|
358
|
+
This helper function creates a subquery that groups agent IDs by their tags.
|
|
359
|
+
If `match_all_tags` is True, it filters agents that have all of the specified tags.
|
|
360
|
+
Otherwise, it filters agents that have any of the tags.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
query: The SQLAlchemy query object to be modified.
|
|
364
|
+
tags (Optional[List[str]]): A list of tags to filter agents.
|
|
365
|
+
match_all_tags (bool): If True, only return agents that match all provided tags.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
The modified query with tag filters applied.
|
|
369
|
+
"""
|
|
370
|
+
if tags:
|
|
371
|
+
# Build a subquery to select agent IDs that have the specified tags.
|
|
372
|
+
subquery = select(AgentsTags.agent_id).where(AgentsTags.tag.in_(tags)).group_by(AgentsTags.agent_id)
|
|
373
|
+
# If all tags must match, add a HAVING clause to ensure the count of tags equals the number provided.
|
|
374
|
+
if match_all_tags:
|
|
375
|
+
subquery = subquery.having(func.count(AgentsTags.tag) == literal(len(tags)))
|
|
376
|
+
# Filter the main query to include only agents present in the subquery.
|
|
377
|
+
query = query.where(AgentModel.id.in_(subquery))
|
|
378
|
+
return query
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _apply_identity_filters(query, identity_id: Optional[str], identifier_keys: Optional[List[str]]):
|
|
382
|
+
"""
|
|
383
|
+
Apply identity-related filters to the agent query.
|
|
384
|
+
|
|
385
|
+
This helper function joins the identities relationship and filters the agents based on
|
|
386
|
+
a specific identity ID and/or a list of identifier keys.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
query: The SQLAlchemy query object to be modified.
|
|
390
|
+
identity_id (Optional[str]): The identity ID to filter by.
|
|
391
|
+
identifier_keys (Optional[List[str]]): A list of identifier keys to filter agents.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
The modified query with identity filters applied.
|
|
395
|
+
"""
|
|
396
|
+
# Join the identities relationship and filter by a specific identity ID.
|
|
397
|
+
if identity_id:
|
|
398
|
+
query = query.join(AgentModel.identities).where(Identity.id == identity_id)
|
|
399
|
+
# Join the identities relationship and filter by a set of identifier keys.
|
|
400
|
+
if identifier_keys:
|
|
401
|
+
query = query.join(AgentModel.identities).where(Identity.identifier_key.in_(identifier_keys))
|
|
402
|
+
return query
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _apply_filters(
|
|
406
|
+
query,
|
|
407
|
+
name: Optional[str],
|
|
408
|
+
query_text: Optional[str],
|
|
409
|
+
project_id: Optional[str],
|
|
410
|
+
template_id: Optional[str],
|
|
411
|
+
base_template_id: Optional[str],
|
|
412
|
+
):
|
|
413
|
+
"""
|
|
414
|
+
Apply basic filtering criteria to the agent query.
|
|
415
|
+
|
|
416
|
+
This helper function adds WHERE clauses based on provided parameters such as
|
|
417
|
+
exact name, partial name match (using ILIKE), project ID, template ID, and base template ID.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
query: The SQLAlchemy query object to be modified.
|
|
421
|
+
name (Optional[str]): Exact name to filter by.
|
|
422
|
+
query_text (Optional[str]): Partial text to search in the agent's name (case-insensitive).
|
|
423
|
+
project_id (Optional[str]): Filter for agents belonging to a specific project.
|
|
424
|
+
template_id (Optional[str]): Filter for agents using a specific template.
|
|
425
|
+
base_template_id (Optional[str]): Filter for agents using a specific base template.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
The modified query with the applied filters.
|
|
429
|
+
"""
|
|
430
|
+
# Filter by exact agent name if provided.
|
|
431
|
+
if name:
|
|
432
|
+
query = query.where(AgentModel.name == name)
|
|
433
|
+
# Apply a case-insensitive partial match for the agent's name.
|
|
434
|
+
if query_text:
|
|
435
|
+
query = query.where(AgentModel.name.ilike(f"%{query_text}%"))
|
|
436
|
+
# Filter agents by project ID.
|
|
437
|
+
if project_id:
|
|
438
|
+
query = query.where(AgentModel.project_id == project_id)
|
|
439
|
+
# Filter agents by template ID.
|
|
440
|
+
if template_id:
|
|
441
|
+
query = query.where(AgentModel.template_id == template_id)
|
|
442
|
+
# Filter agents by base template ID.
|
|
443
|
+
if base_template_id:
|
|
444
|
+
query = query.where(AgentModel.base_template_id == base_template_id)
|
|
445
|
+
return query
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from typing import List, Optional
|
|
3
3
|
|
|
4
|
-
from sqlalchemy import and_, or_
|
|
4
|
+
from sqlalchemy import and_, exists, func, or_, select, text
|
|
5
5
|
|
|
6
6
|
from letta.log import get_logger
|
|
7
7
|
from letta.orm.agent import Agent as AgentModel
|
|
@@ -233,9 +233,17 @@ class MessageManager:
|
|
|
233
233
|
# Build a query that directly filters the Message table by agent_id.
|
|
234
234
|
query = session.query(MessageModel).filter(MessageModel.agent_id == agent_id)
|
|
235
235
|
|
|
236
|
-
# If query_text is provided, filter messages
|
|
236
|
+
# If query_text is provided, filter messages using subquery.
|
|
237
237
|
if query_text:
|
|
238
|
-
|
|
238
|
+
content_element = func.json_array_elements(MessageModel.content).alias("content_element")
|
|
239
|
+
query = query.filter(
|
|
240
|
+
exists(
|
|
241
|
+
select(1)
|
|
242
|
+
.select_from(content_element)
|
|
243
|
+
.where(text("content_element->>'type' = 'text' AND content_element->>'text' ILIKE :query_text"))
|
|
244
|
+
.params(query_text=f"%{query_text}%")
|
|
245
|
+
)
|
|
246
|
+
)
|
|
239
247
|
|
|
240
248
|
# If role is provided, filter messages by role.
|
|
241
249
|
if role:
|
|
@@ -203,3 +203,18 @@ class PassageManager:
|
|
|
203
203
|
for passage in passages:
|
|
204
204
|
self.delete_passage_by_id(passage_id=passage.id, actor=actor)
|
|
205
205
|
return True
|
|
206
|
+
|
|
207
|
+
@enforce_types
|
|
208
|
+
def size(
|
|
209
|
+
self,
|
|
210
|
+
actor: PydanticUser,
|
|
211
|
+
agent_id: Optional[str] = None,
|
|
212
|
+
) -> int:
|
|
213
|
+
"""Get the total count of messages with optional filters.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
actor: The user requesting the count
|
|
217
|
+
agent_id: The agent ID of the messages
|
|
218
|
+
"""
|
|
219
|
+
with self.session_maker() as session:
|
|
220
|
+
return AgentPassage.size(db_session=session, actor=actor, agent_id=agent_id)
|
letta/settings.py
CHANGED
|
@@ -174,6 +174,11 @@ class Settings(BaseSettings):
|
|
|
174
174
|
# telemetry logging
|
|
175
175
|
verbose_telemetry_logging: bool = False
|
|
176
176
|
|
|
177
|
+
# uvicorn settings
|
|
178
|
+
uvicorn_workers: int = 1
|
|
179
|
+
uvicorn_reload: bool = False
|
|
180
|
+
uvicorn_timeout_keep_alive: int = 5
|
|
181
|
+
|
|
177
182
|
@property
|
|
178
183
|
def letta_pg_uri(self) -> str:
|
|
179
184
|
if self.pg_uri:
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from letta.agent import Agent, AgentState
|
|
4
|
+
from letta.constants import DEFAULT_MESSAGE_TOOL
|
|
5
|
+
from letta.functions.function_sets.multi_agent import send_message_to_all_agents_in_group
|
|
6
|
+
from letta.interface import AgentInterface
|
|
7
|
+
from letta.orm import User
|
|
8
|
+
from letta.orm.enums import ToolType
|
|
9
|
+
from letta.schemas.letta_message_content import TextContent
|
|
10
|
+
from letta.schemas.message import Message, MessageCreate
|
|
11
|
+
from letta.schemas.tool_rule import ChildToolRule, InitToolRule, TerminalToolRule
|
|
12
|
+
from letta.schemas.usage import LettaUsageStatistics
|
|
13
|
+
from letta.services.agent_manager import AgentManager
|
|
14
|
+
from letta.services.tool_manager import ToolManager
|
|
15
|
+
from tests.helpers.utils import create_tool_from_func
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SupervisorMultiAgent(Agent):
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
interface: AgentInterface,
|
|
22
|
+
agent_state: AgentState,
|
|
23
|
+
user: User = None,
|
|
24
|
+
# custom
|
|
25
|
+
group_id: str = "",
|
|
26
|
+
agent_ids: List[str] = [],
|
|
27
|
+
description: str = "",
|
|
28
|
+
):
|
|
29
|
+
super().__init__(interface, agent_state, user)
|
|
30
|
+
self.group_id = group_id
|
|
31
|
+
self.agent_ids = agent_ids
|
|
32
|
+
self.description = description
|
|
33
|
+
self.agent_manager = AgentManager()
|
|
34
|
+
self.tool_manager = ToolManager()
|
|
35
|
+
|
|
36
|
+
def step(
|
|
37
|
+
self,
|
|
38
|
+
messages: List[MessageCreate],
|
|
39
|
+
chaining: bool = True,
|
|
40
|
+
max_chaining_steps: Optional[int] = None,
|
|
41
|
+
put_inner_thoughts_first: bool = True,
|
|
42
|
+
assistant_message_tool_name: str = DEFAULT_MESSAGE_TOOL,
|
|
43
|
+
**kwargs,
|
|
44
|
+
) -> LettaUsageStatistics:
|
|
45
|
+
token_streaming = self.interface.streaming_mode if hasattr(self.interface, "streaming_mode") else False
|
|
46
|
+
metadata = self.interface.metadata if hasattr(self.interface, "metadata") else None
|
|
47
|
+
|
|
48
|
+
# add multi agent tool
|
|
49
|
+
if self.tool_manager.get_tool_by_name(tool_name="send_message_to_all_agents_in_group", actor=self.user) is None:
|
|
50
|
+
multi_agent_tool = create_tool_from_func(send_message_to_all_agents_in_group)
|
|
51
|
+
multi_agent_tool.tool_type = ToolType.LETTA_MULTI_AGENT_CORE
|
|
52
|
+
multi_agent_tool = self.tool_manager.create_or_update_tool(
|
|
53
|
+
pydantic_tool=multi_agent_tool,
|
|
54
|
+
actor=self.user,
|
|
55
|
+
)
|
|
56
|
+
self.agent_state = self.agent_manager.attach_tool(agent_id=self.agent_state.id, tool_id=multi_agent_tool.id, actor=self.user)
|
|
57
|
+
|
|
58
|
+
# override tool rules
|
|
59
|
+
self.agent_state.tool_rules = [
|
|
60
|
+
InitToolRule(
|
|
61
|
+
tool_name="send_message_to_all_agents_in_group",
|
|
62
|
+
),
|
|
63
|
+
TerminalToolRule(
|
|
64
|
+
tool_name=assistant_message_tool_name,
|
|
65
|
+
),
|
|
66
|
+
ChildToolRule(
|
|
67
|
+
tool_name="send_message_to_all_agents_in_group",
|
|
68
|
+
children=[assistant_message_tool_name],
|
|
69
|
+
),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
supervisor_messages = [
|
|
73
|
+
Message(
|
|
74
|
+
agent_id=self.agent_state.id,
|
|
75
|
+
role="user",
|
|
76
|
+
content=[TextContent(text=message.content)],
|
|
77
|
+
name=None,
|
|
78
|
+
model=None,
|
|
79
|
+
tool_calls=None,
|
|
80
|
+
tool_call_id=None,
|
|
81
|
+
group_id=self.group_id,
|
|
82
|
+
)
|
|
83
|
+
for message in messages
|
|
84
|
+
]
|
|
85
|
+
try:
|
|
86
|
+
supervisor_agent = Agent(agent_state=self.agent_state, interface=self.interface, user=self.user)
|
|
87
|
+
usage_stats = supervisor_agent.step(
|
|
88
|
+
messages=supervisor_messages,
|
|
89
|
+
chaining=chaining,
|
|
90
|
+
max_chaining_steps=max_chaining_steps,
|
|
91
|
+
stream=token_streaming,
|
|
92
|
+
skip_verify=True,
|
|
93
|
+
metadata=metadata,
|
|
94
|
+
put_inner_thoughts_first=put_inner_thoughts_first,
|
|
95
|
+
)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
raise e
|
|
98
|
+
finally:
|
|
99
|
+
self.interface.step_yield()
|
|
100
|
+
|
|
101
|
+
self.interface.step_complete()
|
|
102
|
+
|
|
103
|
+
return usage_stats
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: letta-nightly
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.40.dev20250314173529
|
|
4
4
|
Summary: Create LLM agents with long-term memory and custom tools
|
|
5
5
|
License: Apache License
|
|
6
6
|
Author: Letta Team
|
|
@@ -32,7 +32,6 @@ Requires-Dist: colorama (>=0.4.6,<0.5.0)
|
|
|
32
32
|
Requires-Dist: composio-core (>=0.7.7,<0.8.0)
|
|
33
33
|
Requires-Dist: composio-langchain (>=0.7.7,<0.8.0)
|
|
34
34
|
Requires-Dist: datamodel-code-generator[http] (>=0.25.0,<0.26.0) ; extra == "desktop" or extra == "all"
|
|
35
|
-
Requires-Dist: datasets (>=2.14.6,<3.0.0)
|
|
36
35
|
Requires-Dist: demjson3 (>=3.0.6,<4.0.0)
|
|
37
36
|
Requires-Dist: docker (>=7.1.0,<8.0.0) ; extra == "external-tools" or extra == "desktop" or extra == "all"
|
|
38
37
|
Requires-Dist: docstring-parser (>=0.16,<0.17)
|