letta-nightly 0.11.6.dev20250903104037__py3-none-any.whl → 0.11.7.dev20250904045700__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.
- letta/__init__.py +1 -1
- letta/agent.py +10 -14
- letta/agents/base_agent.py +18 -0
- letta/agents/helpers.py +32 -7
- letta/agents/letta_agent.py +953 -762
- letta/agents/voice_agent.py +1 -1
- letta/client/streaming.py +0 -1
- letta/constants.py +11 -8
- letta/errors.py +9 -0
- letta/functions/function_sets/base.py +77 -69
- letta/functions/function_sets/builtin.py +41 -22
- letta/functions/function_sets/multi_agent.py +1 -2
- letta/functions/schema_generator.py +0 -1
- letta/helpers/converters.py +8 -3
- letta/helpers/datetime_helpers.py +5 -4
- letta/helpers/message_helper.py +1 -2
- letta/helpers/pinecone_utils.py +0 -1
- letta/helpers/tool_rule_solver.py +10 -0
- letta/helpers/tpuf_client.py +848 -0
- letta/interface.py +8 -8
- letta/interfaces/anthropic_streaming_interface.py +7 -0
- letta/interfaces/openai_streaming_interface.py +29 -6
- letta/llm_api/anthropic_client.py +188 -18
- letta/llm_api/azure_client.py +0 -1
- letta/llm_api/bedrock_client.py +1 -2
- letta/llm_api/deepseek_client.py +319 -5
- letta/llm_api/google_vertex_client.py +75 -17
- letta/llm_api/groq_client.py +0 -1
- letta/llm_api/helpers.py +2 -2
- letta/llm_api/llm_api_tools.py +1 -50
- letta/llm_api/llm_client.py +6 -8
- letta/llm_api/mistral.py +1 -1
- letta/llm_api/openai.py +16 -13
- letta/llm_api/openai_client.py +31 -16
- letta/llm_api/together_client.py +0 -1
- letta/llm_api/xai_client.py +0 -1
- letta/local_llm/chat_completion_proxy.py +7 -6
- letta/local_llm/settings/settings.py +1 -1
- letta/orm/__init__.py +1 -0
- letta/orm/agent.py +8 -6
- letta/orm/archive.py +9 -1
- letta/orm/block.py +3 -4
- letta/orm/block_history.py +3 -1
- letta/orm/group.py +2 -3
- letta/orm/identity.py +1 -2
- letta/orm/job.py +1 -2
- letta/orm/llm_batch_items.py +1 -2
- letta/orm/message.py +8 -4
- letta/orm/mixins.py +18 -0
- letta/orm/organization.py +2 -0
- letta/orm/passage.py +8 -1
- letta/orm/passage_tag.py +55 -0
- letta/orm/sandbox_config.py +1 -3
- letta/orm/step.py +1 -2
- letta/orm/tool.py +1 -0
- letta/otel/resource.py +2 -2
- letta/plugins/plugins.py +1 -1
- letta/prompts/prompt_generator.py +10 -2
- letta/schemas/agent.py +11 -0
- letta/schemas/archive.py +4 -0
- letta/schemas/block.py +13 -0
- letta/schemas/embedding_config.py +0 -1
- letta/schemas/enums.py +24 -7
- letta/schemas/group.py +12 -0
- letta/schemas/letta_message.py +55 -1
- letta/schemas/letta_message_content.py +28 -0
- letta/schemas/letta_request.py +21 -4
- letta/schemas/letta_stop_reason.py +9 -1
- letta/schemas/llm_config.py +24 -8
- letta/schemas/mcp.py +0 -3
- letta/schemas/memory.py +14 -0
- letta/schemas/message.py +245 -141
- letta/schemas/openai/chat_completion_request.py +2 -1
- letta/schemas/passage.py +1 -0
- letta/schemas/providers/bedrock.py +1 -1
- letta/schemas/providers/openai.py +2 -2
- letta/schemas/tool.py +11 -5
- letta/schemas/tool_execution_result.py +0 -1
- letta/schemas/tool_rule.py +71 -0
- letta/serialize_schemas/marshmallow_agent.py +1 -2
- letta/server/rest_api/app.py +3 -3
- letta/server/rest_api/auth/index.py +0 -1
- letta/server/rest_api/interface.py +3 -11
- letta/server/rest_api/redis_stream_manager.py +3 -4
- letta/server/rest_api/routers/v1/agents.py +143 -84
- letta/server/rest_api/routers/v1/blocks.py +1 -1
- letta/server/rest_api/routers/v1/folders.py +1 -1
- letta/server/rest_api/routers/v1/groups.py +23 -22
- letta/server/rest_api/routers/v1/internal_templates.py +68 -0
- letta/server/rest_api/routers/v1/sandbox_configs.py +11 -5
- letta/server/rest_api/routers/v1/sources.py +1 -1
- letta/server/rest_api/routers/v1/tools.py +167 -15
- letta/server/rest_api/streaming_response.py +4 -3
- letta/server/rest_api/utils.py +75 -18
- letta/server/server.py +24 -35
- letta/services/agent_manager.py +359 -45
- letta/services/agent_serialization_manager.py +23 -3
- letta/services/archive_manager.py +72 -3
- letta/services/block_manager.py +1 -2
- letta/services/context_window_calculator/token_counter.py +11 -6
- letta/services/file_manager.py +1 -3
- letta/services/files_agents_manager.py +2 -4
- letta/services/group_manager.py +73 -12
- letta/services/helpers/agent_manager_helper.py +5 -5
- letta/services/identity_manager.py +8 -3
- letta/services/job_manager.py +2 -14
- letta/services/llm_batch_manager.py +1 -3
- letta/services/mcp/base_client.py +1 -2
- letta/services/mcp_manager.py +5 -6
- letta/services/message_manager.py +536 -15
- letta/services/organization_manager.py +1 -2
- letta/services/passage_manager.py +287 -12
- letta/services/provider_manager.py +1 -3
- letta/services/sandbox_config_manager.py +12 -7
- letta/services/source_manager.py +1 -2
- letta/services/step_manager.py +0 -1
- letta/services/summarizer/summarizer.py +4 -2
- letta/services/telemetry_manager.py +1 -3
- letta/services/tool_executor/builtin_tool_executor.py +136 -316
- letta/services/tool_executor/core_tool_executor.py +231 -74
- letta/services/tool_executor/files_tool_executor.py +2 -2
- letta/services/tool_executor/mcp_tool_executor.py +0 -1
- letta/services/tool_executor/multi_agent_tool_executor.py +2 -2
- letta/services/tool_executor/sandbox_tool_executor.py +0 -1
- letta/services/tool_executor/tool_execution_sandbox.py +2 -3
- letta/services/tool_manager.py +181 -64
- letta/services/tool_sandbox/modal_deployment_manager.py +2 -2
- letta/services/user_manager.py +1 -2
- letta/settings.py +5 -3
- letta/streaming_interface.py +3 -3
- letta/system.py +1 -1
- letta/utils.py +0 -1
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/METADATA +11 -7
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/RECORD +137 -135
- letta/llm_api/deepseek.py +0 -303
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/licenses/LICENSE +0 -0
@@ -28,6 +28,7 @@ from letta.schemas.agent_file import (
|
|
28
28
|
ToolSchema,
|
29
29
|
)
|
30
30
|
from letta.schemas.block import Block
|
31
|
+
from letta.schemas.embedding_config import EmbeddingConfig
|
31
32
|
from letta.schemas.enums import FileProcessingStatus
|
32
33
|
from letta.schemas.file import FileMetadata
|
33
34
|
from letta.schemas.group import Group, GroupCreate
|
@@ -432,6 +433,8 @@ class AgentSerializationManager:
|
|
432
433
|
override_existing_tools: bool = True,
|
433
434
|
dry_run: bool = False,
|
434
435
|
env_vars: Optional[Dict[str, Any]] = None,
|
436
|
+
override_embedding_config: Optional[EmbeddingConfig] = None,
|
437
|
+
project_id: Optional[str] = None,
|
435
438
|
) -> ImportResult:
|
436
439
|
"""
|
437
440
|
Import AgentFileSchema into the database.
|
@@ -530,6 +533,12 @@ class AgentSerializationManager:
|
|
530
533
|
source_names_to_check = [s.name for s in schema.sources]
|
531
534
|
existing_source_names = await self.source_manager.get_existing_source_names(source_names_to_check, actor)
|
532
535
|
|
536
|
+
# override embedding_config
|
537
|
+
if override_embedding_config:
|
538
|
+
for source_schema in schema.sources:
|
539
|
+
source_schema.embedding_config = override_embedding_config
|
540
|
+
source_schema.embedding = override_embedding_config.handle
|
541
|
+
|
533
542
|
for source_schema in schema.sources:
|
534
543
|
source_data = source_schema.model_dump(exclude={"id", "embedding", "embedding_chunk_size"})
|
535
544
|
|
@@ -577,10 +586,12 @@ class AgentSerializationManager:
|
|
577
586
|
# Start background tasks for file processing
|
578
587
|
background_tasks = []
|
579
588
|
if schema.files and any(f.content for f in schema.files):
|
589
|
+
# Use override embedding config if provided, otherwise use agent's config
|
590
|
+
embedder_config = override_embedding_config if override_embedding_config else schema.agents[0].embedding_config
|
580
591
|
if should_use_pinecone():
|
581
|
-
embedder = PineconeEmbedder(embedding_config=
|
592
|
+
embedder = PineconeEmbedder(embedding_config=embedder_config)
|
582
593
|
else:
|
583
|
-
embedder = OpenAIEmbedder(embedding_config=
|
594
|
+
embedder = OpenAIEmbedder(embedding_config=embedder_config)
|
584
595
|
file_processor = FileProcessor(
|
585
596
|
file_parser=self.file_parser,
|
586
597
|
embedder=embedder,
|
@@ -613,6 +624,11 @@ class AgentSerializationManager:
|
|
613
624
|
|
614
625
|
# 6. Create agents with empty message history
|
615
626
|
for agent_schema in schema.agents:
|
627
|
+
# Override embedding_config if provided
|
628
|
+
if override_embedding_config:
|
629
|
+
agent_schema.embedding_config = override_embedding_config
|
630
|
+
agent_schema.embedding = override_embedding_config.handle
|
631
|
+
|
616
632
|
# Convert AgentSchema back to CreateAgent, remapping tool/block IDs
|
617
633
|
agent_data = agent_schema.model_dump(exclude={"id", "in_context_message_ids", "messages"})
|
618
634
|
if append_copy_suffix:
|
@@ -634,6 +650,10 @@ class AgentSerializationManager:
|
|
634
650
|
for var in agent_data["tool_exec_environment_variables"]:
|
635
651
|
var["value"] = env_vars.get(var["key"], "")
|
636
652
|
|
653
|
+
# Override project_id if provided
|
654
|
+
if project_id:
|
655
|
+
agent_data["project_id"] = project_id
|
656
|
+
|
637
657
|
agent_create = CreateAgent(**agent_data)
|
638
658
|
created_agent = await self.agent_manager.create_agent_async(agent_create, actor, _init_with_no_messages=True)
|
639
659
|
file_to_db_ids[agent_schema.id] = created_agent.id
|
@@ -648,7 +668,7 @@ class AgentSerializationManager:
|
|
648
668
|
messages = []
|
649
669
|
for message_schema in agent_schema.messages:
|
650
670
|
# Convert MessageSchema back to Message, setting agent_id to new DB ID
|
651
|
-
message_data = message_schema.model_dump(exclude={"id"})
|
671
|
+
message_data = message_schema.model_dump(exclude={"id", "type"})
|
652
672
|
message_data["agent_id"] = agent_db_id # Remap agent_id to new database ID
|
653
673
|
message_obj = Message(**message_data)
|
654
674
|
messages.append(message_obj)
|
@@ -2,13 +2,14 @@ from typing import List, Optional
|
|
2
2
|
|
3
3
|
from sqlalchemy import select
|
4
4
|
|
5
|
+
from letta.helpers.tpuf_client import should_use_tpuf
|
5
6
|
from letta.log import get_logger
|
6
|
-
from letta.orm import ArchivalPassage
|
7
|
-
from letta.orm import Archive as ArchiveModel
|
8
|
-
from letta.orm import ArchivesAgents
|
7
|
+
from letta.orm import ArchivalPassage, Archive as ArchiveModel, ArchivesAgents
|
9
8
|
from letta.schemas.archive import Archive as PydanticArchive
|
9
|
+
from letta.schemas.enums import VectorDBProvider
|
10
10
|
from letta.schemas.user import User as PydanticUser
|
11
11
|
from letta.server.db import db_registry
|
12
|
+
from letta.settings import settings
|
12
13
|
from letta.utils import enforce_types
|
13
14
|
|
14
15
|
logger = get_logger(__name__)
|
@@ -27,10 +28,14 @@ class ArchiveManager:
|
|
27
28
|
"""Create a new archive."""
|
28
29
|
try:
|
29
30
|
with db_registry.session() as session:
|
31
|
+
# determine vector db provider based on settings
|
32
|
+
vector_db_provider = VectorDBProvider.TPUF if should_use_tpuf() else VectorDBProvider.NATIVE
|
33
|
+
|
30
34
|
archive = ArchiveModel(
|
31
35
|
name=name,
|
32
36
|
description=description,
|
33
37
|
organization_id=actor.organization_id,
|
38
|
+
vector_db_provider=vector_db_provider,
|
34
39
|
)
|
35
40
|
archive.create(session, actor=actor)
|
36
41
|
return archive.to_pydantic()
|
@@ -48,10 +53,14 @@ class ArchiveManager:
|
|
48
53
|
"""Create a new archive."""
|
49
54
|
try:
|
50
55
|
async with db_registry.async_session() as session:
|
56
|
+
# determine vector db provider based on settings
|
57
|
+
vector_db_provider = VectorDBProvider.TPUF if should_use_tpuf() else VectorDBProvider.NATIVE
|
58
|
+
|
51
59
|
archive = ArchiveModel(
|
52
60
|
name=name,
|
53
61
|
description=description,
|
54
62
|
organization_id=actor.organization_id,
|
63
|
+
vector_db_provider=vector_db_provider,
|
55
64
|
)
|
56
65
|
await archive.create_async(session, actor=actor)
|
57
66
|
return archive.to_pydantic()
|
@@ -138,6 +147,37 @@ class ArchiveManager:
|
|
138
147
|
session.add(archives_agents)
|
139
148
|
await session.commit()
|
140
149
|
|
150
|
+
@enforce_types
|
151
|
+
async def get_default_archive_for_agent_async(
|
152
|
+
self,
|
153
|
+
agent_id: str,
|
154
|
+
actor: PydanticUser = None,
|
155
|
+
) -> Optional[PydanticArchive]:
|
156
|
+
"""Get the agent's default archive if it exists, return None otherwise."""
|
157
|
+
# First check if agent has any archives
|
158
|
+
from letta.services.agent_manager import AgentManager
|
159
|
+
|
160
|
+
agent_manager = AgentManager()
|
161
|
+
|
162
|
+
archive_ids = await agent_manager.get_agent_archive_ids_async(
|
163
|
+
agent_id=agent_id,
|
164
|
+
actor=actor,
|
165
|
+
)
|
166
|
+
|
167
|
+
if archive_ids:
|
168
|
+
# TODO: Remove this check once we support multiple archives per agent
|
169
|
+
if len(archive_ids) > 1:
|
170
|
+
raise ValueError(f"Agent {agent_id} has multiple archives, which is not yet supported")
|
171
|
+
# Get the archive
|
172
|
+
archive = await self.get_archive_by_id_async(
|
173
|
+
archive_id=archive_ids[0],
|
174
|
+
actor=actor,
|
175
|
+
)
|
176
|
+
return archive
|
177
|
+
|
178
|
+
# No archive found, return None
|
179
|
+
return None
|
180
|
+
|
141
181
|
@enforce_types
|
142
182
|
async def get_or_create_default_archive_for_agent_async(
|
143
183
|
self,
|
@@ -267,3 +307,32 @@ class ArchiveManager:
|
|
267
307
|
|
268
308
|
# For now, return the first agent (backwards compatibility)
|
269
309
|
return agent_ids[0]
|
310
|
+
|
311
|
+
@enforce_types
|
312
|
+
async def get_or_set_vector_db_namespace_async(
|
313
|
+
self,
|
314
|
+
archive_id: str,
|
315
|
+
) -> str:
|
316
|
+
"""Get the vector database namespace for an archive, creating it if it doesn't exist."""
|
317
|
+
from sqlalchemy import update
|
318
|
+
|
319
|
+
async with db_registry.async_session() as session:
|
320
|
+
# check if namespace already exists
|
321
|
+
result = await session.execute(select(ArchiveModel._vector_db_namespace).where(ArchiveModel.id == archive_id))
|
322
|
+
row = result.fetchone()
|
323
|
+
|
324
|
+
if row and row[0]:
|
325
|
+
return row[0]
|
326
|
+
|
327
|
+
# generate namespace name using same logic as tpuf_client
|
328
|
+
environment = settings.environment
|
329
|
+
if environment:
|
330
|
+
namespace_name = f"archive_{archive_id}_{environment.lower()}"
|
331
|
+
else:
|
332
|
+
namespace_name = f"archive_{archive_id}"
|
333
|
+
|
334
|
+
# update the archive with the namespace
|
335
|
+
await session.execute(update(ArchiveModel).where(ArchiveModel.id == archive_id).values(_vector_db_namespace=namespace_name))
|
336
|
+
await session.commit()
|
337
|
+
|
338
|
+
return namespace_name
|
letta/services/block_manager.py
CHANGED
@@ -13,8 +13,7 @@ from letta.orm.blocks_agents import BlocksAgents
|
|
13
13
|
from letta.orm.errors import NoResultFound
|
14
14
|
from letta.otel.tracing import trace_method
|
15
15
|
from letta.schemas.agent import AgentState as PydanticAgentState
|
16
|
-
from letta.schemas.block import Block as PydanticBlock
|
17
|
-
from letta.schemas.block import BlockUpdate
|
16
|
+
from letta.schemas.block import Block as PydanticBlock, BlockUpdate
|
18
17
|
from letta.schemas.enums import ActorType
|
19
18
|
from letta.schemas.user import User as PydanticUser
|
20
19
|
from letta.server.db import db_registry
|
@@ -6,6 +6,7 @@ from typing import Any, Dict, List
|
|
6
6
|
from letta.helpers.decorators import async_redis_cache
|
7
7
|
from letta.llm_api.anthropic_client import AnthropicClient
|
8
8
|
from letta.otel.tracing import trace_method
|
9
|
+
from letta.schemas.message import Message
|
9
10
|
from letta.schemas.openai.chat_completion_request import Tool as OpenAITool
|
10
11
|
from letta.utils import count_tokens
|
11
12
|
|
@@ -50,7 +51,8 @@ class AnthropicTokenCounter(TokenCounter):
|
|
50
51
|
|
51
52
|
@trace_method
|
52
53
|
@async_redis_cache(
|
53
|
-
key_func=lambda self,
|
54
|
+
key_func=lambda self,
|
55
|
+
messages: f"anthropic_message_tokens:{self.model}:{hashlib.sha256(json.dumps(messages, sort_keys=True).encode()).hexdigest()[:16]}",
|
54
56
|
prefix="token_counter",
|
55
57
|
ttl_s=3600, # cache for 1 hour
|
56
58
|
)
|
@@ -61,7 +63,8 @@ class AnthropicTokenCounter(TokenCounter):
|
|
61
63
|
|
62
64
|
@trace_method
|
63
65
|
@async_redis_cache(
|
64
|
-
key_func=lambda self,
|
66
|
+
key_func=lambda self,
|
67
|
+
tools: f"anthropic_tool_tokens:{self.model}:{hashlib.sha256(json.dumps([t.model_dump() for t in tools], sort_keys=True).encode()).hexdigest()[:16]}",
|
65
68
|
prefix="token_counter",
|
66
69
|
ttl_s=3600, # cache for 1 hour
|
67
70
|
)
|
@@ -71,7 +74,7 @@ class AnthropicTokenCounter(TokenCounter):
|
|
71
74
|
return await self.client.count_tokens(model=self.model, tools=tools)
|
72
75
|
|
73
76
|
def convert_messages(self, messages: List[Any]) -> List[Dict[str, Any]]:
|
74
|
-
return
|
77
|
+
return Message.to_anthropic_dicts_from_list(messages)
|
75
78
|
|
76
79
|
|
77
80
|
class TiktokenCounter(TokenCounter):
|
@@ -93,7 +96,8 @@ class TiktokenCounter(TokenCounter):
|
|
93
96
|
|
94
97
|
@trace_method
|
95
98
|
@async_redis_cache(
|
96
|
-
key_func=lambda self,
|
99
|
+
key_func=lambda self,
|
100
|
+
messages: f"tiktoken_message_tokens:{self.model}:{hashlib.sha256(json.dumps(messages, sort_keys=True).encode()).hexdigest()[:16]}",
|
97
101
|
prefix="token_counter",
|
98
102
|
ttl_s=3600, # cache for 1 hour
|
99
103
|
)
|
@@ -106,7 +110,8 @@ class TiktokenCounter(TokenCounter):
|
|
106
110
|
|
107
111
|
@trace_method
|
108
112
|
@async_redis_cache(
|
109
|
-
key_func=lambda self,
|
113
|
+
key_func=lambda self,
|
114
|
+
tools: f"tiktoken_tool_tokens:{self.model}:{hashlib.sha256(json.dumps([t.model_dump() for t in tools], sort_keys=True).encode()).hexdigest()[:16]}",
|
110
115
|
prefix="token_counter",
|
111
116
|
ttl_s=3600, # cache for 1 hour
|
112
117
|
)
|
@@ -120,4 +125,4 @@ class TiktokenCounter(TokenCounter):
|
|
120
125
|
return num_tokens_from_functions(functions=functions, model=self.model)
|
121
126
|
|
122
127
|
def convert_messages(self, messages: List[Any]) -> List[Dict[str, Any]]:
|
123
|
-
return
|
128
|
+
return Message.to_openai_dicts_from_list(messages)
|
letta/services/file_manager.py
CHANGED
@@ -12,8 +12,7 @@ from letta.constants import MAX_FILENAME_LENGTH
|
|
12
12
|
from letta.helpers.pinecone_utils import list_pinecone_index_for_files, should_use_pinecone
|
13
13
|
from letta.log import get_logger
|
14
14
|
from letta.orm.errors import NoResultFound
|
15
|
-
from letta.orm.file import FileContent as FileContentModel
|
16
|
-
from letta.orm.file import FileMetadata as FileMetadataModel
|
15
|
+
from letta.orm.file import FileContent as FileContentModel, FileMetadata as FileMetadataModel
|
17
16
|
from letta.orm.sqlalchemy_base import AccessType
|
18
17
|
from letta.otel.tracing import trace_method
|
19
18
|
from letta.schemas.enums import FileProcessingStatus
|
@@ -60,7 +59,6 @@ class FileManager:
|
|
60
59
|
*,
|
61
60
|
text: Optional[str] = None,
|
62
61
|
) -> PydanticFileMetadata:
|
63
|
-
|
64
62
|
# short-circuit if it already exists
|
65
63
|
existing = await self.get_file_by_id(file_metadata.id, actor=actor)
|
66
64
|
if existing:
|
@@ -7,10 +7,8 @@ from letta.log import get_logger
|
|
7
7
|
from letta.orm.errors import NoResultFound
|
8
8
|
from letta.orm.files_agents import FileAgent as FileAgentModel
|
9
9
|
from letta.otel.tracing import trace_method
|
10
|
-
from letta.schemas.block import Block as PydanticBlock
|
11
|
-
from letta.schemas.
|
12
|
-
from letta.schemas.file import FileAgent as PydanticFileAgent
|
13
|
-
from letta.schemas.file import FileMetadata
|
10
|
+
from letta.schemas.block import Block as PydanticBlock, FileBlock as PydanticFileBlock
|
11
|
+
from letta.schemas.file import FileAgent as PydanticFileAgent, FileMetadata
|
14
12
|
from letta.schemas.user import User as PydanticUser
|
15
13
|
from letta.server.db import db_registry
|
16
14
|
from letta.utils import enforce_types
|
letta/services/group_manager.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
from typing import List, Optional
|
1
|
+
from typing import List, Optional, Union
|
2
2
|
|
3
|
-
from sqlalchemy import select
|
3
|
+
from sqlalchemy import delete, select
|
4
4
|
from sqlalchemy.orm import Session
|
5
5
|
|
6
6
|
from letta.orm.agent import Agent as AgentModel
|
@@ -8,8 +8,7 @@ from letta.orm.errors import NoResultFound
|
|
8
8
|
from letta.orm.group import Group as GroupModel
|
9
9
|
from letta.orm.message import Message as MessageModel
|
10
10
|
from letta.otel.tracing import trace_method
|
11
|
-
from letta.schemas.group import Group as PydanticGroup
|
12
|
-
from letta.schemas.group import GroupCreate, GroupUpdate, ManagerType
|
11
|
+
from letta.schemas.group import Group as PydanticGroup, GroupCreate, GroupUpdate, InternalTemplateGroupCreate, ManagerType
|
13
12
|
from letta.schemas.letta_message import LettaMessage
|
14
13
|
from letta.schemas.message import Message as PydanticMessage
|
15
14
|
from letta.schemas.user import User as PydanticUser
|
@@ -20,7 +19,7 @@ from letta.utils import enforce_types
|
|
20
19
|
class GroupManager:
|
21
20
|
@enforce_types
|
22
21
|
@trace_method
|
23
|
-
def
|
22
|
+
async def list_groups_async(
|
24
23
|
self,
|
25
24
|
actor: PydanticUser,
|
26
25
|
project_id: Optional[str] = None,
|
@@ -29,13 +28,13 @@ class GroupManager:
|
|
29
28
|
after: Optional[str] = None,
|
30
29
|
limit: Optional[int] = 50,
|
31
30
|
) -> list[PydanticGroup]:
|
32
|
-
with db_registry.
|
31
|
+
async with db_registry.async_session() as session:
|
33
32
|
filters = {"organization_id": actor.organization_id}
|
34
33
|
if project_id:
|
35
34
|
filters["project_id"] = project_id
|
36
35
|
if manager_type:
|
37
36
|
filters["manager_type"] = manager_type
|
38
|
-
groups = GroupModel.
|
37
|
+
groups = await GroupModel.list_async(
|
39
38
|
db_session=session,
|
40
39
|
before=before,
|
41
40
|
after=after,
|
@@ -60,7 +59,7 @@ class GroupManager:
|
|
60
59
|
|
61
60
|
@enforce_types
|
62
61
|
@trace_method
|
63
|
-
def create_group(self, group: GroupCreate, actor: PydanticUser) -> PydanticGroup:
|
62
|
+
def create_group(self, group: Union[GroupCreate, InternalTemplateGroupCreate], actor: PydanticUser) -> PydanticGroup:
|
64
63
|
with db_registry.session() as session:
|
65
64
|
new_group = GroupModel()
|
66
65
|
new_group.organization_id = actor.organization_id
|
@@ -96,6 +95,11 @@ class GroupManager:
|
|
96
95
|
case _:
|
97
96
|
raise ValueError(f"Unsupported manager type: {group.manager_config.manager_type}")
|
98
97
|
|
98
|
+
if isinstance(group, InternalTemplateGroupCreate):
|
99
|
+
new_group.base_template_id = group.base_template_id
|
100
|
+
new_group.template_id = group.template_id
|
101
|
+
new_group.deployment_id = group.deployment_id
|
102
|
+
|
99
103
|
self._process_agent_relationship(session=session, group=new_group, agent_ids=group.agent_ids, allow_partial=False)
|
100
104
|
|
101
105
|
if group.shared_block_ids:
|
@@ -105,7 +109,7 @@ class GroupManager:
|
|
105
109
|
return new_group.to_pydantic()
|
106
110
|
|
107
111
|
@enforce_types
|
108
|
-
async def create_group_async(self, group: GroupCreate, actor: PydanticUser) -> PydanticGroup:
|
112
|
+
async def create_group_async(self, group: Union[GroupCreate, InternalTemplateGroupCreate], actor: PydanticUser) -> PydanticGroup:
|
109
113
|
async with db_registry.async_session() as session:
|
110
114
|
new_group = GroupModel()
|
111
115
|
new_group.organization_id = actor.organization_id
|
@@ -141,6 +145,11 @@ class GroupManager:
|
|
141
145
|
case _:
|
142
146
|
raise ValueError(f"Unsupported manager type: {group.manager_config.manager_type}")
|
143
147
|
|
148
|
+
if isinstance(group, InternalTemplateGroupCreate):
|
149
|
+
new_group.base_template_id = group.base_template_id
|
150
|
+
new_group.template_id = group.template_id
|
151
|
+
new_group.deployment_id = group.deployment_id
|
152
|
+
|
144
153
|
await self._process_agent_relationship_async(session=session, group=new_group, agent_ids=group.agent_ids, allow_partial=False)
|
145
154
|
|
146
155
|
if group.shared_block_ids:
|
@@ -264,6 +273,43 @@ class GroupManager:
|
|
264
273
|
|
265
274
|
return messages
|
266
275
|
|
276
|
+
@enforce_types
|
277
|
+
@trace_method
|
278
|
+
async def list_group_messages_async(
|
279
|
+
self,
|
280
|
+
actor: PydanticUser,
|
281
|
+
group_id: Optional[str] = None,
|
282
|
+
before: Optional[str] = None,
|
283
|
+
after: Optional[str] = None,
|
284
|
+
limit: Optional[int] = 50,
|
285
|
+
use_assistant_message: bool = True,
|
286
|
+
assistant_message_tool_name: str = "send_message",
|
287
|
+
assistant_message_tool_kwarg: str = "message",
|
288
|
+
) -> list[LettaMessage]:
|
289
|
+
async with db_registry.async_session() as session:
|
290
|
+
filters = {
|
291
|
+
"organization_id": actor.organization_id,
|
292
|
+
"group_id": group_id,
|
293
|
+
}
|
294
|
+
messages = await MessageModel.list_async(
|
295
|
+
db_session=session,
|
296
|
+
before=before,
|
297
|
+
after=after,
|
298
|
+
limit=limit,
|
299
|
+
**filters,
|
300
|
+
)
|
301
|
+
|
302
|
+
messages = PydanticMessage.to_letta_messages_from_list(
|
303
|
+
messages=[msg.to_pydantic() for msg in messages],
|
304
|
+
use_assistant_message=use_assistant_message,
|
305
|
+
assistant_message_tool_name=assistant_message_tool_name,
|
306
|
+
assistant_message_tool_kwarg=assistant_message_tool_kwarg,
|
307
|
+
)
|
308
|
+
|
309
|
+
# TODO: filter messages to return a clean conversation history
|
310
|
+
|
311
|
+
return messages
|
312
|
+
|
267
313
|
@enforce_types
|
268
314
|
@trace_method
|
269
315
|
def reset_messages(self, group_id: str, actor: PydanticUser) -> None:
|
@@ -278,6 +324,21 @@ class GroupManager:
|
|
278
324
|
|
279
325
|
session.commit()
|
280
326
|
|
327
|
+
@enforce_types
|
328
|
+
@trace_method
|
329
|
+
async def reset_messages_async(self, group_id: str, actor: PydanticUser) -> None:
|
330
|
+
async with db_registry.async_session() as session:
|
331
|
+
# Ensure group is loadable by user
|
332
|
+
group = await GroupModel.read_async(db_session=session, identifier=group_id, actor=actor)
|
333
|
+
|
334
|
+
# Delete all messages in the group
|
335
|
+
delete_stmt = delete(MessageModel).where(
|
336
|
+
MessageModel.organization_id == actor.organization_id, MessageModel.group_id == group_id
|
337
|
+
)
|
338
|
+
await session.execute(delete_stmt)
|
339
|
+
|
340
|
+
await session.commit()
|
341
|
+
|
281
342
|
@enforce_types
|
282
343
|
@trace_method
|
283
344
|
def bump_turns_counter(self, group_id: str, actor: PydanticUser) -> int:
|
@@ -332,15 +393,15 @@ class GroupManager:
|
|
332
393
|
return prev_last_processed_message_id
|
333
394
|
|
334
395
|
@enforce_types
|
335
|
-
def size(
|
396
|
+
async def size(
|
336
397
|
self,
|
337
398
|
actor: PydanticUser,
|
338
399
|
) -> int:
|
339
400
|
"""
|
340
401
|
Get the total count of groups for the given user.
|
341
402
|
"""
|
342
|
-
with db_registry.
|
343
|
-
return GroupModel.
|
403
|
+
async with db_registry.async_session() as session:
|
404
|
+
return await GroupModel.size_async(db_session=session, actor=actor)
|
344
405
|
|
345
406
|
def _process_agent_relationship(self, session: Session, group: GroupModel, agent_ids: List[str], allow_partial=False, replace=True):
|
346
407
|
if not agent_ids:
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import os
|
2
1
|
import uuid
|
3
2
|
from datetime import datetime
|
4
3
|
from typing import List, Literal, Optional, Set
|
@@ -465,7 +464,6 @@ def package_initial_message_sequence(
|
|
465
464
|
# create the agent object
|
466
465
|
init_messages = []
|
467
466
|
for message_create in initial_message_sequence:
|
468
|
-
|
469
467
|
if message_create.role == MessageRole.user:
|
470
468
|
packed_message = system.package_user_message(
|
471
469
|
user_message=message_create.content,
|
@@ -499,8 +497,10 @@ def package_initial_message_sequence(
|
|
499
497
|
import json
|
500
498
|
import uuid
|
501
499
|
|
502
|
-
from openai.types.chat.chat_completion_message_tool_call import
|
503
|
-
|
500
|
+
from openai.types.chat.chat_completion_message_tool_call import (
|
501
|
+
ChatCompletionMessageToolCall as OpenAIToolCall,
|
502
|
+
Function as OpenAIFunction,
|
503
|
+
)
|
504
504
|
|
505
505
|
from letta.constants import DEFAULT_MESSAGE_TOOL
|
506
506
|
|
@@ -1208,7 +1208,7 @@ def calculate_base_tools(is_v2: bool) -> Set[str]:
|
|
1208
1208
|
|
1209
1209
|
def calculate_multi_agent_tools() -> Set[str]:
|
1210
1210
|
"""Calculate multi-agent tools, excluding local-only tools in production environment."""
|
1211
|
-
if
|
1211
|
+
if settings.environment == "PRODUCTION":
|
1212
1212
|
return set(MULTI_AGENT_TOOLS) - set(LOCAL_ONLY_MULTI_AGENT_TOOLS)
|
1213
1213
|
else:
|
1214
1214
|
return set(MULTI_AGENT_TOOLS)
|
@@ -9,8 +9,14 @@ from letta.orm.block import Block as BlockModel
|
|
9
9
|
from letta.orm.errors import UniqueConstraintViolationError
|
10
10
|
from letta.orm.identity import Identity as IdentityModel
|
11
11
|
from letta.otel.tracing import trace_method
|
12
|
-
from letta.schemas.identity import
|
13
|
-
|
12
|
+
from letta.schemas.identity import (
|
13
|
+
Identity as PydanticIdentity,
|
14
|
+
IdentityCreate,
|
15
|
+
IdentityProperty,
|
16
|
+
IdentityType,
|
17
|
+
IdentityUpdate,
|
18
|
+
IdentityUpsert,
|
19
|
+
)
|
14
20
|
from letta.schemas.user import User as PydanticUser
|
15
21
|
from letta.server.db import db_registry
|
16
22
|
from letta.settings import DatabaseChoice, settings
|
@@ -18,7 +24,6 @@ from letta.utils import enforce_types
|
|
18
24
|
|
19
25
|
|
20
26
|
class IdentityManager:
|
21
|
-
|
22
27
|
@enforce_types
|
23
28
|
@trace_method
|
24
29
|
async def list_identities_async(
|
letta/services/job_manager.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
from datetime import datetime
|
2
1
|
from functools import partial, reduce
|
3
2
|
from operator import add
|
4
3
|
from typing import List, Literal, Optional, Union
|
@@ -14,13 +13,10 @@ from letta.orm.job import Job as JobModel
|
|
14
13
|
from letta.orm.job_messages import JobMessage
|
15
14
|
from letta.orm.message import Message as MessageModel
|
16
15
|
from letta.orm.sqlalchemy_base import AccessType
|
17
|
-
from letta.orm.step import Step
|
18
|
-
from letta.orm.step import Step as StepModel
|
16
|
+
from letta.orm.step import Step, Step as StepModel
|
19
17
|
from letta.otel.tracing import log_event, trace_method
|
20
18
|
from letta.schemas.enums import JobStatus, JobType, MessageRole
|
21
|
-
from letta.schemas.job import BatchJob as PydanticBatchJob
|
22
|
-
from letta.schemas.job import Job as PydanticJob
|
23
|
-
from letta.schemas.job import JobUpdate, LettaRequestConfig
|
19
|
+
from letta.schemas.job import BatchJob as PydanticBatchJob, Job as PydanticJob, JobUpdate, LettaRequestConfig
|
24
20
|
from letta.schemas.letta_message import LettaMessage
|
25
21
|
from letta.schemas.message import Message as PydanticMessage
|
26
22
|
from letta.schemas.run import Run as PydanticRun
|
@@ -28,7 +24,6 @@ from letta.schemas.step import Step as PydanticStep
|
|
28
24
|
from letta.schemas.usage import LettaUsageStatistics
|
29
25
|
from letta.schemas.user import User as PydanticUser
|
30
26
|
from letta.server.db import db_registry
|
31
|
-
from letta.settings import DatabaseChoice, settings
|
32
27
|
from letta.utils import enforce_types
|
33
28
|
|
34
29
|
logger = get_logger(__name__)
|
@@ -337,11 +332,7 @@ class JobManager:
|
|
337
332
|
conditions = []
|
338
333
|
if before_obj:
|
339
334
|
# records before this cursor (older)
|
340
|
-
|
341
335
|
before_timestamp = before_obj.created_at
|
342
|
-
# SQLite does not support as granular timestamping, so we need to round the timestamp
|
343
|
-
if settings.database_engine is DatabaseChoice.SQLITE and isinstance(before_timestamp, datetime):
|
344
|
-
before_timestamp = before_timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
345
336
|
|
346
337
|
conditions.append(
|
347
338
|
or_(
|
@@ -353,9 +344,6 @@ class JobManager:
|
|
353
344
|
if after_obj:
|
354
345
|
# records after this cursor (newer)
|
355
346
|
after_timestamp = after_obj.created_at
|
356
|
-
# SQLite does not support as granular timestamping, so we need to round the timestamp
|
357
|
-
if settings.database_engine is DatabaseChoice.SQLITE and isinstance(after_timestamp, datetime):
|
358
|
-
after_timestamp = after_timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
359
347
|
|
360
348
|
conditions.append(
|
361
349
|
or_(JobModel.created_at > after_timestamp, and_(JobModel.created_at == after_timestamp, JobModel.id > after_obj.id))
|
@@ -11,9 +11,7 @@ from letta.orm.llm_batch_items import LLMBatchItem
|
|
11
11
|
from letta.orm.llm_batch_job import LLMBatchJob
|
12
12
|
from letta.otel.tracing import trace_method
|
13
13
|
from letta.schemas.enums import AgentStepStatus, JobStatus, ProviderType
|
14
|
-
from letta.schemas.llm_batch_job import AgentStepState
|
15
|
-
from letta.schemas.llm_batch_job import LLMBatchItem as PydanticLLMBatchItem
|
16
|
-
from letta.schemas.llm_batch_job import LLMBatchJob as PydanticLLMBatchJob
|
14
|
+
from letta.schemas.llm_batch_job import AgentStepState, LLMBatchItem as PydanticLLMBatchItem, LLMBatchJob as PydanticLLMBatchJob
|
17
15
|
from letta.schemas.llm_config import LLMConfig
|
18
16
|
from letta.schemas.message import Message as PydanticMessage
|
19
17
|
from letta.schemas.user import User as PydanticUser
|
@@ -1,8 +1,7 @@
|
|
1
1
|
from contextlib import AsyncExitStack
|
2
2
|
from typing import Optional, Tuple
|
3
3
|
|
4
|
-
from mcp import ClientSession
|
5
|
-
from mcp import Tool as MCPTool
|
4
|
+
from mcp import ClientSession, Tool as MCPTool
|
6
5
|
from mcp.client.auth import OAuthClientProvider
|
7
6
|
from mcp.types import TextContent
|
8
7
|
|
letta/services/mcp_manager.py
CHANGED
@@ -33,8 +33,7 @@ from letta.schemas.mcp import (
|
|
33
33
|
UpdateStdioMCPServer,
|
34
34
|
UpdateStreamableHTTPMCPServer,
|
35
35
|
)
|
36
|
-
from letta.schemas.tool import Tool as PydanticTool
|
37
|
-
from letta.schemas.tool import ToolCreate
|
36
|
+
from letta.schemas.tool import Tool as PydanticTool, ToolCreate
|
38
37
|
from letta.schemas.user import User as PydanticUser
|
39
38
|
from letta.server.db import db_registry
|
40
39
|
from letta.services.mcp.sse_client import MCP_CONFIG_TOPLEVEL_KEY, AsyncSSEMCPClient
|
@@ -137,8 +136,7 @@ class MCPManager:
|
|
137
136
|
if mcp_tool.health:
|
138
137
|
if mcp_tool.health.status == "INVALID":
|
139
138
|
raise ValueError(
|
140
|
-
f"Tool {mcp_tool_name} cannot be attached, JSON schema is invalid."
|
141
|
-
f"Reasons: {', '.join(mcp_tool.health.reasons)}"
|
139
|
+
f"Tool {mcp_tool_name} cannot be attached, JSON schema is invalid.Reasons: {', '.join(mcp_tool.health.reasons)}"
|
142
140
|
)
|
143
141
|
|
144
142
|
tool_create = ToolCreate.from_mcp(mcp_server_name=mcp_server_name, mcp_tool=mcp_tool)
|
@@ -305,7 +303,9 @@ class MCPManager:
|
|
305
303
|
|
306
304
|
async with db_registry.async_session() as session:
|
307
305
|
mcp_servers = await MCPServerModel.list_async(
|
308
|
-
db_session=session,
|
306
|
+
db_session=session,
|
307
|
+
organization_id=actor.organization_id,
|
308
|
+
id=mcp_server_ids, # This will use the IN operator
|
309
309
|
)
|
310
310
|
return [mcp_server.to_pydantic() for mcp_server in mcp_servers]
|
311
311
|
|
@@ -407,7 +407,6 @@ class MCPManager:
|
|
407
407
|
# with the value being the schema from StdioServerParameters
|
408
408
|
if MCP_CONFIG_TOPLEVEL_KEY in mcp_config:
|
409
409
|
for server_name, server_params_raw in mcp_config[MCP_CONFIG_TOPLEVEL_KEY].items():
|
410
|
-
|
411
410
|
# No support for duplicate server names
|
412
411
|
if server_name in mcp_server_list:
|
413
412
|
# Duplicate server names are configuration issues, not system errors
|