letta-nightly 0.7.6.dev20250430104233__py3-none-any.whl → 0.7.8.dev20250501064110__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 (70) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +8 -12
  3. letta/agents/exceptions.py +6 -0
  4. letta/agents/helpers.py +1 -1
  5. letta/agents/letta_agent.py +48 -35
  6. letta/agents/letta_agent_batch.py +6 -2
  7. letta/agents/voice_agent.py +41 -59
  8. letta/agents/{ephemeral_memory_agent.py → voice_sleeptime_agent.py} +106 -129
  9. letta/client/client.py +3 -3
  10. letta/constants.py +18 -2
  11. letta/functions/composio_helpers.py +100 -0
  12. letta/functions/function_sets/base.py +0 -10
  13. letta/functions/function_sets/voice.py +92 -0
  14. letta/functions/functions.py +4 -2
  15. letta/functions/helpers.py +19 -101
  16. letta/groups/helpers.py +1 -0
  17. letta/groups/sleeptime_multi_agent.py +5 -1
  18. letta/helpers/message_helper.py +21 -4
  19. letta/helpers/tool_execution_helper.py +1 -1
  20. letta/interfaces/anthropic_streaming_interface.py +165 -158
  21. letta/interfaces/openai_chat_completions_streaming_interface.py +1 -1
  22. letta/llm_api/anthropic.py +15 -10
  23. letta/llm_api/anthropic_client.py +5 -1
  24. letta/llm_api/google_vertex_client.py +1 -1
  25. letta/llm_api/llm_api_tools.py +7 -0
  26. letta/llm_api/llm_client.py +12 -2
  27. letta/llm_api/llm_client_base.py +4 -0
  28. letta/llm_api/openai.py +9 -3
  29. letta/llm_api/openai_client.py +18 -4
  30. letta/memory.py +3 -1
  31. letta/orm/enums.py +1 -0
  32. letta/orm/group.py +2 -0
  33. letta/orm/provider.py +10 -0
  34. letta/personas/examples/voice_memory_persona.txt +5 -0
  35. letta/prompts/system/voice_chat.txt +29 -0
  36. letta/prompts/system/voice_sleeptime.txt +74 -0
  37. letta/schemas/agent.py +14 -2
  38. letta/schemas/enums.py +11 -0
  39. letta/schemas/group.py +37 -2
  40. letta/schemas/llm_config.py +1 -0
  41. letta/schemas/llm_config_overrides.py +2 -2
  42. letta/schemas/message.py +4 -3
  43. letta/schemas/providers.py +75 -213
  44. letta/schemas/tool.py +8 -12
  45. letta/server/rest_api/app.py +12 -0
  46. letta/server/rest_api/chat_completions_interface.py +1 -1
  47. letta/server/rest_api/interface.py +8 -10
  48. letta/server/rest_api/{optimistic_json_parser.py → json_parser.py} +62 -26
  49. letta/server/rest_api/routers/v1/agents.py +1 -1
  50. letta/server/rest_api/routers/v1/embeddings.py +4 -3
  51. letta/server/rest_api/routers/v1/llms.py +4 -3
  52. letta/server/rest_api/routers/v1/providers.py +4 -1
  53. letta/server/rest_api/routers/v1/voice.py +0 -2
  54. letta/server/rest_api/utils.py +22 -33
  55. letta/server/server.py +91 -37
  56. letta/services/agent_manager.py +14 -7
  57. letta/services/group_manager.py +61 -0
  58. letta/services/helpers/agent_manager_helper.py +69 -12
  59. letta/services/message_manager.py +2 -2
  60. letta/services/passage_manager.py +13 -4
  61. letta/services/provider_manager.py +25 -14
  62. letta/services/summarizer/summarizer.py +20 -15
  63. letta/services/tool_executor/tool_execution_manager.py +1 -1
  64. letta/services/tool_executor/tool_executor.py +3 -3
  65. letta/services/tool_manager.py +32 -7
  66. {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/METADATA +4 -5
  67. {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/RECORD +70 -64
  68. {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/LICENSE +0 -0
  69. {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/WHEEL +0 -0
  70. {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/entry_points.txt +0 -0
letta/server/server.py CHANGED
@@ -44,7 +44,7 @@ from letta.schemas.embedding_config import EmbeddingConfig
44
44
  # openai schemas
45
45
  from letta.schemas.enums import JobStatus, MessageStreamStatus
46
46
  from letta.schemas.environment_variables import SandboxEnvironmentVariableCreate
47
- from letta.schemas.group import GroupCreate, SleeptimeManager
47
+ from letta.schemas.group import GroupCreate, ManagerType, SleeptimeManager, VoiceSleeptimeManager
48
48
  from letta.schemas.job import Job, JobUpdate
49
49
  from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage, ToolReturnMessage
50
50
  from letta.schemas.letta_message_content import TextContent
@@ -268,10 +268,11 @@ class SyncServer(Server):
268
268
  )
269
269
 
270
270
  # collect providers (always has Letta as a default)
271
- self._enabled_providers: List[Provider] = [LettaProvider()]
271
+ self._enabled_providers: List[Provider] = [LettaProvider(name="letta")]
272
272
  if model_settings.openai_api_key:
273
273
  self._enabled_providers.append(
274
274
  OpenAIProvider(
275
+ name="openai",
275
276
  api_key=model_settings.openai_api_key,
276
277
  base_url=model_settings.openai_api_base,
277
278
  )
@@ -279,12 +280,14 @@ class SyncServer(Server):
279
280
  if model_settings.anthropic_api_key:
280
281
  self._enabled_providers.append(
281
282
  AnthropicProvider(
283
+ name="anthropic",
282
284
  api_key=model_settings.anthropic_api_key,
283
285
  )
284
286
  )
285
287
  if model_settings.ollama_base_url:
286
288
  self._enabled_providers.append(
287
289
  OllamaProvider(
290
+ name="ollama",
288
291
  base_url=model_settings.ollama_base_url,
289
292
  api_key=None,
290
293
  default_prompt_formatter=model_settings.default_prompt_formatter,
@@ -293,12 +296,14 @@ class SyncServer(Server):
293
296
  if model_settings.gemini_api_key:
294
297
  self._enabled_providers.append(
295
298
  GoogleAIProvider(
299
+ name="google_ai",
296
300
  api_key=model_settings.gemini_api_key,
297
301
  )
298
302
  )
299
303
  if model_settings.google_cloud_location and model_settings.google_cloud_project:
300
304
  self._enabled_providers.append(
301
305
  GoogleVertexProvider(
306
+ name="google_vertex",
302
307
  google_cloud_project=model_settings.google_cloud_project,
303
308
  google_cloud_location=model_settings.google_cloud_location,
304
309
  )
@@ -307,6 +312,7 @@ class SyncServer(Server):
307
312
  assert model_settings.azure_api_version, "AZURE_API_VERSION is required"
308
313
  self._enabled_providers.append(
309
314
  AzureProvider(
315
+ name="azure",
310
316
  api_key=model_settings.azure_api_key,
311
317
  base_url=model_settings.azure_base_url,
312
318
  api_version=model_settings.azure_api_version,
@@ -315,12 +321,14 @@ class SyncServer(Server):
315
321
  if model_settings.groq_api_key:
316
322
  self._enabled_providers.append(
317
323
  GroqProvider(
324
+ name="groq",
318
325
  api_key=model_settings.groq_api_key,
319
326
  )
320
327
  )
321
328
  if model_settings.together_api_key:
322
329
  self._enabled_providers.append(
323
330
  TogetherProvider(
331
+ name="together",
324
332
  api_key=model_settings.together_api_key,
325
333
  default_prompt_formatter=model_settings.default_prompt_formatter,
326
334
  )
@@ -329,6 +337,7 @@ class SyncServer(Server):
329
337
  # vLLM exposes both a /chat/completions and a /completions endpoint
330
338
  self._enabled_providers.append(
331
339
  VLLMCompletionsProvider(
340
+ name="vllm",
332
341
  base_url=model_settings.vllm_api_base,
333
342
  default_prompt_formatter=model_settings.default_prompt_formatter,
334
343
  )
@@ -338,12 +347,14 @@ class SyncServer(Server):
338
347
  # e.g. "... --enable-auto-tool-choice --tool-call-parser hermes"
339
348
  self._enabled_providers.append(
340
349
  VLLMChatCompletionsProvider(
350
+ name="vllm",
341
351
  base_url=model_settings.vllm_api_base,
342
352
  )
343
353
  )
344
354
  if model_settings.aws_access_key and model_settings.aws_secret_access_key and model_settings.aws_region:
345
355
  self._enabled_providers.append(
346
356
  AnthropicBedrockProvider(
357
+ name="bedrock",
347
358
  aws_region=model_settings.aws_region,
348
359
  )
349
360
  )
@@ -355,37 +366,37 @@ class SyncServer(Server):
355
366
  if model_settings.lmstudio_base_url.endswith("/v1")
356
367
  else model_settings.lmstudio_base_url + "/v1"
357
368
  )
358
- self._enabled_providers.append(LMStudioOpenAIProvider(base_url=lmstudio_url))
369
+ self._enabled_providers.append(LMStudioOpenAIProvider(name="lmstudio_openai", base_url=lmstudio_url))
359
370
  if model_settings.deepseek_api_key:
360
- self._enabled_providers.append(DeepSeekProvider(api_key=model_settings.deepseek_api_key))
371
+ self._enabled_providers.append(DeepSeekProvider(name="deepseek", api_key=model_settings.deepseek_api_key))
361
372
  if model_settings.xai_api_key:
362
- self._enabled_providers.append(XAIProvider(api_key=model_settings.xai_api_key))
373
+ self._enabled_providers.append(XAIProvider(name="xai", api_key=model_settings.xai_api_key))
363
374
 
364
375
  # For MCP
365
376
  """Initialize the MCP clients (there may be multiple)"""
366
- # mcp_server_configs = self.get_mcp_servers()
377
+ mcp_server_configs = self.get_mcp_servers()
367
378
  self.mcp_clients: Dict[str, BaseMCPClient] = {}
368
- #
369
- # for server_name, server_config in mcp_server_configs.items():
370
- # if server_config.type == MCPServerType.SSE:
371
- # self.mcp_clients[server_name] = SSEMCPClient(server_config)
372
- # elif server_config.type == MCPServerType.STDIO:
373
- # self.mcp_clients[server_name] = StdioMCPClient(server_config)
374
- # else:
375
- # raise ValueError(f"Invalid MCP server config: {server_config}")
376
- #
377
- # try:
378
- # self.mcp_clients[server_name].connect_to_server()
379
- # except Exception as e:
380
- # logger.error(e)
381
- # self.mcp_clients.pop(server_name)
382
- #
383
- # # Print out the tools that are connected
384
- # for server_name, client in self.mcp_clients.items():
385
- # logger.info(f"Attempting to fetch tools from MCP server: {server_name}")
386
- # mcp_tools = client.list_tools()
387
- # logger.info(f"MCP tools connected: {', '.join([t.name for t in mcp_tools])}")
388
- # logger.debug(f"MCP tools: {', '.join([str(t) for t in mcp_tools])}")
379
+
380
+ for server_name, server_config in mcp_server_configs.items():
381
+ if server_config.type == MCPServerType.SSE:
382
+ self.mcp_clients[server_name] = SSEMCPClient(server_config)
383
+ elif server_config.type == MCPServerType.STDIO:
384
+ self.mcp_clients[server_name] = StdioMCPClient(server_config)
385
+ else:
386
+ raise ValueError(f"Invalid MCP server config: {server_config}")
387
+
388
+ try:
389
+ self.mcp_clients[server_name].connect_to_server()
390
+ except Exception as e:
391
+ logger.error(e)
392
+ self.mcp_clients.pop(server_name)
393
+
394
+ # Print out the tools that are connected
395
+ for server_name, client in self.mcp_clients.items():
396
+ logger.info(f"Attempting to fetch tools from MCP server: {server_name}")
397
+ mcp_tools = client.list_tools()
398
+ logger.info(f"MCP tools connected: {', '.join([t.name for t in mcp_tools])}")
399
+ logger.debug(f"MCP tools: {', '.join([str(t) for t in mcp_tools])}")
389
400
 
390
401
  # TODO: Remove these in memory caches
391
402
  self._llm_config_cache = {}
@@ -397,7 +408,9 @@ class SyncServer(Server):
397
408
  def load_agent(self, agent_id: str, actor: User, interface: Union[AgentInterface, None] = None) -> Agent:
398
409
  """Updated method to load agents from persisted storage"""
399
410
  agent_state = self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor)
400
- if agent_state.multi_agent_group:
411
+ # TODO: Think about how to integrate voice sleeptime into sleeptime
412
+ # TODO: Voice sleeptime agents turn into normal agents when being messaged
413
+ if agent_state.multi_agent_group and agent_state.multi_agent_group.manager_type != ManagerType.voice_sleeptime:
401
414
  return load_multi_agent(
402
415
  group=agent_state.multi_agent_group, agent_state=agent_state, actor=actor, interface=interface, mcp_clients=self.mcp_clients
403
416
  )
@@ -769,7 +782,10 @@ class SyncServer(Server):
769
782
  log_event(name="end create_agent db")
770
783
 
771
784
  if request.enable_sleeptime:
772
- main_agent = self.create_sleeptime_agent(main_agent=main_agent, actor=actor)
785
+ if request.agent_type == AgentType.voice_convo_agent:
786
+ main_agent = self.create_voice_sleeptime_agent(main_agent=main_agent, actor=actor)
787
+ else:
788
+ main_agent = self.create_sleeptime_agent(main_agent=main_agent, actor=actor)
773
789
 
774
790
  return main_agent
775
791
 
@@ -788,7 +804,10 @@ class SyncServer(Server):
788
804
  if request.enable_sleeptime:
789
805
  agent = self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor)
790
806
  if agent.multi_agent_group is None:
791
- self.create_sleeptime_agent(main_agent=agent, actor=actor)
807
+ if agent.agent_type == AgentType.voice_convo_agent:
808
+ self.create_voice_sleeptime_agent(main_agent=agent, actor=actor)
809
+ else:
810
+ self.create_sleeptime_agent(main_agent=agent, actor=actor)
792
811
 
793
812
  return self.agent_manager.update_agent(
794
813
  agent_id=agent_id,
@@ -828,6 +847,40 @@ class SyncServer(Server):
828
847
  )
829
848
  return self.agent_manager.get_agent_by_id(agent_id=main_agent.id, actor=actor)
830
849
 
850
+ def create_voice_sleeptime_agent(self, main_agent: AgentState, actor: User) -> AgentState:
851
+ # TODO: Inject system
852
+ request = CreateAgent(
853
+ name=main_agent.name + "-sleeptime",
854
+ agent_type=AgentType.voice_sleeptime_agent,
855
+ block_ids=[block.id for block in main_agent.memory.blocks],
856
+ memory_blocks=[
857
+ CreateBlock(
858
+ label="memory_persona",
859
+ value=get_persona_text("voice_memory_persona"),
860
+ ),
861
+ ],
862
+ llm_config=main_agent.llm_config,
863
+ embedding_config=main_agent.embedding_config,
864
+ project_id=main_agent.project_id,
865
+ )
866
+ voice_sleeptime_agent = self.agent_manager.create_agent(
867
+ agent_create=request,
868
+ actor=actor,
869
+ )
870
+ self.group_manager.create_group(
871
+ group=GroupCreate(
872
+ description="Low latency voice chat with async memory management.",
873
+ agent_ids=[voice_sleeptime_agent.id],
874
+ manager_config=VoiceSleeptimeManager(
875
+ manager_agent_id=main_agent.id,
876
+ max_message_buffer_length=constants.DEFAULT_MAX_MESSAGE_BUFFER_LENGTH,
877
+ min_message_buffer_length=constants.DEFAULT_MIN_MESSAGE_BUFFER_LENGTH,
878
+ ),
879
+ ),
880
+ actor=actor,
881
+ )
882
+ return self.agent_manager.get_agent_by_id(agent_id=main_agent.id, actor=actor)
883
+
831
884
  # convert name->id
832
885
 
833
886
  # TODO: These can be moved to agent_manager
@@ -1142,10 +1195,10 @@ class SyncServer(Server):
1142
1195
  except NoResultFound:
1143
1196
  raise HTTPException(status_code=404, detail=f"Organization with id {org_id} not found")
1144
1197
 
1145
- def list_llm_models(self) -> List[LLMConfig]:
1198
+ def list_llm_models(self, byok_only: bool = False) -> List[LLMConfig]:
1146
1199
  """List available models"""
1147
1200
  llm_models = []
1148
- for provider in self.get_enabled_providers():
1201
+ for provider in self.get_enabled_providers(byok_only=byok_only):
1149
1202
  try:
1150
1203
  llm_models.extend(provider.list_llm_models())
1151
1204
  except Exception as e:
@@ -1165,11 +1218,12 @@ class SyncServer(Server):
1165
1218
  warnings.warn(f"An error occurred while listing embedding models for provider {provider}: {e}")
1166
1219
  return embedding_models
1167
1220
 
1168
- def get_enabled_providers(self):
1221
+ def get_enabled_providers(self, byok_only: bool = False):
1222
+ providers_from_db = {p.name: p.cast_to_subtype() for p in self.provider_manager.list_providers()}
1223
+ if byok_only:
1224
+ return list(providers_from_db.values())
1169
1225
  providers_from_env = {p.name: p for p in self._enabled_providers}
1170
- providers_from_db = {p.name: p for p in self.provider_manager.list_providers()}
1171
- # Merge the two dictionaries, keeping the values from providers_from_db where conflicts occur
1172
- return {**providers_from_env, **providers_from_db}.values()
1226
+ return list(providers_from_env.values()) + list(providers_from_db.values())
1173
1227
 
1174
1228
  @trace_method
1175
1229
  def get_llm_config_from_handle(
@@ -1254,7 +1308,7 @@ class SyncServer(Server):
1254
1308
  return embedding_config
1255
1309
 
1256
1310
  def get_provider_from_name(self, provider_name: str) -> Provider:
1257
- providers = [provider for provider in self._enabled_providers if provider.name == provider_name]
1311
+ providers = [provider for provider in self.get_enabled_providers() if provider.name == provider_name]
1258
1312
  if not providers:
1259
1313
  raise ValueError(f"Provider {provider_name} is not supported")
1260
1314
  elif len(providers) > 1:
@@ -11,6 +11,8 @@ from letta.constants import (
11
11
  BASE_SLEEPTIME_CHAT_TOOLS,
12
12
  BASE_SLEEPTIME_TOOLS,
13
13
  BASE_TOOLS,
14
+ BASE_VOICE_SLEEPTIME_CHAT_TOOLS,
15
+ BASE_VOICE_SLEEPTIME_TOOLS,
14
16
  DATA_SOURCE_ATTACH_ALERT,
15
17
  MAX_EMBEDDING_DIM,
16
18
  MULTI_AGENT_TOOLS,
@@ -179,7 +181,11 @@ class AgentManager:
179
181
  # tools
180
182
  tool_names = set(agent_create.tools or [])
181
183
  if agent_create.include_base_tools:
182
- if agent_create.agent_type == AgentType.sleeptime_agent:
184
+ if agent_create.agent_type == AgentType.voice_sleeptime_agent:
185
+ tool_names |= set(BASE_VOICE_SLEEPTIME_TOOLS)
186
+ elif agent_create.agent_type == AgentType.voice_convo_agent:
187
+ tool_names |= set(BASE_VOICE_SLEEPTIME_CHAT_TOOLS)
188
+ elif agent_create.agent_type == AgentType.sleeptime_agent:
183
189
  tool_names |= set(BASE_SLEEPTIME_TOOLS)
184
190
  elif agent_create.enable_sleeptime:
185
191
  tool_names |= set(BASE_SLEEPTIME_CHAT_TOOLS)
@@ -603,12 +609,13 @@ class AgentManager:
603
609
  # Delete sleeptime agent and group (TODO this is flimsy pls fix)
604
610
  if agent.multi_agent_group:
605
611
  participant_agent_ids = agent.multi_agent_group.agent_ids
606
- if agent.multi_agent_group.manager_type == ManagerType.sleeptime and len(participant_agent_ids) == 1:
607
- try:
608
- sleeptime_agent = AgentModel.read(db_session=session, identifier=participant_agent_ids[0], actor=actor)
609
- agents_to_delete.append(sleeptime_agent)
610
- except NoResultFound:
611
- pass # agent already deleted
612
+ if agent.multi_agent_group.manager_type in {ManagerType.sleeptime, ManagerType.voice_sleeptime} and participant_agent_ids:
613
+ for participant_agent_id in participant_agent_ids:
614
+ try:
615
+ sleeptime_agent = AgentModel.read(db_session=session, identifier=participant_agent_id, actor=actor)
616
+ agents_to_delete.append(sleeptime_agent)
617
+ except NoResultFound:
618
+ pass # agent already deleted
612
619
  sleeptime_agent_group = GroupModel.read(db_session=session, identifier=agent.multi_agent_group.id, actor=actor)
613
620
  sleeptime_group_to_delete = sleeptime_agent_group
614
621
 
@@ -77,6 +77,15 @@ class GroupManager:
77
77
  new_group.sleeptime_agent_frequency = group.manager_config.sleeptime_agent_frequency
78
78
  if new_group.sleeptime_agent_frequency:
79
79
  new_group.turns_counter = -1
80
+ case ManagerType.voice_sleeptime:
81
+ new_group.manager_type = ManagerType.voice_sleeptime
82
+ new_group.manager_agent_id = group.manager_config.manager_agent_id
83
+ max_message_buffer_length = group.manager_config.max_message_buffer_length
84
+ min_message_buffer_length = group.manager_config.min_message_buffer_length
85
+ # Safety check for buffer length range
86
+ self.ensure_buffer_length_range_valid(max_value=max_message_buffer_length, min_value=min_message_buffer_length)
87
+ new_group.max_message_buffer_length = max_message_buffer_length
88
+ new_group.min_message_buffer_length = min_message_buffer_length
80
89
  case _:
81
90
  raise ValueError(f"Unsupported manager type: {group.manager_config.manager_type}")
82
91
 
@@ -94,6 +103,8 @@ class GroupManager:
94
103
  group = GroupModel.read(db_session=session, identifier=group_id, actor=actor)
95
104
 
96
105
  sleeptime_agent_frequency = None
106
+ max_message_buffer_length = None
107
+ min_message_buffer_length = None
97
108
  max_turns = None
98
109
  termination_token = None
99
110
  manager_agent_id = None
@@ -114,11 +125,24 @@ class GroupManager:
114
125
  sleeptime_agent_frequency = group_update.manager_config.sleeptime_agent_frequency
115
126
  if sleeptime_agent_frequency and group.turns_counter is None:
116
127
  group.turns_counter = -1
128
+ case ManagerType.voice_sleeptime:
129
+ manager_agent_id = group_update.manager_config.manager_agent_id
130
+ max_message_buffer_length = group_update.manager_config.max_message_buffer_length or group.max_message_buffer_length
131
+ min_message_buffer_length = group_update.manager_config.min_message_buffer_length or group.min_message_buffer_length
132
+ if sleeptime_agent_frequency and group.turns_counter is None:
133
+ group.turns_counter = -1
117
134
  case _:
118
135
  raise ValueError(f"Unsupported manager type: {group_update.manager_config.manager_type}")
119
136
 
137
+ # Safety check for buffer length range
138
+ self.ensure_buffer_length_range_valid(max_value=max_message_buffer_length, min_value=min_message_buffer_length)
139
+
120
140
  if sleeptime_agent_frequency:
121
141
  group.sleeptime_agent_frequency = sleeptime_agent_frequency
142
+ if max_message_buffer_length:
143
+ group.max_message_buffer_length = max_message_buffer_length
144
+ if min_message_buffer_length:
145
+ group.min_message_buffer_length = min_message_buffer_length
122
146
  if max_turns:
123
147
  group.max_turns = max_turns
124
148
  if termination_token:
@@ -271,3 +295,40 @@ class GroupManager:
271
295
  if manager_agent:
272
296
  for block in blocks:
273
297
  session.add(BlocksAgents(agent_id=manager_agent.id, block_id=block.id, block_label=block.label))
298
+
299
+ @staticmethod
300
+ def ensure_buffer_length_range_valid(
301
+ max_value: Optional[int],
302
+ min_value: Optional[int],
303
+ max_name: str = "max_message_buffer_length",
304
+ min_name: str = "min_message_buffer_length",
305
+ ) -> None:
306
+ """
307
+ 1) Both-or-none: if one is set, the other must be set.
308
+ 2) Both must be ints > 4.
309
+ 3) max_value must be strictly greater than min_value.
310
+ """
311
+ # 1) require both-or-none
312
+ if (max_value is None) != (min_value is None):
313
+ raise ValueError(
314
+ f"Both '{max_name}' and '{min_name}' must be provided together " f"(got {max_name}={max_value}, {min_name}={min_value})"
315
+ )
316
+
317
+ # no further checks if neither is provided
318
+ if max_value is None:
319
+ return
320
+
321
+ # 2) type & lower‐bound checks
322
+ if not isinstance(max_value, int) or not isinstance(min_value, int):
323
+ raise ValueError(
324
+ f"Both '{max_name}' and '{min_name}' must be integers "
325
+ f"(got {max_name}={type(max_value).__name__}, {min_name}={type(min_value).__name__})"
326
+ )
327
+ if max_value <= 4 or min_value <= 4:
328
+ raise ValueError(
329
+ f"Both '{max_name}' and '{min_name}' must be greater than 4 " f"(got {max_name}={max_value}, {min_name}={min_value})"
330
+ )
331
+
332
+ # 3) ordering
333
+ if max_value <= min_value:
334
+ raise ValueError(f"'{max_name}' must be greater than '{min_name}' " f"(got {max_name}={max_value} <= {min_name}={min_value})")
@@ -20,7 +20,7 @@ from letta.schemas.message import Message, MessageCreate
20
20
  from letta.schemas.passage import Passage as PydanticPassage
21
21
  from letta.schemas.tool_rule import ToolRule
22
22
  from letta.schemas.user import User
23
- from letta.system import get_initial_boot_messages, get_login_event
23
+ from letta.system import get_initial_boot_messages, get_login_event, package_function_response
24
24
  from letta.tracing import trace_method
25
25
 
26
26
 
@@ -94,7 +94,11 @@ def _process_tags(agent: AgentModel, tags: List[str], replace=True):
94
94
  def derive_system_message(agent_type: AgentType, enable_sleeptime: Optional[bool] = None, system: Optional[str] = None):
95
95
  if system is None:
96
96
  # TODO: don't hardcode
97
- if agent_type == AgentType.memgpt_agent and not enable_sleeptime:
97
+ if agent_type == AgentType.voice_convo_agent:
98
+ system = gpt_system.get_system_text("voice_chat")
99
+ elif agent_type == AgentType.voice_sleeptime_agent:
100
+ system = gpt_system.get_system_text("voice_sleeptime")
101
+ elif agent_type == AgentType.memgpt_agent and not enable_sleeptime:
98
102
  system = gpt_system.get_system_text("memgpt_chat")
99
103
  elif agent_type == AgentType.memgpt_agent and enable_sleeptime:
100
104
  system = gpt_system.get_system_text("memgpt_sleeptime_chat")
@@ -278,23 +282,76 @@ def package_initial_message_sequence(
278
282
  packed_message = system.package_user_message(
279
283
  user_message=message_create.content,
280
284
  )
285
+ init_messages.append(
286
+ Message(
287
+ role=message_create.role,
288
+ content=[TextContent(text=packed_message)],
289
+ name=message_create.name,
290
+ organization_id=actor.organization_id,
291
+ agent_id=agent_id,
292
+ model=model,
293
+ )
294
+ )
281
295
  elif message_create.role == MessageRole.system:
282
296
  packed_message = system.package_system_message(
283
297
  system_message=message_create.content,
284
298
  )
299
+ init_messages.append(
300
+ Message(
301
+ role=message_create.role,
302
+ content=[TextContent(text=packed_message)],
303
+ name=message_create.name,
304
+ organization_id=actor.organization_id,
305
+ agent_id=agent_id,
306
+ model=model,
307
+ )
308
+ )
309
+ elif message_create.role == MessageRole.assistant:
310
+ # append tool call to send_message
311
+ import json
312
+ import uuid
313
+
314
+ from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall
315
+ from openai.types.chat.chat_completion_message_tool_call import Function as OpenAIFunction
316
+
317
+ from letta.constants import DEFAULT_MESSAGE_TOOL
318
+
319
+ tool_call_id = str(uuid.uuid4())
320
+ init_messages.append(
321
+ Message(
322
+ role=MessageRole.assistant,
323
+ content=None,
324
+ name=message_create.name,
325
+ organization_id=actor.organization_id,
326
+ agent_id=agent_id,
327
+ model=model,
328
+ tool_calls=[
329
+ OpenAIToolCall(
330
+ id=tool_call_id,
331
+ type="function",
332
+ function=OpenAIFunction(name=DEFAULT_MESSAGE_TOOL, arguments=json.dumps({"message": message_create.content})),
333
+ )
334
+ ],
335
+ )
336
+ )
337
+
338
+ # add tool return
339
+ function_response = package_function_response(True, "None")
340
+ init_messages.append(
341
+ Message(
342
+ role=MessageRole.tool,
343
+ content=[TextContent(text=function_response)],
344
+ name=message_create.name,
345
+ organization_id=actor.organization_id,
346
+ agent_id=agent_id,
347
+ model=model,
348
+ tool_call_id=tool_call_id,
349
+ )
350
+ )
285
351
  else:
352
+ # TODO: add tool call and tool return
286
353
  raise ValueError(f"Invalid message role: {message_create.role}")
287
354
 
288
- init_messages.append(
289
- Message(
290
- role=message_create.role,
291
- content=[TextContent(text=packed_message)],
292
- name=message_create.name,
293
- organization_id=actor.organization_id,
294
- agent_id=agent_id,
295
- model=model,
296
- )
297
- )
298
355
  return init_messages
299
356
 
300
357
 
@@ -122,7 +122,7 @@ class MessageManager:
122
122
  message = self.update_message_by_id(message_id=message_id, message_update=update_message, actor=actor)
123
123
 
124
124
  # convert back to LettaMessage
125
- for letta_msg in message.to_letta_message(use_assistant_message=True):
125
+ for letta_msg in message.to_letta_messages(use_assistant_message=True):
126
126
  if letta_msg.message_type == letta_message_update.message_type:
127
127
  return letta_msg
128
128
 
@@ -160,7 +160,7 @@ class MessageManager:
160
160
  message = self.update_message_by_id(message_id=message_id, message_update=update_message, actor=actor)
161
161
 
162
162
  # convert back to LettaMessage
163
- for letta_msg in message.to_letta_message(use_assistant_message=True):
163
+ for letta_msg in message.to_letta_messages(use_assistant_message=True):
164
164
  if letta_msg.message_type == letta_message_update.message_type:
165
165
  return letta_msg
166
166
 
@@ -220,15 +220,24 @@ class PassageManager:
220
220
  with self.session_maker() as session:
221
221
  return AgentPassage.size(db_session=session, actor=actor, agent_id=agent_id)
222
222
 
223
- def estimate_embeddings_size_GB(
223
+ def estimate_embeddings_size(
224
224
  self,
225
225
  actor: PydanticUser,
226
226
  agent_id: Optional[str] = None,
227
+ storage_unit: str = "GB",
227
228
  ) -> float:
228
229
  """
229
- Estimate the size of the embeddings in GB.
230
+ Estimate the size of the embeddings. Defaults to GB.
230
231
  """
232
+ BYTES_PER_STORAGE_UNIT = {
233
+ "B": 1,
234
+ "KB": 1024,
235
+ "MB": 1024**2,
236
+ "GB": 1024**3,
237
+ "TB": 1024**4,
238
+ }
239
+ if storage_unit not in BYTES_PER_STORAGE_UNIT:
240
+ raise ValueError(f"Invalid storage unit: {storage_unit}. Must be one of {list(BYTES_PER_STORAGE_UNIT.keys())}.")
231
241
  BYTES_PER_EMBEDDING_DIM = 4
232
- BYTES_PER_GB = 1024 * 1024 * 1024
233
- GB_PER_EMBEDDING = BYTES_PER_EMBEDDING_DIM / BYTES_PER_GB * MAX_EMBEDDING_DIM
242
+ GB_PER_EMBEDDING = BYTES_PER_EMBEDDING_DIM / BYTES_PER_STORAGE_UNIT[storage_unit] * MAX_EMBEDDING_DIM
234
243
  return self.size(actor=actor, agent_id=agent_id) * GB_PER_EMBEDDING
@@ -1,6 +1,7 @@
1
- from typing import List, Optional
1
+ from typing import List, Optional, Union
2
2
 
3
3
  from letta.orm.provider import Provider as ProviderModel
4
+ from letta.schemas.enums import ProviderType
4
5
  from letta.schemas.providers import Provider as PydanticProvider
5
6
  from letta.schemas.providers import ProviderUpdate
6
7
  from letta.schemas.user import User as PydanticUser
@@ -18,6 +19,9 @@ class ProviderManager:
18
19
  def create_provider(self, provider: PydanticProvider, actor: PydanticUser) -> PydanticProvider:
19
20
  """Create a new provider if it doesn't already exist."""
20
21
  with self.session_maker() as session:
22
+ if provider.name == provider.provider_type.value:
23
+ raise ValueError("Provider name must be unique and different from provider type")
24
+
21
25
  # Assign the organization id based on the actor
22
26
  provider.organization_id = actor.organization_id
23
27
 
@@ -59,29 +63,36 @@ class ProviderManager:
59
63
  session.commit()
60
64
 
61
65
  @enforce_types
62
- def list_providers(self, after: Optional[str] = None, limit: Optional[int] = 50, actor: PydanticUser = None) -> List[PydanticProvider]:
66
+ def list_providers(
67
+ self,
68
+ name: Optional[str] = None,
69
+ provider_type: Optional[ProviderType] = None,
70
+ after: Optional[str] = None,
71
+ limit: Optional[int] = 50,
72
+ actor: PydanticUser = None,
73
+ ) -> List[PydanticProvider]:
63
74
  """List all providers with optional pagination."""
75
+ filter_kwargs = {}
76
+ if name:
77
+ filter_kwargs["name"] = name
78
+ if provider_type:
79
+ filter_kwargs["provider_type"] = provider_type
64
80
  with self.session_maker() as session:
65
81
  providers = ProviderModel.list(
66
82
  db_session=session,
67
83
  after=after,
68
84
  limit=limit,
69
85
  actor=actor,
86
+ **filter_kwargs,
70
87
  )
71
88
  return [provider.to_pydantic() for provider in providers]
72
89
 
73
90
  @enforce_types
74
- def get_anthropic_override_provider_id(self) -> Optional[str]:
75
- """Helper function to fetch custom anthropic provider id for v0 BYOK feature"""
76
- anthropic_provider = [provider for provider in self.list_providers() if provider.name == "anthropic"]
77
- if len(anthropic_provider) != 0:
78
- return anthropic_provider[0].id
79
- return None
91
+ def get_provider_id_from_name(self, provider_name: Union[str, None]) -> Optional[str]:
92
+ providers = self.list_providers(name=provider_name)
93
+ return providers[0].id if providers else None
80
94
 
81
95
  @enforce_types
82
- def get_anthropic_override_key(self) -> Optional[str]:
83
- """Helper function to fetch custom anthropic key for v0 BYOK feature"""
84
- anthropic_provider = [provider for provider in self.list_providers() if provider.name == "anthropic"]
85
- if len(anthropic_provider) != 0:
86
- return anthropic_provider[0].api_key
87
- return None
96
+ def get_override_key(self, provider_name: Union[str, None]) -> Optional[str]:
97
+ providers = self.list_providers(name=provider_name)
98
+ return providers[0].api_key if providers else None