letta-nightly 0.8.17.dev20250722104501__py3-none-any.whl → 0.9.0.dev20250724081419__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 (96) hide show
  1. letta/__init__.py +5 -3
  2. letta/agent.py +3 -2
  3. letta/agents/base_agent.py +4 -1
  4. letta/agents/voice_agent.py +1 -0
  5. letta/constants.py +4 -2
  6. letta/functions/schema_generator.py +2 -1
  7. letta/groups/dynamic_multi_agent.py +1 -0
  8. letta/helpers/converters.py +13 -5
  9. letta/helpers/json_helpers.py +6 -1
  10. letta/llm_api/anthropic.py +2 -2
  11. letta/llm_api/aws_bedrock.py +24 -94
  12. letta/llm_api/deepseek.py +1 -1
  13. letta/llm_api/google_ai_client.py +0 -38
  14. letta/llm_api/google_constants.py +6 -3
  15. letta/llm_api/helpers.py +1 -1
  16. letta/llm_api/llm_api_tools.py +4 -7
  17. letta/llm_api/mistral.py +12 -37
  18. letta/llm_api/openai.py +17 -17
  19. letta/llm_api/sample_response_jsons/aws_bedrock.json +38 -0
  20. letta/llm_api/sample_response_jsons/lmstudio_embedding_list.json +15 -0
  21. letta/llm_api/sample_response_jsons/lmstudio_model_list.json +15 -0
  22. letta/local_llm/constants.py +2 -23
  23. letta/local_llm/json_parser.py +11 -1
  24. letta/local_llm/llm_chat_completion_wrappers/airoboros.py +9 -9
  25. letta/local_llm/llm_chat_completion_wrappers/chatml.py +7 -8
  26. letta/local_llm/llm_chat_completion_wrappers/configurable_wrapper.py +6 -6
  27. letta/local_llm/llm_chat_completion_wrappers/dolphin.py +3 -3
  28. letta/local_llm/llm_chat_completion_wrappers/simple_summary_wrapper.py +1 -1
  29. letta/local_llm/ollama/api.py +2 -2
  30. letta/orm/__init__.py +1 -0
  31. letta/orm/agent.py +33 -2
  32. letta/orm/files_agents.py +13 -10
  33. letta/orm/mixins.py +8 -0
  34. letta/orm/prompt.py +13 -0
  35. letta/orm/sqlite_functions.py +61 -17
  36. letta/otel/db_pool_monitoring.py +13 -12
  37. letta/schemas/agent.py +69 -4
  38. letta/schemas/agent_file.py +2 -0
  39. letta/schemas/block.py +11 -0
  40. letta/schemas/embedding_config.py +15 -3
  41. letta/schemas/enums.py +2 -0
  42. letta/schemas/file.py +1 -1
  43. letta/schemas/folder.py +74 -0
  44. letta/schemas/memory.py +12 -6
  45. letta/schemas/prompt.py +9 -0
  46. letta/schemas/providers/__init__.py +47 -0
  47. letta/schemas/providers/anthropic.py +78 -0
  48. letta/schemas/providers/azure.py +80 -0
  49. letta/schemas/providers/base.py +201 -0
  50. letta/schemas/providers/bedrock.py +78 -0
  51. letta/schemas/providers/cerebras.py +79 -0
  52. letta/schemas/providers/cohere.py +18 -0
  53. letta/schemas/providers/deepseek.py +63 -0
  54. letta/schemas/providers/google_gemini.py +102 -0
  55. letta/schemas/providers/google_vertex.py +54 -0
  56. letta/schemas/providers/groq.py +35 -0
  57. letta/schemas/providers/letta.py +39 -0
  58. letta/schemas/providers/lmstudio.py +97 -0
  59. letta/schemas/providers/mistral.py +41 -0
  60. letta/schemas/providers/ollama.py +151 -0
  61. letta/schemas/providers/openai.py +241 -0
  62. letta/schemas/providers/together.py +85 -0
  63. letta/schemas/providers/vllm.py +57 -0
  64. letta/schemas/providers/xai.py +66 -0
  65. letta/server/db.py +0 -5
  66. letta/server/rest_api/app.py +4 -3
  67. letta/server/rest_api/routers/v1/__init__.py +2 -0
  68. letta/server/rest_api/routers/v1/agents.py +152 -4
  69. letta/server/rest_api/routers/v1/folders.py +490 -0
  70. letta/server/rest_api/routers/v1/providers.py +2 -2
  71. letta/server/rest_api/routers/v1/sources.py +21 -26
  72. letta/server/rest_api/routers/v1/tools.py +90 -15
  73. letta/server/server.py +50 -95
  74. letta/services/agent_manager.py +420 -81
  75. letta/services/agent_serialization_manager.py +707 -0
  76. letta/services/block_manager.py +132 -11
  77. letta/services/file_manager.py +104 -29
  78. letta/services/file_processor/embedder/pinecone_embedder.py +8 -2
  79. letta/services/file_processor/file_processor.py +75 -24
  80. letta/services/file_processor/parser/markitdown_parser.py +95 -0
  81. letta/services/files_agents_manager.py +57 -17
  82. letta/services/group_manager.py +7 -0
  83. letta/services/helpers/agent_manager_helper.py +25 -15
  84. letta/services/provider_manager.py +2 -2
  85. letta/services/source_manager.py +35 -16
  86. letta/services/tool_executor/files_tool_executor.py +12 -5
  87. letta/services/tool_manager.py +12 -0
  88. letta/services/tool_sandbox/e2b_sandbox.py +52 -48
  89. letta/settings.py +9 -6
  90. letta/streaming_utils.py +2 -1
  91. letta/utils.py +34 -1
  92. {letta_nightly-0.8.17.dev20250722104501.dist-info → letta_nightly-0.9.0.dev20250724081419.dist-info}/METADATA +9 -8
  93. {letta_nightly-0.8.17.dev20250722104501.dist-info → letta_nightly-0.9.0.dev20250724081419.dist-info}/RECORD +96 -68
  94. {letta_nightly-0.8.17.dev20250722104501.dist-info → letta_nightly-0.9.0.dev20250724081419.dist-info}/LICENSE +0 -0
  95. {letta_nightly-0.8.17.dev20250722104501.dist-info → letta_nightly-0.9.0.dev20250724081419.dist-info}/WHEEL +0 -0
  96. {letta_nightly-0.8.17.dev20250722104501.dist-info → letta_nightly-0.9.0.dev20250724081419.dist-info}/entry_points.txt +0 -0
