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.
Files changed (138) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +10 -14
  3. letta/agents/base_agent.py +18 -0
  4. letta/agents/helpers.py +32 -7
  5. letta/agents/letta_agent.py +953 -762
  6. letta/agents/voice_agent.py +1 -1
  7. letta/client/streaming.py +0 -1
  8. letta/constants.py +11 -8
  9. letta/errors.py +9 -0
  10. letta/functions/function_sets/base.py +77 -69
  11. letta/functions/function_sets/builtin.py +41 -22
  12. letta/functions/function_sets/multi_agent.py +1 -2
  13. letta/functions/schema_generator.py +0 -1
  14. letta/helpers/converters.py +8 -3
  15. letta/helpers/datetime_helpers.py +5 -4
  16. letta/helpers/message_helper.py +1 -2
  17. letta/helpers/pinecone_utils.py +0 -1
  18. letta/helpers/tool_rule_solver.py +10 -0
  19. letta/helpers/tpuf_client.py +848 -0
  20. letta/interface.py +8 -8
  21. letta/interfaces/anthropic_streaming_interface.py +7 -0
  22. letta/interfaces/openai_streaming_interface.py +29 -6
  23. letta/llm_api/anthropic_client.py +188 -18
  24. letta/llm_api/azure_client.py +0 -1
  25. letta/llm_api/bedrock_client.py +1 -2
  26. letta/llm_api/deepseek_client.py +319 -5
  27. letta/llm_api/google_vertex_client.py +75 -17
  28. letta/llm_api/groq_client.py +0 -1
  29. letta/llm_api/helpers.py +2 -2
  30. letta/llm_api/llm_api_tools.py +1 -50
  31. letta/llm_api/llm_client.py +6 -8
  32. letta/llm_api/mistral.py +1 -1
  33. letta/llm_api/openai.py +16 -13
  34. letta/llm_api/openai_client.py +31 -16
  35. letta/llm_api/together_client.py +0 -1
  36. letta/llm_api/xai_client.py +0 -1
  37. letta/local_llm/chat_completion_proxy.py +7 -6
  38. letta/local_llm/settings/settings.py +1 -1
  39. letta/orm/__init__.py +1 -0
  40. letta/orm/agent.py +8 -6
  41. letta/orm/archive.py +9 -1
  42. letta/orm/block.py +3 -4
  43. letta/orm/block_history.py +3 -1
  44. letta/orm/group.py +2 -3
  45. letta/orm/identity.py +1 -2
  46. letta/orm/job.py +1 -2
  47. letta/orm/llm_batch_items.py +1 -2
  48. letta/orm/message.py +8 -4
  49. letta/orm/mixins.py +18 -0
  50. letta/orm/organization.py +2 -0
  51. letta/orm/passage.py +8 -1
  52. letta/orm/passage_tag.py +55 -0
  53. letta/orm/sandbox_config.py +1 -3
  54. letta/orm/step.py +1 -2
  55. letta/orm/tool.py +1 -0
  56. letta/otel/resource.py +2 -2
  57. letta/plugins/plugins.py +1 -1
  58. letta/prompts/prompt_generator.py +10 -2
  59. letta/schemas/agent.py +11 -0
  60. letta/schemas/archive.py +4 -0
  61. letta/schemas/block.py +13 -0
  62. letta/schemas/embedding_config.py +0 -1
  63. letta/schemas/enums.py +24 -7
  64. letta/schemas/group.py +12 -0
  65. letta/schemas/letta_message.py +55 -1
  66. letta/schemas/letta_message_content.py +28 -0
  67. letta/schemas/letta_request.py +21 -4
  68. letta/schemas/letta_stop_reason.py +9 -1
  69. letta/schemas/llm_config.py +24 -8
  70. letta/schemas/mcp.py +0 -3
  71. letta/schemas/memory.py +14 -0
  72. letta/schemas/message.py +245 -141
  73. letta/schemas/openai/chat_completion_request.py +2 -1
  74. letta/schemas/passage.py +1 -0
  75. letta/schemas/providers/bedrock.py +1 -1
  76. letta/schemas/providers/openai.py +2 -2
  77. letta/schemas/tool.py +11 -5
  78. letta/schemas/tool_execution_result.py +0 -1
  79. letta/schemas/tool_rule.py +71 -0
  80. letta/serialize_schemas/marshmallow_agent.py +1 -2
  81. letta/server/rest_api/app.py +3 -3
  82. letta/server/rest_api/auth/index.py +0 -1
  83. letta/server/rest_api/interface.py +3 -11
  84. letta/server/rest_api/redis_stream_manager.py +3 -4
  85. letta/server/rest_api/routers/v1/agents.py +143 -84
  86. letta/server/rest_api/routers/v1/blocks.py +1 -1
  87. letta/server/rest_api/routers/v1/folders.py +1 -1
  88. letta/server/rest_api/routers/v1/groups.py +23 -22
  89. letta/server/rest_api/routers/v1/internal_templates.py +68 -0
  90. letta/server/rest_api/routers/v1/sandbox_configs.py +11 -5
  91. letta/server/rest_api/routers/v1/sources.py +1 -1
  92. letta/server/rest_api/routers/v1/tools.py +167 -15
  93. letta/server/rest_api/streaming_response.py +4 -3
  94. letta/server/rest_api/utils.py +75 -18
  95. letta/server/server.py +24 -35
  96. letta/services/agent_manager.py +359 -45
  97. letta/services/agent_serialization_manager.py +23 -3
  98. letta/services/archive_manager.py +72 -3
  99. letta/services/block_manager.py +1 -2
  100. letta/services/context_window_calculator/token_counter.py +11 -6
  101. letta/services/file_manager.py +1 -3
  102. letta/services/files_agents_manager.py +2 -4
  103. letta/services/group_manager.py +73 -12
  104. letta/services/helpers/agent_manager_helper.py +5 -5
  105. letta/services/identity_manager.py +8 -3
  106. letta/services/job_manager.py +2 -14
  107. letta/services/llm_batch_manager.py +1 -3
  108. letta/services/mcp/base_client.py +1 -2
  109. letta/services/mcp_manager.py +5 -6
  110. letta/services/message_manager.py +536 -15
  111. letta/services/organization_manager.py +1 -2
  112. letta/services/passage_manager.py +287 -12
  113. letta/services/provider_manager.py +1 -3
  114. letta/services/sandbox_config_manager.py +12 -7
  115. letta/services/source_manager.py +1 -2
  116. letta/services/step_manager.py +0 -1
  117. letta/services/summarizer/summarizer.py +4 -2
  118. letta/services/telemetry_manager.py +1 -3
  119. letta/services/tool_executor/builtin_tool_executor.py +136 -316
  120. letta/services/tool_executor/core_tool_executor.py +231 -74
  121. letta/services/tool_executor/files_tool_executor.py +2 -2
  122. letta/services/tool_executor/mcp_tool_executor.py +0 -1
  123. letta/services/tool_executor/multi_agent_tool_executor.py +2 -2
  124. letta/services/tool_executor/sandbox_tool_executor.py +0 -1
  125. letta/services/tool_executor/tool_execution_sandbox.py +2 -3
  126. letta/services/tool_manager.py +181 -64
  127. letta/services/tool_sandbox/modal_deployment_manager.py +2 -2
  128. letta/services/user_manager.py +1 -2
  129. letta/settings.py +5 -3
  130. letta/streaming_interface.py +3 -3
  131. letta/system.py +1 -1
  132. letta/utils.py +0 -1
  133. {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/METADATA +11 -7
  134. {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/RECORD +137 -135
  135. letta/llm_api/deepseek.py +0 -303
  136. {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/WHEEL +0 -0
  137. {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/entry_points.txt +0 -0
  138. {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 SandboxEnvironmentVariable as PydanticEnvVar
11
- from letta.schemas.environment_variables import SandboxEnvironmentVariableCreate, SandboxEnvironmentVariableUpdate
12
- from letta.schemas.sandbox_config import LocalSandboxConfig
13
- from letta.schemas.sandbox_config import SandboxConfig as PydanticSandboxConfig
14
- from letta.schemas.sandbox_config import SandboxConfigCreate, SandboxConfigUpdate
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
@@ -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
@@ -29,7 +29,6 @@ class FeedbackType(str, Enum):
29
29
 
30
30
 
31
31
  class StepManager:
32
-
33
32
  @enforce_types
34
33
  @trace_method
35
34
  async def list_steps_async(
@@ -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 = [message.to_openai_dict() for message in messages if message.role != MessageRole.system or include_system]
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(