letta-nightly 0.11.6.dev20250902104140__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.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/METADATA +11 -7
- {letta_nightly-0.11.6.dev20250902104140.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.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/licenses/LICENSE +0 -0
@@ -1,19 +1,24 @@
|
|
1
|
+
import uuid
|
1
2
|
from datetime import datetime, timezone
|
2
3
|
from functools import lru_cache
|
3
|
-
from typing import List, Optional
|
4
|
+
from typing import Dict, List, Optional
|
4
5
|
|
5
6
|
from openai import AsyncOpenAI, OpenAI
|
6
|
-
from sqlalchemy import select
|
7
|
+
from sqlalchemy import func, select
|
8
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
7
9
|
|
8
10
|
from letta.constants import MAX_EMBEDDING_DIM
|
9
11
|
from letta.embeddings import parse_and_chunk_text
|
10
12
|
from letta.helpers.decorators import async_redis_cache
|
11
13
|
from letta.llm_api.llm_client import LLMClient
|
14
|
+
from letta.log import get_logger
|
12
15
|
from letta.orm import ArchivesAgents
|
13
16
|
from letta.orm.errors import NoResultFound
|
14
17
|
from letta.orm.passage import ArchivalPassage, SourcePassage
|
18
|
+
from letta.orm.passage_tag import PassageTag
|
15
19
|
from letta.otel.tracing import trace_method
|
16
20
|
from letta.schemas.agent import AgentState
|
21
|
+
from letta.schemas.enums import VectorDBProvider
|
17
22
|
from letta.schemas.file import FileMetadata as PydanticFileMetadata
|
18
23
|
from letta.schemas.passage import Passage as PydanticPassage
|
19
24
|
from letta.schemas.user import User as PydanticUser
|
@@ -21,6 +26,8 @@ from letta.server.db import db_registry
|
|
21
26
|
from letta.services.archive_manager import ArchiveManager
|
22
27
|
from letta.utils import enforce_types
|
23
28
|
|
29
|
+
logger = get_logger(__name__)
|
30
|
+
|
24
31
|
|
25
32
|
# TODO: Add redis-backed caching for backend
|
26
33
|
@lru_cache(maxsize=8192)
|
@@ -47,6 +54,44 @@ class PassageManager:
|
|
47
54
|
def __init__(self):
|
48
55
|
self.archive_manager = ArchiveManager()
|
49
56
|
|
57
|
+
async def _create_tags_for_passage(
|
58
|
+
self,
|
59
|
+
session: AsyncSession,
|
60
|
+
passage_id: str,
|
61
|
+
archive_id: str,
|
62
|
+
organization_id: str,
|
63
|
+
tags: List[str],
|
64
|
+
actor: PydanticUser,
|
65
|
+
) -> List[PassageTag]:
|
66
|
+
"""Create tag entries in junction table (complements tags stored in JSON column).
|
67
|
+
|
68
|
+
Junction table enables efficient DISTINCT queries and tag-based filtering.
|
69
|
+
|
70
|
+
Note: Tags are already deduplicated before being passed to this method.
|
71
|
+
"""
|
72
|
+
if not tags:
|
73
|
+
return []
|
74
|
+
|
75
|
+
tag_objects = []
|
76
|
+
for tag in tags:
|
77
|
+
tag_obj = PassageTag(
|
78
|
+
id=f"passage-tag-{uuid.uuid4()}",
|
79
|
+
tag=tag,
|
80
|
+
passage_id=passage_id,
|
81
|
+
archive_id=archive_id,
|
82
|
+
organization_id=organization_id,
|
83
|
+
)
|
84
|
+
tag_objects.append(tag_obj)
|
85
|
+
|
86
|
+
# batch create all tags
|
87
|
+
created_tags = await PassageTag.batch_create_async(
|
88
|
+
items=tag_objects,
|
89
|
+
db_session=session,
|
90
|
+
actor=actor,
|
91
|
+
)
|
92
|
+
|
93
|
+
return created_tags
|
94
|
+
|
50
95
|
# AGENT PASSAGE METHODS
|
51
96
|
@enforce_types
|
52
97
|
@trace_method
|
@@ -154,6 +199,12 @@ class PassageManager:
|
|
154
199
|
raise ValueError("Agent passage cannot have source_id")
|
155
200
|
|
156
201
|
data = pydantic_passage.model_dump(to_orm=True)
|
202
|
+
|
203
|
+
# Deduplicate tags if provided (for dual storage consistency)
|
204
|
+
tags = data.get("tags")
|
205
|
+
if tags:
|
206
|
+
tags = list(set(tags))
|
207
|
+
|
157
208
|
common_fields = {
|
158
209
|
"id": data.get("id"),
|
159
210
|
"text": data["text"],
|
@@ -161,6 +212,7 @@ class PassageManager:
|
|
161
212
|
"embedding_config": data["embedding_config"],
|
162
213
|
"organization_id": data["organization_id"],
|
163
214
|
"metadata_": data.get("metadata", {}),
|
215
|
+
"tags": tags,
|
164
216
|
"is_deleted": data.get("is_deleted", False),
|
165
217
|
"created_at": data.get("created_at", datetime.now(timezone.utc)),
|
166
218
|
}
|
@@ -181,6 +233,12 @@ class PassageManager:
|
|
181
233
|
raise ValueError("Agent passage cannot have source_id")
|
182
234
|
|
183
235
|
data = pydantic_passage.model_dump(to_orm=True)
|
236
|
+
|
237
|
+
# Deduplicate tags if provided (for dual storage consistency)
|
238
|
+
tags = data.get("tags")
|
239
|
+
if tags:
|
240
|
+
tags = list(set(tags))
|
241
|
+
|
184
242
|
common_fields = {
|
185
243
|
"id": data.get("id"),
|
186
244
|
"text": data["text"],
|
@@ -188,6 +246,7 @@ class PassageManager:
|
|
188
246
|
"embedding_config": data["embedding_config"],
|
189
247
|
"organization_id": data["organization_id"],
|
190
248
|
"metadata_": data.get("metadata", {}),
|
249
|
+
"tags": tags,
|
191
250
|
"is_deleted": data.get("is_deleted", False),
|
192
251
|
"created_at": data.get("created_at", datetime.now(timezone.utc)),
|
193
252
|
}
|
@@ -196,6 +255,18 @@ class PassageManager:
|
|
196
255
|
|
197
256
|
async with db_registry.async_session() as session:
|
198
257
|
passage = await passage.create_async(session, actor=actor)
|
258
|
+
|
259
|
+
# dual storage: save tags to junction table for efficient queries
|
260
|
+
if tags: # use the deduplicated tags variable
|
261
|
+
await self._create_tags_for_passage(
|
262
|
+
session=session,
|
263
|
+
passage_id=passage.id,
|
264
|
+
archive_id=passage.archive_id,
|
265
|
+
organization_id=passage.organization_id,
|
266
|
+
tags=tags, # pass deduplicated tags
|
267
|
+
actor=actor,
|
268
|
+
)
|
269
|
+
|
199
270
|
return passage.to_pydantic()
|
200
271
|
|
201
272
|
@enforce_types
|
@@ -210,6 +281,12 @@ class PassageManager:
|
|
210
281
|
raise ValueError("Source passage cannot have archive_id")
|
211
282
|
|
212
283
|
data = pydantic_passage.model_dump(to_orm=True)
|
284
|
+
|
285
|
+
# Deduplicate tags if provided (for dual storage consistency)
|
286
|
+
tags = data.get("tags")
|
287
|
+
if tags:
|
288
|
+
tags = list(set(tags))
|
289
|
+
|
213
290
|
common_fields = {
|
214
291
|
"id": data.get("id"),
|
215
292
|
"text": data["text"],
|
@@ -217,6 +294,7 @@ class PassageManager:
|
|
217
294
|
"embedding_config": data["embedding_config"],
|
218
295
|
"organization_id": data["organization_id"],
|
219
296
|
"metadata_": data.get("metadata", {}),
|
297
|
+
"tags": tags,
|
220
298
|
"is_deleted": data.get("is_deleted", False),
|
221
299
|
"created_at": data.get("created_at", datetime.now(timezone.utc)),
|
222
300
|
}
|
@@ -243,6 +321,12 @@ class PassageManager:
|
|
243
321
|
raise ValueError("Source passage cannot have archive_id")
|
244
322
|
|
245
323
|
data = pydantic_passage.model_dump(to_orm=True)
|
324
|
+
|
325
|
+
# Deduplicate tags if provided (for dual storage consistency)
|
326
|
+
tags = data.get("tags")
|
327
|
+
if tags:
|
328
|
+
tags = list(set(tags))
|
329
|
+
|
246
330
|
common_fields = {
|
247
331
|
"id": data.get("id"),
|
248
332
|
"text": data["text"],
|
@@ -250,6 +334,7 @@ class PassageManager:
|
|
250
334
|
"embedding_config": data["embedding_config"],
|
251
335
|
"organization_id": data["organization_id"],
|
252
336
|
"metadata_": data.get("metadata", {}),
|
337
|
+
"tags": tags,
|
253
338
|
"is_deleted": data.get("is_deleted", False),
|
254
339
|
"created_at": data.get("created_at", datetime.now(timezone.utc)),
|
255
340
|
}
|
@@ -309,6 +394,7 @@ class PassageManager:
|
|
309
394
|
"embedding_config": data["embedding_config"],
|
310
395
|
"organization_id": data["organization_id"],
|
311
396
|
"metadata_": data.get("metadata", {}),
|
397
|
+
"tags": data.get("tags"),
|
312
398
|
"is_deleted": data.get("is_deleted", False),
|
313
399
|
"created_at": data.get("created_at", datetime.now(timezone.utc)),
|
314
400
|
}
|
@@ -356,6 +442,7 @@ class PassageManager:
|
|
356
442
|
"embedding_config": data["embedding_config"],
|
357
443
|
"organization_id": data["organization_id"],
|
358
444
|
"metadata_": data.get("metadata", {}),
|
445
|
+
"tags": data.get("tags"),
|
359
446
|
"is_deleted": data.get("is_deleted", False),
|
360
447
|
"created_at": data.get("created_at", datetime.now(timezone.utc)),
|
361
448
|
}
|
@@ -395,6 +482,7 @@ class PassageManager:
|
|
395
482
|
"embedding_config": data["embedding_config"],
|
396
483
|
"organization_id": data["organization_id"],
|
397
484
|
"metadata_": data.get("metadata", {}),
|
485
|
+
"tags": data.get("tags"),
|
398
486
|
"is_deleted": data.get("is_deleted", False),
|
399
487
|
"created_at": data.get("created_at", datetime.now(timezone.utc)),
|
400
488
|
}
|
@@ -465,8 +553,21 @@ class PassageManager:
|
|
465
553
|
agent_state: AgentState,
|
466
554
|
text: str,
|
467
555
|
actor: PydanticUser,
|
556
|
+
tags: Optional[List[str]] = None,
|
557
|
+
created_at: Optional[datetime] = None,
|
558
|
+
strict_mode: bool = False,
|
468
559
|
) -> List[PydanticPassage]:
|
469
|
-
"""Insert passage(s) into archival memory
|
560
|
+
"""Insert passage(s) into archival memory
|
561
|
+
|
562
|
+
Args:
|
563
|
+
agent_state: Agent state for embedding configuration
|
564
|
+
text: Text content to store as passages
|
565
|
+
actor: User performing the operation
|
566
|
+
tags: Optional list of tags to attach to all created passages
|
567
|
+
|
568
|
+
Returns:
|
569
|
+
List of created passage objects
|
570
|
+
"""
|
470
571
|
|
471
572
|
embedding_chunk_size = agent_state.embedding_config.embedding_chunk_size
|
472
573
|
embedding_client = LLMClient.create(
|
@@ -489,19 +590,53 @@ class PassageManager:
|
|
489
590
|
embeddings = await embedding_client.request_embeddings(text_chunks, agent_state.embedding_config)
|
490
591
|
|
491
592
|
passages = []
|
593
|
+
|
594
|
+
# Always write to SQL database first
|
492
595
|
for chunk_text, embedding in zip(text_chunks, embeddings):
|
596
|
+
passage_data = {
|
597
|
+
"organization_id": actor.organization_id,
|
598
|
+
"archive_id": archive.id,
|
599
|
+
"text": chunk_text,
|
600
|
+
"embedding": embedding,
|
601
|
+
"embedding_config": agent_state.embedding_config,
|
602
|
+
"tags": tags,
|
603
|
+
}
|
604
|
+
# only include created_at if provided
|
605
|
+
if created_at is not None:
|
606
|
+
passage_data["created_at"] = created_at
|
607
|
+
|
493
608
|
passage = await self.create_agent_passage_async(
|
494
|
-
PydanticPassage(
|
495
|
-
organization_id=actor.organization_id,
|
496
|
-
archive_id=archive.id,
|
497
|
-
text=chunk_text,
|
498
|
-
embedding=embedding,
|
499
|
-
embedding_config=agent_state.embedding_config,
|
500
|
-
),
|
609
|
+
PydanticPassage(**passage_data),
|
501
610
|
actor=actor,
|
502
611
|
)
|
503
612
|
passages.append(passage)
|
504
613
|
|
614
|
+
# If archive uses Turbopuffer, also write to Turbopuffer (dual-write)
|
615
|
+
if archive.vector_db_provider == VectorDBProvider.TPUF:
|
616
|
+
try:
|
617
|
+
from letta.helpers.tpuf_client import TurbopufferClient
|
618
|
+
|
619
|
+
tpuf_client = TurbopufferClient()
|
620
|
+
|
621
|
+
# Extract IDs and texts from the created passages
|
622
|
+
passage_ids = [p.id for p in passages]
|
623
|
+
passage_texts = [p.text for p in passages]
|
624
|
+
|
625
|
+
# Insert to Turbopuffer with the same IDs as SQL
|
626
|
+
await tpuf_client.insert_archival_memories(
|
627
|
+
archive_id=archive.id,
|
628
|
+
text_chunks=passage_texts,
|
629
|
+
embeddings=embeddings,
|
630
|
+
passage_ids=passage_ids, # Use same IDs as SQL
|
631
|
+
organization_id=actor.organization_id,
|
632
|
+
tags=tags,
|
633
|
+
created_at=passages[0].created_at if passages else None,
|
634
|
+
)
|
635
|
+
except Exception as e:
|
636
|
+
logger.error(f"Failed to insert passages to Turbopuffer: {e}")
|
637
|
+
if strict_mode:
|
638
|
+
raise # Re-raise the exception in strict mode
|
639
|
+
|
505
640
|
return passages
|
506
641
|
|
507
642
|
except Exception as e:
|
@@ -567,6 +702,34 @@ class PassageManager:
|
|
567
702
|
|
568
703
|
# Update the database record with values from the provided record
|
569
704
|
update_data = passage.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
|
705
|
+
|
706
|
+
# Handle tags update separately for junction table
|
707
|
+
new_tags = update_data.pop("tags", None)
|
708
|
+
if new_tags is not None:
|
709
|
+
# Deduplicate tags
|
710
|
+
if new_tags:
|
711
|
+
new_tags = list(set(new_tags))
|
712
|
+
|
713
|
+
# Delete existing tags from junction table
|
714
|
+
from sqlalchemy import delete
|
715
|
+
|
716
|
+
await session.execute(delete(PassageTag).where(PassageTag.passage_id == passage_id))
|
717
|
+
|
718
|
+
# Create new tags in junction table
|
719
|
+
if new_tags:
|
720
|
+
await self._create_tags_for_passage(
|
721
|
+
session=session,
|
722
|
+
passage_id=passage_id,
|
723
|
+
archive_id=curr_passage.archive_id,
|
724
|
+
organization_id=curr_passage.organization_id,
|
725
|
+
tags=new_tags,
|
726
|
+
actor=actor,
|
727
|
+
)
|
728
|
+
|
729
|
+
# Update the tags on the passage object
|
730
|
+
setattr(curr_passage, "tags", new_tags)
|
731
|
+
|
732
|
+
# Update other fields
|
570
733
|
for key, value in update_data.items():
|
571
734
|
setattr(curr_passage, key, value)
|
572
735
|
|
@@ -647,7 +810,7 @@ class PassageManager:
|
|
647
810
|
|
648
811
|
@enforce_types
|
649
812
|
@trace_method
|
650
|
-
async def delete_agent_passage_by_id_async(self, passage_id: str, actor: PydanticUser) -> bool:
|
813
|
+
async def delete_agent_passage_by_id_async(self, passage_id: str, actor: PydanticUser, strict_mode: bool = False) -> bool:
|
651
814
|
"""Delete an agent passage."""
|
652
815
|
if not passage_id:
|
653
816
|
raise ValueError("Passage ID must be provided.")
|
@@ -655,7 +818,25 @@ class PassageManager:
|
|
655
818
|
async with db_registry.async_session() as session:
|
656
819
|
try:
|
657
820
|
passage = await ArchivalPassage.read_async(db_session=session, identifier=passage_id, actor=actor)
|
821
|
+
archive_id = passage.archive_id
|
822
|
+
|
823
|
+
# Delete from SQL first
|
658
824
|
await passage.hard_delete_async(session, actor=actor)
|
825
|
+
|
826
|
+
# Check if archive uses Turbopuffer and dual-delete
|
827
|
+
if archive_id:
|
828
|
+
archive = await self.archive_manager.get_archive_by_id_async(archive_id=archive_id, actor=actor)
|
829
|
+
if archive.vector_db_provider == VectorDBProvider.TPUF:
|
830
|
+
try:
|
831
|
+
from letta.helpers.tpuf_client import TurbopufferClient
|
832
|
+
|
833
|
+
tpuf_client = TurbopufferClient()
|
834
|
+
await tpuf_client.delete_passage(archive_id=archive_id, passage_id=passage_id)
|
835
|
+
except Exception as e:
|
836
|
+
logger.error(f"Failed to delete passage from Turbopuffer: {e}")
|
837
|
+
if strict_mode:
|
838
|
+
raise # Re-raise the exception in strict mode
|
839
|
+
|
659
840
|
return True
|
660
841
|
except NoResultFound:
|
661
842
|
raise NoResultFound(f"Agent passage with id {passage_id} not found.")
|
@@ -812,12 +993,40 @@ class PassageManager:
|
|
812
993
|
@trace_method
|
813
994
|
async def delete_agent_passages_async(
|
814
995
|
self,
|
815
|
-
actor: PydanticUser,
|
816
996
|
passages: List[PydanticPassage],
|
997
|
+
actor: PydanticUser,
|
998
|
+
strict_mode: bool = False,
|
817
999
|
) -> bool:
|
818
1000
|
"""Delete multiple agent passages."""
|
1001
|
+
if not passages:
|
1002
|
+
return True
|
1003
|
+
|
819
1004
|
async with db_registry.async_session() as session:
|
1005
|
+
# Delete from SQL first
|
820
1006
|
await ArchivalPassage.bulk_hard_delete_async(db_session=session, identifiers=[p.id for p in passages], actor=actor)
|
1007
|
+
|
1008
|
+
# Group passages by archive_id for efficient Turbopuffer deletion
|
1009
|
+
passages_by_archive = {}
|
1010
|
+
for passage in passages:
|
1011
|
+
if passage.archive_id:
|
1012
|
+
if passage.archive_id not in passages_by_archive:
|
1013
|
+
passages_by_archive[passage.archive_id] = []
|
1014
|
+
passages_by_archive[passage.archive_id].append(passage.id)
|
1015
|
+
|
1016
|
+
# Check each archive and delete from Turbopuffer if needed
|
1017
|
+
for archive_id, passage_ids in passages_by_archive.items():
|
1018
|
+
archive = await self.archive_manager.get_archive_by_id_async(archive_id=archive_id, actor=actor)
|
1019
|
+
if archive.vector_db_provider == VectorDBProvider.TPUF:
|
1020
|
+
try:
|
1021
|
+
from letta.helpers.tpuf_client import TurbopufferClient
|
1022
|
+
|
1023
|
+
tpuf_client = TurbopufferClient()
|
1024
|
+
await tpuf_client.delete_passages(archive_id=archive_id, passage_ids=passage_ids)
|
1025
|
+
except Exception as e:
|
1026
|
+
logger.error(f"Failed to delete passages from Turbopuffer: {e}")
|
1027
|
+
if strict_mode:
|
1028
|
+
raise # Re-raise the exception in strict mode
|
1029
|
+
|
821
1030
|
return True
|
822
1031
|
|
823
1032
|
@enforce_types
|
@@ -1009,3 +1218,69 @@ class PassageManager:
|
|
1009
1218
|
)
|
1010
1219
|
passages = result.scalars().all()
|
1011
1220
|
return [p.to_pydantic() for p in passages]
|
1221
|
+
|
1222
|
+
@enforce_types
|
1223
|
+
@trace_method
|
1224
|
+
async def get_unique_tags_for_archive_async(
|
1225
|
+
self,
|
1226
|
+
archive_id: str,
|
1227
|
+
actor: PydanticUser,
|
1228
|
+
) -> List[str]:
|
1229
|
+
"""Get all unique tags for an archive.
|
1230
|
+
|
1231
|
+
Args:
|
1232
|
+
archive_id: ID of the archive
|
1233
|
+
actor: User performing the operation
|
1234
|
+
|
1235
|
+
Returns:
|
1236
|
+
List of unique tag values
|
1237
|
+
"""
|
1238
|
+
async with db_registry.async_session() as session:
|
1239
|
+
stmt = (
|
1240
|
+
select(PassageTag.tag)
|
1241
|
+
.distinct()
|
1242
|
+
.where(
|
1243
|
+
PassageTag.archive_id == archive_id,
|
1244
|
+
PassageTag.organization_id == actor.organization_id,
|
1245
|
+
PassageTag.is_deleted == False,
|
1246
|
+
)
|
1247
|
+
.order_by(PassageTag.tag)
|
1248
|
+
)
|
1249
|
+
|
1250
|
+
result = await session.execute(stmt)
|
1251
|
+
tags = result.scalars().all()
|
1252
|
+
|
1253
|
+
return list(tags)
|
1254
|
+
|
1255
|
+
@enforce_types
|
1256
|
+
@trace_method
|
1257
|
+
async def get_tag_counts_for_archive_async(
|
1258
|
+
self,
|
1259
|
+
archive_id: str,
|
1260
|
+
actor: PydanticUser,
|
1261
|
+
) -> Dict[str, int]:
|
1262
|
+
"""Get tag counts for an archive.
|
1263
|
+
|
1264
|
+
Args:
|
1265
|
+
archive_id: ID of the archive
|
1266
|
+
actor: User performing the operation
|
1267
|
+
|
1268
|
+
Returns:
|
1269
|
+
Dictionary mapping tag values to their counts
|
1270
|
+
"""
|
1271
|
+
async with db_registry.async_session() as session:
|
1272
|
+
stmt = (
|
1273
|
+
select(PassageTag.tag, func.count(PassageTag.id).label("count"))
|
1274
|
+
.where(
|
1275
|
+
PassageTag.archive_id == archive_id,
|
1276
|
+
PassageTag.organization_id == actor.organization_id,
|
1277
|
+
PassageTag.is_deleted == False,
|
1278
|
+
)
|
1279
|
+
.group_by(PassageTag.tag)
|
1280
|
+
.order_by(PassageTag.tag)
|
1281
|
+
)
|
1282
|
+
|
1283
|
+
result = await session.execute(stmt)
|
1284
|
+
rows = result.all()
|
1285
|
+
|
1286
|
+
return {row.tag: row.count for row in rows}
|
@@ -3,15 +3,13 @@ from typing import List, Optional, Tuple, Union
|
|
3
3
|
from letta.orm.provider import Provider as ProviderModel
|
4
4
|
from letta.otel.tracing import trace_method
|
5
5
|
from letta.schemas.enums import ProviderCategory, ProviderType
|
6
|
-
from letta.schemas.providers import Provider as PydanticProvider
|
7
|
-
from letta.schemas.providers import ProviderCheck, ProviderCreate, ProviderUpdate
|
6
|
+
from letta.schemas.providers import Provider as PydanticProvider, ProviderCheck, ProviderCreate, ProviderUpdate
|
8
7
|
from letta.schemas.user import User as PydanticUser
|
9
8
|
from letta.server.db import db_registry
|
10
9
|
from letta.utils import enforce_types
|
11
10
|
|
12
11
|
|
13
12
|
class ProviderManager:
|
14
|
-
|
15
13
|
@enforce_types
|
16
14
|
@trace_method
|
17
15
|
def create_provider(self, request: ProviderCreate, actor: PydanticUser) -> PydanticProvider:
|
@@ -3,15 +3,20 @@ from typing import Dict, List, Optional
|
|
3
3
|
from letta.constants import LETTA_TOOL_EXECUTION_DIR
|
4
4
|
from letta.log import get_logger
|
5
5
|
from letta.orm.errors import NoResultFound
|
6
|
-
from letta.orm.sandbox_config import SandboxConfig as SandboxConfigModel
|
7
|
-
from letta.orm.sandbox_config import SandboxEnvironmentVariable as SandboxEnvVarModel
|
6
|
+
from letta.orm.sandbox_config import SandboxConfig as SandboxConfigModel, SandboxEnvironmentVariable as SandboxEnvVarModel
|
8
7
|
from letta.otel.tracing import trace_method
|
9
8
|
from letta.schemas.enums import SandboxType
|
10
|
-
from letta.schemas.environment_variables import
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
from letta.schemas.environment_variables import (
|
10
|
+
SandboxEnvironmentVariable as PydanticEnvVar,
|
11
|
+
SandboxEnvironmentVariableCreate,
|
12
|
+
SandboxEnvironmentVariableUpdate,
|
13
|
+
)
|
14
|
+
from letta.schemas.sandbox_config import (
|
15
|
+
LocalSandboxConfig,
|
16
|
+
SandboxConfig as PydanticSandboxConfig,
|
17
|
+
SandboxConfigCreate,
|
18
|
+
SandboxConfigUpdate,
|
19
|
+
)
|
15
20
|
from letta.schemas.user import User as PydanticUser
|
16
21
|
from letta.server.db import db_registry
|
17
22
|
from letta.utils import enforce_types, printd
|
letta/services/source_manager.py
CHANGED
@@ -9,8 +9,7 @@ from letta.orm.source import Source as SourceModel
|
|
9
9
|
from letta.orm.sources_agents import SourcesAgents
|
10
10
|
from letta.otel.tracing import trace_method
|
11
11
|
from letta.schemas.agent import AgentState as PydanticAgentState
|
12
|
-
from letta.schemas.source import Source as PydanticSource
|
13
|
-
from letta.schemas.source import SourceUpdate
|
12
|
+
from letta.schemas.source import Source as PydanticSource, SourceUpdate
|
14
13
|
from letta.schemas.user import User as PydanticUser
|
15
14
|
from letta.server.db import db_registry
|
16
15
|
from letta.utils import enforce_types, printd
|
letta/services/step_manager.py
CHANGED
@@ -137,7 +137,7 @@ class Summarizer:
|
|
137
137
|
total_message_count = len(all_in_context_messages)
|
138
138
|
assert self.partial_evict_summarizer_percentage >= 0.0 and self.partial_evict_summarizer_percentage <= 1.0
|
139
139
|
target_message_start = round((1.0 - self.partial_evict_summarizer_percentage) * total_message_count)
|
140
|
-
logger.info(f"Target message count: {total_message_count}->{(total_message_count-target_message_start)}")
|
140
|
+
logger.info(f"Target message count: {total_message_count}->{(total_message_count - target_message_start)}")
|
141
141
|
|
142
142
|
# The summary message we'll insert is role 'user' (vs 'assistant', 'tool', or 'system')
|
143
143
|
# We are going to put it at index 1 (index 0 is the system message)
|
@@ -295,7 +295,9 @@ class Summarizer:
|
|
295
295
|
def simple_formatter(messages: List[Message], include_system: bool = False) -> str:
|
296
296
|
"""Go from an OpenAI-style list of messages to a concatenated string"""
|
297
297
|
|
298
|
-
parsed_messages =
|
298
|
+
parsed_messages = Message.to_openai_dicts_from_list(
|
299
|
+
[message for message in messages if message.role != MessageRole.system or include_system]
|
300
|
+
)
|
299
301
|
return "\n".join(json.dumps(msg) for msg in parsed_messages)
|
300
302
|
|
301
303
|
|
@@ -2,8 +2,7 @@ from letta.helpers.json_helpers import json_dumps, json_loads
|
|
2
2
|
from letta.helpers.singleton import singleton
|
3
3
|
from letta.orm.provider_trace import ProviderTrace as ProviderTraceModel
|
4
4
|
from letta.otel.tracing import trace_method
|
5
|
-
from letta.schemas.provider_trace import ProviderTrace as PydanticProviderTrace
|
6
|
-
from letta.schemas.provider_trace import ProviderTraceCreate
|
5
|
+
from letta.schemas.provider_trace import ProviderTrace as PydanticProviderTrace, ProviderTraceCreate
|
7
6
|
from letta.schemas.step import Step as PydanticStep
|
8
7
|
from letta.schemas.user import User as PydanticUser
|
9
8
|
from letta.server.db import db_registry
|
@@ -11,7 +10,6 @@ from letta.utils import enforce_types
|
|
11
10
|
|
12
11
|
|
13
12
|
class TelemetryManager:
|
14
|
-
|
15
13
|
@enforce_types
|
16
14
|
@trace_method
|
17
15
|
async def get_provider_trace_by_step_id_async(
|