@@ -1,3 +1,4 @@
1
+ import json
1
2
  from typing import Any, Dict, List, Optional, Union
2
3
 
3
4
  from composio.client import ComposioClientError, HTTPError, NoItemsFound
@@ -17,10 +18,14 @@ from letta.functions.functions import derive_openai_json_schema
17
18
  from letta.functions.mcp_client.exceptions import MCPTimeoutError
18
19
  from letta.functions.mcp_client.types import MCPServerType, MCPTool, SSEServerConfig, StdioServerConfig, StreamableHTTPServerConfig
19
20
  from letta.helpers.composio_helpers import get_composio_api_key
21
+ from letta.llm_api.llm_client import LLMClient
20
22
  from letta.log import get_logger
21
23
  from letta.orm.errors import UniqueConstraintViolationError
24
+ from letta.schemas.enums import MessageRole
22
25
  from letta.schemas.letta_message import ToolReturnMessage
26
+ from letta.schemas.letta_message_content import TextContent
23
27
  from letta.schemas.mcp import UpdateSSEMCPServer, UpdateStdioMCPServer, UpdateStreamableHTTPMCPServer
28
+ from letta.schemas.message import Message
24
29
  from letta.schemas.tool import Tool, ToolCreate, ToolRunFromSource, ToolUpdate
25
30
  from letta.server.rest_api.utils import get_letta_server
26
31
  from letta.server.server import SyncServer
@@ -106,21 +111,6 @@ async def list_tools(
106
111
  raise HTTPException(status_code=500, detail=str(e))
107
112
 
108
113
 
109
- @router.get("/count", response_model=int, operation_id="count_tools")
110
- def count_tools(
111
- server: SyncServer = Depends(get_letta_server),
112
- actor_id: Optional[str] = Header(None, alias="user_id"),
113
- ):
114
- """
115
- Get a count of all tools available to agents belonging to the org of the user
116
- """
117
- try:
118
- return server.tool_manager.size(actor=server.user_manager.get_user_or_default(user_id=actor_id))
119
- except Exception as e:
120
- print(f"Error occurred: {e}")
121
- raise HTTPException(status_code=500, detail=str(e))
122
-
123
-
124
114
  @router.post("/", response_model=Tool, operation_id="create_tool")
125
115
  async def create_tool(
126
116
  request: ToolCreate = Body(...),
@@ -701,3 +691,88 @@ async def generate_json_schema(
701
691
 
702
692
  except Exception as e:
703
693
  raise HTTPException(status_code=400, detail=f"Failed to generate schema: {str(e)}")
694
+
695
+
696
+ class GenerateToolInput(BaseModel):
697
+ tool_name: str = Field(..., description="Name of the tool to generate code for")
698
+ prompt: str = Field(..., description="User prompt to generate code")
699
+ handle: Optional[str] = Field(None, description="Handle of the tool to generate code for")
700
+ starter_code: Optional[str] = Field(None, description="Python source code to parse for JSON schema")
701
+ validation_errors: List[str] = Field(..., description="List of validation errors")
702
+
703
+
704
+ class GenerateToolOutput(BaseModel):
705
+ tool: Tool = Field(..., description="Generated tool")
706
+ sample_args: Dict[str, Any] = Field(..., description="Sample arguments for the tool")
707
+ response: str = Field(..., description="Response from the assistant")
708
+
709
+
710
+ @router.post("/generate-tool", response_model=GenerateToolOutput, operation_id="generate_tool")
711
+ async def generate_tool_from_prompt(
712
+ request: GenerateToolInput = Body(...),
713
+ server: SyncServer = Depends(get_letta_server),
714
+ actor_id: Optional[str] = Header(None, alias="user_id"),
715
+ ):
716
+ """
717
+ Generate a tool from the given user prompt.
718
+ """
719
+ try:
720
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
721
+ llm_config = await server.get_cached_llm_config_async(actor=actor, handle=request.handle or "anthropic/claude-3-5-sonnet-20240620")
722
+ formatted_prompt = (
723
+ f"Generate a python function named {request.tool_name} using the instructions below "
724
+ + (f"based on this starter code: \n\n```\n{request.starter_code}\n```\n\n" if request.starter_code else "\n")
725
+ + (f"Note the following validation errors: \n{' '.join(request.validation_errors)}\n\n" if request.validation_errors else "\n")
726
+ + f"Instructions: {request.prompt}"
727
+ )
728
+ llm_client = LLMClient.create(
729
+ provider_type=llm_config.model_endpoint_type,
730
+ actor=actor,
731
+ )
732
+ assert llm_client is not None
733
+
734
+ input_messages = [
735
+ Message(role=MessageRole.system, content=[TextContent(text="Placeholder system message")]),
736
+ Message(role=MessageRole.assistant, content=[TextContent(text="Placeholder assistant message")]),
737
+ Message(role=MessageRole.user, content=[TextContent(text=formatted_prompt)]),
738
+ ]
739
+
740
+ tool = {
741
+ "name": "generate_tool",
742
+ "description": "This method generates the raw source code for a custom tool that can be attached to and agent for llm invocation.",
743
+ "parameters": {
744
+ "type": "object",
745
+ "properties": {
746
+ "raw_source_code": {"type": "string", "description": "The raw python source code of the custom tool."},
747
+ "sample_args_json": {
748
+ "type": "string",
749
+ "description": "The JSON dict that contains sample args for a test run of the python function. Key is the name of the function parameter and value is an example argument that is passed in.",
750
+ },
751
+ "pip_requirements_json": {
752
+ "type": "string",
753
+ "description": "Optional JSON dict that contains pip packages to be installed if needed by the source code. Key is the name of the pip package and value is the version number.",
754
+ },
755
+ },
756
+ "required": ["raw_source_code", "sample_args_json", "pip_requirements_json"],
757
+ },
758
+ }
759
+ request_data = llm_client.build_request_data(
760
+ input_messages,
761
+ llm_config,
762
+ tools=[tool],
763
+ )
764
+ response_data = await llm_client.request_async(request_data, llm_config)
765
+ response = llm_client.convert_response_to_chat_completion(response_data, input_messages, llm_config)
766
+ output = json.loads(response.choices[0].message.tool_calls[0].function.arguments)
767
+ return GenerateToolOutput(
768
+ tool=Tool(
769
+ name=request.tool_name,
770
+ source_type="python",
771
+ source_code=output["raw_source_code"],
772
+ ),
773
+ sample_args=json.loads(output["sample_args_json"]),
774
+ response=response.choices[0].message.content,
775
+ )
776
+ except Exception as e:
777
+ logger.error(f"Failed to generate tool: {str(e)}")
778
+ raise HTTPException(status_code=500, detail=f"Failed to generate tool: {str(e)}")
letta/server/server.py CHANGED
@@ -42,7 +42,6 @@ from letta.schemas.embedding_config import EmbeddingConfig
42
42
  # openai schemas
43
43
  from letta.schemas.enums import JobStatus, MessageStreamStatus, ProviderCategory, ProviderType
44
44
  from letta.schemas.environment_variables import SandboxEnvironmentVariableCreate
45
- from letta.schemas.file import FileMetadata
46
45
  from letta.schemas.group import GroupCreate, ManagerType, SleeptimeManager, VoiceSleeptimeManager
47
46
  from letta.schemas.job import Job, JobUpdate
48
47
  from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage, MessageType, ToolReturnMessage
@@ -68,8 +67,6 @@ from letta.schemas.providers import (
68
67
  OpenAIProvider,
69
68
  Provider,
70
69
  TogetherProvider,
71
- VLLMChatCompletionsProvider,
72
- VLLMCompletionsProvider,
73
70
  XAIProvider,
74
71
  )
75
72
  from letta.schemas.sandbox_config import LocalSandboxConfig, SandboxConfigCreate, SandboxType
@@ -81,9 +78,9 @@ from letta.server.rest_api.chat_completions_interface import ChatCompletionsStre
81
78
  from letta.server.rest_api.interface import StreamingServerInterface
82
79
  from letta.server.rest_api.utils import sse_async_generator
83
80
  from letta.services.agent_manager import AgentManager
81
+ from letta.services.agent_serialization_manager import AgentSerializationManager
84
82
  from letta.services.block_manager import BlockManager
85
83
  from letta.services.file_manager import FileManager
86
- from letta.services.file_processor.chunker.line_chunker import LineChunker
87
84
  from letta.services.files_agents_manager import FileAgentManager
88
85
  from letta.services.group_manager import GroupManager
89
86
  from letta.services.helpers.tool_execution_helper import prepare_local_sandbox
@@ -226,6 +223,18 @@ class SyncServer(Server):
226
223
  self.file_agent_manager = FileAgentManager()
227
224
  self.file_manager = FileManager()
228
225
 
226
+ self.agent_serialization_manager = AgentSerializationManager(
227
+ agent_manager=self.agent_manager,
228
+ tool_manager=self.tool_manager,
229
+ source_manager=self.source_manager,
230
+ block_manager=self.block_manager,
231
+ group_manager=self.group_manager,
232
+ mcp_manager=self.mcp_manager,
233
+ file_manager=self.file_manager,
234
+ file_agent_manager=self.file_agent_manager,
235
+ message_manager=self.message_manager,
236
+ )
237
+
229
238
  # A resusable httpx client
230
239
  timeout = httpx.Timeout(connect=10.0, read=20.0, write=10.0, pool=10.0)
231
240
  limits = httpx.Limits(max_connections=100, max_keepalive_connections=80, keepalive_expiry=300)
@@ -360,7 +369,7 @@ class SyncServer(Server):
360
369
  )
361
370
  )
362
371
  # NOTE: to use the /chat/completions endpoint, you need to specify extra flags on vLLM startup
363
- # see: https://docs.vllm.ai/en/latest/getting_started/examples/openai_chat_completion_client_with_tools.html
372
+ # see: https://docs.vllm.ai/en/stable/features/tool_calling.html
364
373
  # e.g. "... --enable-auto-tool-choice --tool-call-parser hermes"
365
374
  self._enabled_providers.append(
366
375
  VLLMChatCompletionsProvider(
@@ -460,7 +469,7 @@ class SyncServer(Server):
460
469
  # Determine whether or not to token stream based on the capability of the interface
461
470
  token_streaming = letta_agent.interface.streaming_mode if hasattr(letta_agent.interface, "streaming_mode") else False
462
471
 
463
- logger.debug(f"Starting agent step")
472
+ logger.debug("Starting agent step")
464
473
  if interface:
465
474
  metadata = interface.metadata if hasattr(interface, "metadata") else None
466
475
  else:
@@ -534,7 +543,7 @@ class SyncServer(Server):
534
543
  letta_agent.interface.print_messages_raw(letta_agent.messages)
535
544
 
536
545
  elif command.lower() == "memory":
537
- ret_str = f"\nDumping memory contents:\n" + f"\n{str(letta_agent.agent_state.memory)}" + f"\n{str(letta_agent.passage_manager)}"
546
+ ret_str = "\nDumping memory contents:\n" + f"\n{str(letta_agent.agent_state.memory)}" + f"\n{str(letta_agent.passage_manager)}"
538
547
  return ret_str
539
548
 
540
549
  elif command.lower() == "pop" or command.lower().startswith("pop "):
@@ -554,7 +563,7 @@ class SyncServer(Server):
554
563
 
555
564
  elif command.lower() == "retry":
556
565
  # TODO this needs to also modify the persistence manager
557
- logger.debug(f"Retrying for another answer")
566
+ logger.debug("Retrying for another answer")
558
567
  while len(letta_agent.messages) > 0:
559
568
  if letta_agent.messages[-1].get("role") == "user":
560
569
  # we want to pop up to the last user message and send it again
@@ -770,6 +779,7 @@ class SyncServer(Server):
770
779
  self._embedding_config_cache[key] = self.get_embedding_config_from_handle(actor=actor, **kwargs)
771
780
  return self._embedding_config_cache[key]
772
781
 
782
+ # @async_redis_cache(key_func=lambda (actor, **kwargs): actor.id + hash(kwargs))
773
783
  @trace_method
774
784
  async def get_cached_embedding_config_async(self, actor: User, **kwargs):
775
785
  key = make_key(**kwargs)
@@ -782,9 +792,9 @@ class SyncServer(Server):
782
792
  self,
783
793
  request: CreateAgent,
784
794
  actor: User,
785
- # interface
786
- interface: Union[AgentInterface, None] = None,
795
+ interface: AgentInterface | None = None,
787
796
  ) -> AgentState:
797
+ warnings.warn("This method is deprecated, use create_agent_async where possible.", DeprecationWarning, stacklevel=2)
788
798
  if request.llm_config is None:
789
799
  if request.model is None:
790
800
  raise ValueError("Must specify either model or llm_config in request")
@@ -873,7 +883,9 @@ class SyncServer(Server):
873
883
  if request.source_ids:
874
884
  for source_id in request.source_ids:
875
885
  files = await self.file_manager.list_files(source_id, actor, include_content=True)
876
- await self.insert_files_into_context_window(agent_state=main_agent, file_metadata_with_content=files, actor=actor)
886
+ await self.agent_manager.insert_files_into_context_window(
887
+ agent_state=main_agent, file_metadata_with_content=files, actor=actor
888
+ )
877
889
 
878
890
  main_agent = await self.agent_manager.refresh_file_blocks(agent_state=main_agent, actor=actor)
879
891
  main_agent = await self.agent_manager.attach_missing_files_tools_async(agent_state=main_agent, actor=actor)
@@ -1320,7 +1332,6 @@ class SyncServer(Server):
1320
1332
  # TODO: delete data from agent passage stores (?)
1321
1333
 
1322
1334
  async def load_file_to_source(self, source_id: str, file_path: str, job_id: str, actor: User) -> Job:
1323
-
1324
1335
  # update job
1325
1336
  job = await self.job_manager.get_job_by_id_async(job_id, actor=actor)
1326
1337
  job.status = JobStatus.running
@@ -1397,90 +1408,28 @@ class SyncServer(Server):
1397
1408
  except NoResultFound:
1398
1409
  logger.info(f"File {file_id} already removed from agent {agent_id}, skipping...")
1399
1410
 
1400
- async def insert_file_into_context_windows(
1401
- self, source_id: str, file_metadata_with_content: FileMetadata, actor: User, agent_states: Optional[List[AgentState]] = None
1402
- ) -> List[AgentState]:
1403
- """
1404
- Insert the uploaded document into the context window of all agents
1405
- attached to the given source.
1406
- """
1407
- agent_states = agent_states or await self.source_manager.list_attached_agents(source_id=source_id, actor=actor)
1408
-
1409
- # Return early
1410
- if not agent_states:
1411
- return []
1412
-
1413
- logger.info(f"Inserting document into context window for source: {source_id}")
1414
- logger.info(f"Attached agents: {[a.id for a in agent_states]}")
1415
-
1416
- # Generate visible content for the file
1417
- line_chunker = LineChunker()
1418
- content_lines = line_chunker.chunk_text(file_metadata=file_metadata_with_content)
1419
- visible_content = "\n".join(content_lines)
1420
- visible_content_map = {file_metadata_with_content.file_name: visible_content}
1421
-
1422
- # Attach file to each agent using bulk method (one file per agent, but atomic per agent)
1423
- all_closed_files = await asyncio.gather(
1424
- *(
1425
- self.file_agent_manager.attach_files_bulk(
1426
- agent_id=agent_state.id,
1427
- files_metadata=[file_metadata_with_content],
1428
- visible_content_map=visible_content_map,
1429
- actor=actor,
1430
- )
1431
- for agent_state in agent_states
1432
- )
1433
- )
1434
- # Flatten and log if any files were closed
1435
- closed_files = [file for closed_list in all_closed_files for file in closed_list]
1436
- if closed_files:
1437
- logger.info(f"LRU eviction closed {len(closed_files)} files during bulk attach: {closed_files}")
1438
-
1439
- return agent_states
1440
-
1441
- async def insert_files_into_context_window(
1442
- self, agent_state: AgentState, file_metadata_with_content: List[FileMetadata], actor: User
1443
- ) -> None:
1444
- """
1445
- Insert the uploaded documents into the context window of an agent
1446
- attached to the given source.
1447
- """
1448
- logger.info(f"Inserting {len(file_metadata_with_content)} documents into context window for agent_state: {agent_state.id}")
1449
-
1450
- # Generate visible content for each file
1451
- line_chunker = LineChunker()
1452
- visible_content_map = {}
1453
- for file_metadata in file_metadata_with_content:
1454
- content_lines = line_chunker.chunk_text(file_metadata=file_metadata)
1455
- visible_content_map[file_metadata.file_name] = "\n".join(content_lines)
1456
-
1457
- # Use bulk attach to avoid race conditions and duplicate LRU eviction decisions
1458
- closed_files = await self.file_agent_manager.attach_files_bulk(
1459
- agent_id=agent_state.id,
1460
- files_metadata=file_metadata_with_content,
1461
- visible_content_map=visible_content_map,
1462
- actor=actor,
1463
- )
1464
-
1465
- if closed_files:
1466
- logger.info(f"LRU eviction closed {len(closed_files)} files during bulk insert: {closed_files}")
1467
-
1468
1411
  async def remove_file_from_context_windows(self, source_id: str, file_id: str, actor: User) -> None:
1469
1412
  """
1470
1413
  Remove the document from the context window of all agents
1471
1414
  attached to the given source.
1472
1415
  """
1473
- # TODO: We probably do NOT need to get the entire agent state, we can just get the IDs
1474
- agent_states = await self.source_manager.list_attached_agents(source_id=source_id, actor=actor)
1416
+ # Use the optimized ids_only parameter
1417
+ agent_ids = await self.source_manager.list_attached_agents(source_id=source_id, actor=actor, ids_only=True)
1475
1418
 
1476
- # Return early
1477
- if not agent_states:
1419
+ # Return early if no agents
1420
+ if not agent_ids:
1478
1421
  return
1479
1422
 
1480
1423
  logger.info(f"Removing file from context window for source: {source_id}")
1481
- logger.info(f"Attached agents: {[a.id for a in agent_states]}")
1424
+ logger.info(f"Attached agents: {agent_ids}")
1425
+
1426
+ # Create agent-file pairs for bulk deletion
1427
+ agent_file_pairs = [(agent_id, file_id) for agent_id in agent_ids]
1428
+
1429
+ # Bulk delete in a single query
1430
+ deleted_count = await self.file_agent_manager.detach_file_bulk(agent_file_pairs=agent_file_pairs, actor=actor)
1482
1431
 
1483
- await asyncio.gather(*(self._remove_file_from_agent(agent_state.id, file_id, actor) for agent_state in agent_states))
1432
+ logger.info(f"Removed file {file_id} from {deleted_count} agent context windows")
1484
1433
 
1485
1434
  async def remove_files_from_context_window(self, agent_state: AgentState, file_ids: List[str], actor: User) -> None:
1486
1435
  """
@@ -1490,7 +1439,13 @@ class SyncServer(Server):
1490
1439
  logger.info(f"Removing files from context window for agent_state: {agent_state.id}")
1491
1440
  logger.info(f"Files to remove: {file_ids}")
1492
1441
 
1493
- await asyncio.gather(*(self._remove_file_from_agent(agent_state.id, file_id, actor) for file_id in file_ids))
1442
+ # Create agent-file pairs for bulk deletion
1443
+ agent_file_pairs = [(agent_state.id, file_id) for file_id in file_ids]
1444
+
1445
+ # Bulk delete in a single query
1446
+ deleted_count = await self.file_agent_manager.detach_file_bulk(agent_file_pairs=agent_file_pairs, actor=actor)
1447
+
1448
+ logger.info(f"Removed {deleted_count} files from agent {agent_state.id}")
1494
1449
 
1495
1450
  async def create_document_sleeptime_agent_async(
1496
1451
  self, main_agent: AgentState, source: Source, actor: User, clear_history: bool = False
@@ -1562,7 +1517,6 @@ class SyncServer(Server):
1562
1517
  # Add extra metadata to the sources
1563
1518
  sources_with_metadata = []
1564
1519
  for source in sources:
1565
-
1566
1520
  # count number of passages
1567
1521
  num_passages = self.agent_manager.passage_size(actor=actor, source_id=source.id)
1568
1522
 
@@ -1932,7 +1886,9 @@ class SyncServer(Server):
1932
1886
  def get_provider_from_name(self, provider_name: str, actor: User) -> Provider:
1933
1887
  providers = [provider for provider in self.get_enabled_providers(actor) if provider.name == provider_name]
1934
1888
  if not providers:
1935
- raise ValueError(f"Provider {provider_name} is not supported")
1889
+ raise ValueError(
1890
+ f"Provider {provider_name} is not supported (supported providers: {', '.join([provider.name for provider in self._enabled_providers])})"
1891
+ )
1936
1892
  elif len(providers) > 1:
1937
1893
  raise ValueError(f"Multiple providers with name {provider_name} supported")
1938
1894
  else:
@@ -1944,7 +1900,9 @@ class SyncServer(Server):
1944
1900
  all_providers = await self.get_enabled_providers_async(actor)
1945
1901
  providers = [provider for provider in all_providers if provider.name == provider_name]
1946
1902
  if not providers:
1947
- raise ValueError(f"Provider {provider_name} is not supported")
1903
+ raise ValueError(
1904
+ f"Provider {provider_name} is not supported (supported providers: {', '.join([provider.name for provider in self._enabled_providers])})"
1905
+ )
1948
1906
  elif len(providers) > 1:
1949
1907
  raise ValueError(f"Multiple providers with name {provider_name} supported")
1950
1908
  else:
@@ -2112,7 +2070,6 @@ class SyncServer(Server):
2112
2070
  mcp_config_path = os.path.join(constants.LETTA_DIR, constants.MCP_CONFIG_NAME)
2113
2071
  if os.path.exists(mcp_config_path):
2114
2072
  with open(mcp_config_path, "r") as f:
2115
-
2116
2073
  try:
2117
2074
  mcp_config = json.load(f)
2118
2075
  except Exception as e:
@@ -2124,7 +2081,6 @@ class SyncServer(Server):
2124
2081
  # with the value being the schema from StdioServerParameters
2125
2082
  if MCP_CONFIG_TOPLEVEL_KEY in mcp_config:
2126
2083
  for server_name, server_params_raw in mcp_config[MCP_CONFIG_TOPLEVEL_KEY].items():
2127
-
2128
2084
  # No support for duplicate server names
2129
2085
  if server_name in mcp_server_list:
2130
2086
  logger.error(f"Duplicate MCP server name found (skipping): {server_name}")
@@ -2295,7 +2251,6 @@ class SyncServer(Server):
2295
2251
 
2296
2252
  # For streaming response
2297
2253
  try:
2298
-
2299
2254
  # TODO: move this logic into server.py
2300
2255
 
2301
2256
  # Get the generator object off of the agent's streaming interface
@@ -2435,9 +2390,9 @@ class SyncServer(Server):
2435
2390
  if not stream_steps and stream_tokens:
2436
2391
  raise ValueError("stream_steps must be 'true' if stream_tokens is 'true'")
2437
2392
 
2438
- group = self.group_manager.retrieve_group(group_id=group_id, actor=actor)
2393
+ group = await self.group_manager.retrieve_group_async(group_id=group_id, actor=actor)
2439
2394
  agent_state_id = group.manager_agent_id or (group.agent_ids[0] if len(group.agent_ids) > 0 else None)
2440
- agent_state = self.agent_manager.get_agent_by_id(agent_id=agent_state_id, actor=actor) if agent_state_id else None
2395
+ agent_state = await self.agent_manager.get_agent_by_id_async(agent_id=agent_state_id, actor=actor) if agent_state_id else None
2441
2396
  letta_multi_agent = load_multi_agent(group=group, agent_state=agent_state, actor=actor)
2442
2397
 
2443
2398
  llm_config = letta_multi_agent.agent_state.llm_config